记录下这题koh
基本分析
下载程序后解压下:
1 | ruan@ubuntu ~/release ls |
可以看到一堆的rop+架构开头的程序,所以可以猜到大概就是一个rop打通多个版本,拿到题目的第一眼看下tester.py
,这是给你本地测试用的
tester.py:
1 | #!/usr/bin/python |
连上端口后看下远程是怎么样子的:
1 | [+] Opening connection to chall.0ops.sjtu.edu.cn on port 2005: Done |
写个脚本:
1 | import hashlib,string |
那大概流程就是本地打通了,再去打远程的,然后尽可能的让自己的rop能通过更多架构的test,拿到更多的分数,那现在就看下程序大概是怎么样子的
程序分析
首先肯定拿比较熟悉的开刀,就x86_64的吧:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
看下rop_trampoline
是个啥:
1 | .text:0000000000401D23 rop_trampoline: ; DATA XREF: main+DC↓o |
可以看到就是调用了munmap这些系统调用,把程序段还有libc,stack啥的都unmap
掉了,我们可以下个断点在这里看下:
0x13370000是生成的随机数,0x73310000是我们的ropchain,继续单步直到retn:
xxd ropchain:
1 | ruan@ubuntu /mnt/hgfs/shared/tctf2020final/koh/release xxd ropchain |
可以看到的是程序原先的段都被unmap掉了,这样的话,我们能用的gadget只能来自随机数了,所以直接在随机数中找gadget了,从tester.py
脚本可以看到,生成的随机数有512M,你想要啥gadget,里面一般都能找得到
调试
程序的第一个参数是一个fd,但是我们直接用pwntools的process起程序的话,不太好搞,因为我们没有那个memfd,会导致get_file_size
函数失败退出,所以当时对程序进行了patch,把atoi改成了open
- 原版x64:
1 | .text:0000000000401DEE push rbp |
1 | __asm { endbr64 } |
patch:
1 | .text:0000000000401DEA main proc near ; DATA XREF: _start+21↑o |
1 | __asm { endbr64 } |
这样就可以用process起,gdb.attach贴上去调试了
- 原版i386
1 | .text:08049DBE add ebx, 9B242h |
patch
1 | .text:08049DBE add ebx, 9B242h |
- arm原版:
1 | .text:000104B2 PUSH {R7,LR} |
patch:
1 | .text:000104B4 SUB SP, SP, #0x28 |
解题
基本都分析好了之后,要开始拿分数了,koh这种赛制,个人觉得还是先解简单的,分数先拿了再说,你没解肯定是没分,所以就这题来说,当时先解了一个x64的,因为比较熟悉,koh按轮算的,先拿几轮的分数再说
- 找gadget,当时用了一个比较粗糙的方法:
1 | ruan@ubuntu /mnt/hgfs/shared/tctf2020final/koh/release python |
其中rand_num文件生成方法是:
1 | ./rnd_generator 5 536870912 > rand_num |
第一个参数5是seed,第二个参数是大小,就是512M,把输出重定向下就好
先解了x64的,拿了1分,当时的算分规则是一个架构1分,相同分数就比谁的ropchain短
本着先解决熟悉的架构,所以下一个开刀的就是i386了
需要注意的是,ropchain要同时能打通i386和x64的,这里很明显的一个就是他们的栈大小不同,一个是4字节,一个是8字节,我们可以依靠这个来把他们的栈区分开,这也是一个常用的方法
当时找了一个pop rax;pop rsp;ret
的指令,这条指令在i386下是pop eax;pop esp;ret
,这样就可以用两个架构栈的差别来把esp/rsp
切到不同的地方,以便后续的rop
当时的一份pass [x86_64,i386]
的(种子0):
1 | from pwn import * |
栈的布局(x64):
i386:
现在解决了两个架构,拿了2分,但是都用rop会导致payload太长了,所以当时改成了先mprotect(0x73310000,0x1000,7)
下,直接执行shellcode,短了不少,其实还可以再去找找gadget,比如pop rdi;pop rax;pop rdx;ret
这样连着的,也能使得payload的长度更短
这里贴一下当时shellcode的:
1 | from pwn import * |
还能在优化就是了,但是当时时间有限,突破更多的架构才是上上策,可惜到了最后也没有解出第3个架构
arm,i386,x64
赛后问的AAA的师傅,用的种子5,找到的一个gadget:
1 | offset:191723632 |
用的脚本(./search_gadget.py):
1 | from pwn import * |
这里可以看到我们arm用的arch不是arm
,而是thumb
,这里个人推测是程序有的指令长度是2字节,而thumb
就是2字节的,还有一个就是i386和x64的code偏移了一个字节,输出显示的时候thumb
端序和他们不一样
还有一个细节就是,可以打开arm程序的反汇编界面的此处:
1 | .text:000105F0 MOVS R2, #5 |
你可以看到的是程序并不是直接跳转到0x10000000处,而是0x10000001处,偏移了一个字节,个人推测是arm这个是按大端序取的指令,这样pc指向0x10000001,执行的就是0x10000000和0x10000001处的指令,后面写payload的时候也有这个坑
最后的payload:
1 | from pwn import * |
单独拿出arm的讲下:
1 | # arm |
因为但是arm程序是用qemu-arm-static
起的,段都是可执行的,所以我们可以直接跳到栈上执行shellcode,shellcode干两件事,一个是cat("/flag")
,一个就是exit(0)
,这里对arm不是很熟悉,所以直接用pwntools的shellcraft
模块来生成payload,但是生成cat("/flag")
的shellcode最后3条指令有点问题,所以直接截断补了自己补了两句上去asm(shellcraft.cat("/flag"))[:-10]+asm("mov r7,#0xbb;svc 0x41")
还有一个坑就是,这里直接跳栈上也要偏移一字节,所以是0x73310085,而不是0x73310084,前面还用了两个nop(“\x00”*4)指令来缓冲下
调试arm程序
这里还是和调试其他架构程序一样,一个终端用qemu
起,一个终端用gdb-mutilarch
连接端口调试
1 | qemu-arm-static -g 1234 ./rop_arm ./rand_num ./ropchain |
arm_debug.gdb的内容:
1 | b *0x00010604 |
是个gdb的脚本
得用gef调试,然后就是调试的时候不知道为啥在这里莫名其妙的奔溃:
有时候可以,有时候不行,所以干脆直接跳过了,就是脚本那句set $pc=$pc+26
调试的时候也可以观察下pc的值和gef显示的code的地址其实是相差了1字节的
本地test
x86的和i386的没啥问题直接用:
1 | sudo python tester.py i386 5 ./ropchain |
用sudo是因为要用chroot
arm的稍微修改下tester.py
:
1 | def test_rop(stub_name, ropchain, rop_memfd): |
其实就改了两行:
测试效果:
总结
今年总算是没有爆0了,也算是小小的进步把