【题解】hxpctf2018-pwn
HXPCTF2018 - PWN/Rev
这次CTF做了2个PWN,一个简单REV。算是找回了一些信心。
这次比赛中第一次遇到arm架构的题目,虽然只是栈溢出,但是搭建环境和调试花费了一些时间,并且使用qemu进行调试的步骤也需要记录一下。
解出题目的题解如下:
angrme
这是个reverse,因为之前用过angr,就顺便写了。
angr的套路不必多说,只要程序打印 :) 就说明输入对了。直接上脚本。
中间出了点小插曲,因为程序中使用了一些特殊的指令movaps [rsp+1C0h+var_180], xmm0
,直接上angr崩了。google了之后,好像是因为要求RSP 0x10对齐。所以patch了一下程序,在main函数开始的地方 sub rsp, xxx
修改成了0x190。
1 | import angr |
poor_canary
漏洞分析:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
很明显,看到read函数造成栈溢出。利用read函数不添加\x00的特性,泄露出canary,再利用ROPGadget 跳转到system函数即可。
很神奇,原来ROPGadget对arm架构的ELF也可以使用。
1 | lzh@ubuntu:~/workspace/pwn/hxpctf/poor_canary$ ROPgadget --binary canary --only "pop|ret" |
利用脚本:
1 | # -*- coding:utf-8 -*- |
搭建qemu环境的过程单独mark。(使用qemu调试很烦)
yunospace
很有意思的一个题,写出来也很有成就感
漏洞分析:
有一个外层脚本:
1 | #!/usr/bin/python3 -u |
我们可以通过交互,来执行程序 ./yunospace,并将1Byte的flag作为命令行参数传入程序。
我们再来看程序是如何使用这1Byte的flag的。
1 | void __fastcall main(int a1, char **a2, char **a3) |
简单来说,程序用mmap分配了两个随机的地址空间,一个用来放写入的机器码,一个用来作为栈空间(为了防止栈中的信息泄漏)
然后程序允许我们写入9Byte的机器码,并执行这9Byte。但是注意,1Byte的flag被写入到9Byte的机器码之后了 *((_BYTE *)rand + 11) = *arg1;
所以我们如果能写入机器码把这1Byte用某种方式确定出来,就可以获得flag。
然而出题人是个狠人。
1 | .text:00000000000009CA xor rax, rax |
在跳转到用户输入的code执行之前,清空了所有寄存器,并把esp也指向了分配的随机空间。也就是说我们拿不到任何地址信息,也无法调用函数。
如上图,我写入了9Byte nop指令,可以看到,在跳转到我们写入的code之前,所有寄存器都已经清零,且跳转之后RAX也清零。RSP指向的是mmap分配的随机区域。
我们可以看到,写入的9个 0x90之后,有一个字节0x66。这一个字节就是flag。我们要想办法确定该字节的值。
我的思路是:先拿到RIP的地址,然后通过类似 mov al, BYTE PTR[RIP+xx]
的方式将flag读入寄存器。
第一个问题是:1Byte的flag读入寄存器之后,如何向外反馈?想起来很久以前听杨栩翔说他听鸡哥说的利用 cmp al, bl
,如果相等就用jmp xxx
进入死循环,如果不相等就让程序直接崩掉。通过检测程序是否存活来判断该1Byte的flag是什么。总之就是要爆破。
第二个问题是:所有涉及到 RIP的指令,指令长度都暴增… 类似刚才的指令就需要5Byte,后续的工作没法进行了。这个卡了我很久。后来我尝试有没有什么能用的系统调用。后来发现,如果调用了系统调用,RCX会保存下一条要执行的指令(即RIP的值)。而且syscall
指令只需要2Byte。
据此,写入的code如下:
1 | syscall |
0xaa表示的是flag的值。我遍历尝试所有的可见字符,如果在某个字符处程序进入了死循环而没有崩溃,就说明本次尝试的字符是对的。
调试脚本花费了很多时间,因为太慢又加了线程。
利用脚本:
注:explode(num, idx)
表示要尝试的字符ascii是num,测试的是flag的第idx个字节
expb(idx)
表示爆破flag的第idx个字节。
1 | from pwn import * |
最后爆破出来的结果如下:
1 | ''' |
大概猜了一下,*的地方应该是y,试了一下就过了。