2022DASCTF-Apr-X-FATE-pwn-wp
2022DASCTF-Apr-X-FATE-pwn-wp
时间太仓促了,题目逆向的工作量有点大,远程还有不少毛病……一言难尽。下来把剩下几道题都复现一遍,已写完收工。wp
持续更新中
小广告:解题脚本均使用我自己开发的工具pwncli编写,欢迎感兴趣的
pwner
师傅们试用~
1 good_luck
眼疾手快拿了个一血,这题其实很简单,但是远程的问题很大。附件都更新了两次,就很迷~
checksec
没有给libc
。
漏洞点
要么栈溢出+格式化字符串,要么栈溢出:
利用思路
由于这两种的概率是1/2
,所以可以根据不同的输出来使用不同的payload
。当然,可以编写一个通用的payload
同时适用这两种情况。
观察到fmt
函数的缓冲区距离rbp
是0x70
,overflow
函数的缓冲区距离rbp
是0x50
,所以前面的通用的payload
可以为:
|
|
因此,思路为:
- 使用通用
payload
再次执行fmt
- 第一次
fmt
,利用格式化字符串泄露出libc
地址,并再次执行fmt
- 根据泄露出来的地址,计算并填入
one_gadget
即可获得shell
EXP
|
|
最后测出来远程:libc6_2.23-0ubuntu11.2_amd64
。
远程:
2 ssstring
不得不说,这次比赛的远程真的很很很很迷,本地环境应该和远程是一样的,但是总是打一半就卡死崩掉了。
这题考查的是C++
的string
对象,也不算很难的题。C++ string
对象的布局伪代码:
|
|
初始状态下,data
指针指向pad
处。如果输入的字符串长度大于0x10
,string
对象的操作流程可以简单总结为:
- 首先检查
data
是不是指向pad
,如果是,就会调用malloc
分配堆内存,存储输入的字符串 - 如果
data
不指向pad
:- 如果输入的字符串长度大于
capacity
,释放data
处的内存 - 按照
0x40->0x80->0xf0->0x1e0->0x3e0...
的大小依次进行扩容,直到满足要求(所以一次性读取超过0x400
长度的字符串,会在tcachebins
里面发现很多free chunk
)
- 如果输入的字符串长度大于
checksec
远程libc
版本为:2.31-0ubuntu9.2_amd64
漏洞点
程序不复杂,漏洞也很明显,在change idx
的时候:
输入的idx
可以为负数,也就可以溢出修改capacity
域以及data
指针,虽然每次只能修改1
个字节。
利用思路
根据漏洞点整理利用思路如下:
-
第一次输入不超过
0x10
长度的字符串 -
利用索引负数溢出修改掉
capacity
的值后,cout<<str
即可泄露出栈上的libc
地址以及栈地址 -
继续利用溢出修改
capacity
大于0x7f000000
-
然后将
data
指向的地址的第5
个字节修改成libc
地址的第5
个字节。比如说此时data
的地址是一个栈地址0x7ffdd10d5e90
,泄露出来的libc
地址0x7f7463afc000
,这里将0x7ffdd10d5e90
修改为0x7f74d10d5e90
,是为了方便修改libc
上的数据 -
计算想要修改的
libc
上的数据,和修改后的data
之间的距离,一个字节一个字节修改即可 -
这里我选择的思路是修改
IO_file_jumps
结构体和stdout
结构体以及__free_hook
,篡改puts
的调用链,使得_IO_file_xsputn
调用_IO_str_finish
,调用free(stdout->_IO_buf_base)
,实际调用system("\nsh;")
即可获得shell
EXP
|
|
布局成功后如下所示:
此时修改stdout->vtable
的低一个字节即可:
远程打不动,弃疗了…
3 easysystem
这题需要耐心和时间去逆向以及利用。需要IDA
文件的点这里,我已经逆完了,我使用的IDA
版本为7.6
。需要调试镜像的使用docker pull roderickchan/debug_pwn_env:21.10
拉取即可,已经安装好了pwndbg/gef/pwncli
,使用gdb-gef
和gdb-pwndbg
命令切换插件。
checksec
给的libc
的版本很高,版本为glibc-2.34
。移除了很多hook
,基本上无法使用hook
去控制程序执行流。
噢对,还加了沙箱:
漏洞点
程序的逆向工作有点大,维护了好几个结构体,如下所示:
|
|
漏洞点在openfile
函数,全局变量未清理的漏洞:
由于在read_file/write_file
等函数均会使用到open_file
这个全局变量,当其不为null
的时候,会继续read/write
。而如果在此之前调用close_file
函数,则该变量指向的保存文件数据的内存是已经释放掉的内存:
基于该use after free
的漏洞,可以劫持程序执行任意流程。
利用思路
基于漏洞整理的利用思路如下:
step 1:任意地址写
- 首先
close_file
然后write_file
,即可泄露出libc
地址和heap
地址 - 然后再来一次,
close_file
之后create_file
,然后read_file
即可伪造上面整理的FILE
结构体,修改其next_file
字段和length
字段,使其指向tcache_perthread_struct
delete_file
释放伪造的file
即可释放掉tcache_perthread_struct
- 利用
tcache
让任意地址分配内存
step 2:劫持程序执行流
-
分配到
stdout->vtable
上方,tcache
里有检查,分配的地址需要0x10
对齐 -
修改
stdout->vtable
为_IO_cookie_jumps+0x40
-
布局好
__cookie
字段和_IO_cookie_io_functions_t
结构体 -
输入
exit\n
,然后输入一个不存在的用户名,即可调用puts
,本来是调用_IO_file_jumps->_IO_file_xsputn
,劫持后调用(struct _IO_cookie_file *) stdout->__io_functions.write((struct _IO_cookie_file *) stdout->__cookie)
,这样就控制了rip
和rdi
-
用两段
gadget
劫持rsp
1 2
0x0000000000165fa0: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]; 0x0000000000059fa0: mov rsp, rdx; ret;
-
然后
rop
修改heap
的执行权限,执行提前布局好的shellcode
-
利用
retfq
切换到32
位执行orw
读取flag
EXP
|
|
调试截图:
执行到puts
:
准备劫持rsp
:
栈迁移到堆上,修改其权限:
切到32
位:
读取到flag
:
远程打:
4 try2findme
逆向题,侧信道爆破即可。
checksec
还开启了沙箱,不过不影响做题,libc
的版本也不影响做题。
题目分析
应该叫题目分析更为合适~
首先用IDA
恢复跳表:
修复后好看多了:
然后恢复结构体信息:
|
|
初始化的地方需要注意一下:
而{
是0x7b
,}
是0x7d
。
总结各个分支的流程如下:
|
|
漏洞在于i
的值可以由输入控制,而i
又可以控制分支执行。因此,类似于汇编中的各类跳转分支,当控制了i
之后,我们可以在各个分支之间跳转。
利用思路
每个分支分析清楚之后,不难想到,可以用侧信道爆破。思路如下:
- 由于初始化中的字节都是小于
0x7a
的,所以可以用}
字符去判断是否找到了存放flag
的位置。初始化:将p
减小0x60
,跳过管理的chunk
和0x30
个字节 - 然后
case 1
,将}
输入到s[p]
- 使用
case 0xd
判断前一个字符是否等于}
,然后借助case 8
和case 9
分别进行跳转 - 如果不等于,
p -= 2
,然后跳转到第二步重复;如果相等,说明找到flag
的尾部了,跳转到下一步,猜测当前字符 case 0xd
猜测当前字符,借助case 8
和case 9
分别进行跳转- 猜测成功的时候跳转到
case 0x12
,一直睡眠;猜测失败的时候跳转到case other
,会输出See u next time~
- 使用当前方法爆破出所有字符即可
EXP
|
|
远程的环境没有了,在本地试了一下,很快就爆破出flag
:
5 storage
1.2.2 musl libc
的堆管理方式也没有特别的复杂,虽然与1.1.24
版本的管理方式完全不同。理解了meta/group/meta_area/malloc_context
几个结构体后,即可很快厘清堆管理方式。
因此题目提供的了完备的增删改查功能,所以其实做本题甚至不需要完全搞懂musl libc
的分配方式,只需要找到一个特殊的地址进行操作即可。关于musl libc
的分析文章,可参考这里,写得很详细,建议边读边结合源码分析。
自己编译源码:
|
|
然后在./musl/lib
中即可找到带调试信息的libc.so
checksec
musl
版本为1.2.2
。
漏洞点
在store
函数:
其实一开始我注意到这个点,但是根据之前的经验,就是这里的描述:
我以为read
会直接返回,然后ptr + 0xffffffff
这里将是一个无效的地址,赋值的时候段错误。后来找了半天没找到漏洞,然后试了一下这里,发现竟然没有报错,可以继续输入,真的很神奇……所以,经验主义确实害人啊~
既然这里可以溢出,那么利用就很简单了。
利用思路
需要注意的是,musl
分配的堆,除了属于动态内存区域,还可能属于静态内存区域。是因为musl
在初始化的时候,会在libc.so
映射的地址空间寻找未使用片段,然后当作静态内存管理起来。所以,可能分配到的堆地址是一个libc
地址。
首先观察到,程序一开始申请的0x80
大小的chunk
就在libc
上:
在malloc(0)
的大小的内存也在libc.so
的地址空间,而且恰好在这个0x80
的chunk
的上方:
距离为0x3f0
。也就是说,可以溢出修改ptrs
中存储的指针。然后由于有个\x00
的截断,所以需要寻找一个地址x
,恰好在x & ~0xff
的地址处存储着libc
地址或者堆地址,或者栈地址,满足一个即可。后来测试出来,malloc(0x400)
的时候,满足需求:
这里有个堆地址,然后堆地址上一定会有libc
地址,因为会有group
结构在libc
上。
经过多次测试,这里的libc
地址相对于基地址是固定的。
总结以上利用思路为:
-
store(0x400)
准备好要利用的指针 -
store(0xffffffff)
溢出修改ptrs[0]
存储的指针的最低字节为\x00
-
show(0)
即可泄露堆地址 -
继续溢出修改指针为堆地址,然后泄露
libc
地址 -
计算得到
__stderr_used
地址,劫持其write
函数指针,触发一个exit
即可控制程序执行流
EXP
|
|
打远程:
引用与参考
1、My Blog
2、Ctf Wiki
3、pwncli