duet
https://www.anquanke.com/post/id/210160#h2-4
有且只有一次off-by-one
的机会,只有ptr_list
只有两个位置,且无法在没有delete
的情况下new
,只有calloc
,且size
范围在0x80~0x400
之间,libc
为2.29
,只能orw
。
解法一
思路
这道题的堆风水复杂程度在我做过的glibc
题里绝对可以排进前五了,所以还是稍微把过程写的细一点。
先选定一个用来smallbin_attack
的target_size
,越小越好,我这里用的是0xa0
,因为最小的0x90
被程序自身用了。(为什么说越小越好呢,因为我们需要构造的情景是在对应size
的tcache
未满的情况下将chunk
放入smallbin
,所以不能直接free
,直接free
会被放入tcache
中,只能用切割剩余的方法来间接放入,比如先申请一个大的size1
,然后释放他进入ub
,然后在申请一个小的size2
,所以ub
里剩下的被切割的size1-size2
为我们提前设定好用来smallbin_attack
的target_size
,然后再申请一个比target_size
大的chunk
,这时target_size
的chunk
就会被放入smallbin
,因为最后必须申请一个比target_size
大的chunk
,所以target_size
与越小,我们可操纵的size
范围就越大。),然后用普通的堆风水将一个0xa0
的chunk
放进smallbin
中。
之后用堆风水配合off-by-one
,构造出chunk overlapping
,这里为了表述简单,把overlapping
自己的next_chunk
的chunk
称作主chunk
,被自己prev_chunk
给overlapping
的chunk
称作从chunk
。用主chunk
去改从chunk
的size
,然后在从chunk
里的中间部位切割出一个0xa0
的chunk
,然后free
掉主chunk
,再申请一个0x300
的chunk
将0xa0
的chunk
挤入smallbin
,且其fd
和bk
正好存有heap
和libc
的地址,所以我们可以用show从chunk
来得到heapbase
和libcbase
,然后free
我们的从chunk(size已被改写且其对应的tcache在开始已布置慢)
再calloc
回来,改写位于其中间的smallbin
中的倒数第二个chunk
的fd
和bk
,进行smallbin_attack
攻击global_max_fast
。
这时候我们的从chunk
成为了新的主chunk
,那个0xa0
的chunk
成为了新的从chunk
,对主chunk
进行一系列的free+calloc
去改写从chunk
的size
和fd
,达到向main_arena
中写入一个fake_size
的效果,然后将从chunk
的size
改为fake_size
,并将其free
掉,然后再改其fd
,再calloc
出来,再将主chunk
给舍弃掉(因为他的任务已经完成了,现在这个位置要拿去做更重要的事情),再calloc
一个fake_size
的chunk
,这时就会分配到main_arena
上,我们得以控制main_arena
,进一步控制topchunk
去劫持__free_hook
,注意calloc
里在没进入_int_malloc
之前会有一步将topchunk
的size
取出来,但是貌似没进一步检测,所以topchunk
的位置必须放一个可读的地址,可先写为目标地址__free_hook-0xb58
,这时__free_hook-0xb58
处的值还不合法,绕不过后续检测,我们后续需要改其为0x21001
。
然后观察在__free_hook-0xb68-1
的位置有一个0x100
可作为fake_size
,将其写入对应size
的fastbin
中,且因为我们需要将用来控制main_arena
的chunk
给成功free
(因为一个位置是肯定不够用的),所以需要在其后布置一个next_chunk
的fake_size
,这一点在控制main_arena
的情形下配合另一个位置很容易实现。
然后calloc
一个0xf0
的chunk
,成功改__free_hook-0xb58
处值为0x21001
,然后free
掉控制main_arena
的chunk
,不断calloc
,知道申请到__free_hook
。(需要提前在main_arena
布置好数据,绕过*(main_arena+0x78) == main_arena+0x60
的检测)
至于劫持控制流:
用__IO_wfile_sync
函数中的gadget
配合setcontext
:
1 | 0x7ffff7e59462 <__GI__IO_wfile_sync+2>: push rbp |
至于找的方法么,有两种:
1
2ropper --file /path/to/file --nocolor > ./gadget_ropper
cat ./gadget_ropper | grep 'rdx' | grep ', qword ptr \[rdi' > gadget然后打开文本编辑器
ctrl+F
搜索rdx, qword ptr [rdi
,看有无合适的gadget
,这种运气好是可以找到的,但是找不到带有条件跳转语句的复杂gadget
,找不到的话再逐个看吧。这里比较幸运是有两个合适的
gadget
的:1
2
3
40x7ffff7efbe97 <__libc_cleanup_routine+7>: mov rdx,QWORD PTR [rdi+0x8]
0x7ffff7efbe9b <__libc_cleanup_routine+11>: mov rax,QWORD PTR [rdi]
0x7ffff7efbe9e <__libc_cleanup_routine+14>: mov rdi,rdx
0x7ffff7efbea1 <__libc_cleanup_routine+17>: jmp rax1
2
30x7ffff7f20550 <getkeyserv_handle+576>: mov rdx,QWORD PTR [rdi+0x8]
0x7ffff7f20554 <getkeyserv_handle+580>: mov QWORD PTR [rsp],rax
0x7ffff7f20558 <getkeyserv_handle+584>: call QWORD PTR [rdx+0x20]先
objdump -d user_file -M intel > gadget
,然后打开文本编辑器ctrl+F
搜索,QWORD PTR [rdi
,看有误合适的gadget
,这种方法需要审查的gadget
数量较多,但是也还好,建议在第一种方法没找到的情况下再使用,可以找到一些复杂的带有条件跳转语句的gadget
,_IO_wfile_sync
中的这个gadget
我就是这么找到的。(文件结构体中含有合适的gadget
的概率较大)
exp
1 | #coding:utf-8 |
解法二
思路
2.24的_IO_str_finish
:
1 | void |
2.29的_IO_str_finish
:
1 | void |
2.23的_IO_str_overflow
:
1 | int |
2.29的_IO_str_overflow
:
1 | int |
所以2.24
的那一套利用已经不可行了。
但是比赛时Kirin
大佬发现了一种新的可利用点,就是在_IO_str_jumps
中存在malloc+memcpy+free
。
所以可以先用chunk overlapping
改一个已经被放进tcahce
里的chunk
的fd
为__free_hook
,然后用largebin_attack
的任意地址写堆地址,改_IO_list_all
到我们伪造的_IO_FILE
上,我们需要伪造两个fake_IO_FILE
,第一个负责把__free_hook
给放入tcache
尾部,第二个负责将其申请出来并将其中的数据改为劫持执行流的gadget
。
这种方法在堆风水时让我加深了对chunk overlapping
的理解,chunk overlapping
是具有连环效应的,我自己这里是构造了三组相互覆盖的chunk
,最好size
是递增的,因为只有两个位置,所以需要将前面的free
里才能有位置去申请新的,要是新的比旧的size
小的话,就可能出现切割的情况,这是我们不想看到的。
感觉_IO_str_overflow+largebin_attack
的方法在程序没有malloc
的情况下可以通用,比改global_max_fast
要简单一点。
exp
1 | #coding:utf-8 |
解法三
思路
结合解法一和解法二的优势,我个人感觉应该是目前为止最简单的方法了,exp
的长度也比较短。
就是类似于2.24
下_IO_str_overflow
的方法,只不过这里的函数换成了_IO_wfile_sync
,虚表指针也从_IO_str_jumps
变为了_IO_wfile_jumps+72
。
exp
1 | #coding:utf-8 |
其他
r3kapig
的exp
最后的劫持用了一串比较复杂的gadget
,虽然不推荐使用,但是作为收藏还是放到这里。
1 | #coding:utf-8 |
Midnight sunCTF pwn4
新的fmt
用法:%*d
,在屏幕上打印格式化字符串对应位置上的数据的%d
形式数量的字符。
%*25$d%16$n
:将格式化字符串的第25个参数的%d
形式的数量的字符打印到屏幕上并写入到第16个参数所指向的内存中。
复习了一下log.progress
的用法。
1 | #coding:utf-8 |
simple_echoserver
思路
学习的balsn
的思路:
用到了上面那题的思路,也就是%*d
,好像连续两次%k$n
没法连环修改。。但是逐个位置对过去可以。。。
需要爆破:
- Fmt string change rbp chain can overwrite stack.
- Use fmt string %*d to print the count of the lower 4 bytes of main_ret address.
- Change main_ret to one_gadget ,then get the shell.
- need a stack lsb bruteforce & (main_ret_address & 0xffffffff) < 0x80000000 (1/32)
Kirin
大佬的思路没来得及复现,感觉比较吊。
exp
1 | #!/usr/bin/env python |
eeemjio
思路
可控两字节shellcode
和当时的寄存器以及栈中脏数据。
观察寄存器,r11
存有mmap_addr+4
,所以push r11
,在最后ret
时劫持执行流即可。
短跳转的范围:
exp
1 | #coding:utf-8 |
eeeeeemjio
思路
仍然是可以控制两字节的shellcode
,但是这时候寄存器只有rip
,rsp
和rdx
是存有有意义的值的。
突破点在于如何将rip
劫持到可控地址上,也就是mmap ~ mmap+0x100
范围的地址上。
直接用jmp
短跳转是不太可能的,因为后面不可控,结合此时具体情境,此时只有rsp
和rdx
可以操作,所以开始想办法操作rsp
,用rdx
劫持rsp
,考虑到只有两字节可控,所以and esp,edx
成为考虑对象,且题目没有限制mmap
的次数,所以可以一直mmap
直到出现符合要求的mmap
地址,即mmap_addr&0xffff=0x8000
且小于0x10ffff
,且使esp&edx
之后结果的后两个字节为0x0000
即可,在后续add rsp,0x8000
的时候rsp
就会被劫持到mmap_addr
的地址,然后再用最后的ret
控制执行流。
and/or/xor/xchg exx,exx
时会清空两个rxx
的高位,对exx
进行赋值时也会清空rxx
的高位,例如rax
原本为0x1111111111111111
然后执行mov eax,0x22222222
之后,rax
会变为0x22222222
。
exp
1 | #coding:utf-8 |