Open Table of contents
我觉得应该珍惜现在的 ROP,Intel 近几年刚提出的 CET (Control-flow Enforcement Technology)
足以杀死绝大多数 ROP Exploit 了……以后 ROP 应该会更难一点,虽然不排除可能会出现新奇的绕过方式就是了 LOL
唉,算是经历了一个时代的变迁了吧?简单了解了一下 CET,靠,我要是早几年生我也可以想出来!!!就很感慨厉害的技术往往都是一些很简单的概念,却达到了非凡的效果,我也幻想自己可以研究点东西出来。
Overwrite a return address to trigger a win function!
怀着感慨和某种说不清的复杂的心情步入本章的第一题……
很明显的栈溢出吧,然后我们希望执行 win
:
好吧,看来第一题比我想象中的要简单得多的多,可以说是没有任何限制条件,直接覆盖返回地址解决。就当是安慰我了 LMAO
Flag: pwn.college{wpfRp_d39M4TTN2Q66mN_kfp0_g.0VM0MDL5cTNxgzW}
Overwrite a return address to trigger a win function!
参见 Level 1.0。
Flag: pwn.college{kIrK17VA4RSajTUwItMyNbaBDJw.0lM0MDL5cTNxgzW}
Use ROP to trigger a two-stage win function!
真是经经又典典的开场啊,反汇编贴出来都是增加碳排放。
逆向发现有两个 win
函数:
lseek
函数用于修改文件指针位置,它的定义如下:
fd
是文件描述符、offset
是 whence
的偏移量、whence
决定了偏移的基准位置。执行成功返回新的文件偏移量,以字节为单位;失败则返回 -1
,并设置 errno
表示错误原因。whence
有三个可选参数:SEEK_SET (0)
、SEEK_CUR (1)
和 SEEK_END (2)
,分别表示文件头、当前位置和文件末。
就以 win_stage_1
为例简单讲一下 lseek
在这里的作用:首先 open
返回了打开的文件描述符,v3 = lseek(fd, 0LL, 2) / 2 + 1;
做的是将 fd
的文件指针定位到文件末,返回了整个文件的大小,除以 2 相当于取这个文件的一半数据,然后加一,返回值保存到 v3
。之后 lseek(fd, 0LL, 0);
又将文件指针指回文件头,v2 = read(fd, buf, v3);
做的是从 fd
读取 v3
字节数据到 buf
,之后 write(1, buf, v2);
将 buf
中 v2
字节数据写入 stdout
,所以整个函数就是做了一个读取文件一半加一字节内容并输出的工作。注意 win_stage_1
是读取前一半并输出,而 win_stage_2
是读取后一半并输出。
所以我们要做的很简单,就是构造 ROP Chain 依次执行这两个函数呗。
Flag: pwn.college{4v7M0arrSE56nVZynPmA1hGzVcg.01M0MDL5cTNxgzW}
Use ROP to trigger a two-stage win function!
参见 Level 2.0。
Flag: pwn.college{8i7RS4fxOytxYOpVidcCJE-hXqd.0FN0MDL5cTNxgzW}
Use ROP to trigger a multi-stage win function!
为了保护环境,某些增加碳排放的东西我就不贴了,直接贴核心。
这次分成了五个阶段输出,只要分别绕过这五个阶段的 if
即可。因为是 amd64 架构的,所以第一个参数通过 rdi
传递,我们直接找有关这个寄存器的 gadgets,发现:0x402b53: pop rdi ; ret
,很好,这样的话我们先返回到这个 gadget,然后在它后面放绕过判断的参数,这个参数就会被 pop
到 rdi
,之后再接函数地址就好了。
感觉这题也可以用 D(ata)OP
,但是我一定不会告诉你我曾闲的蛋疼放着捷径不走试图绕远路,最后发现这条路是真 tm 远所以掉头回来走捷径的……
Flag: pwn.college{EE8eEBP70ZX0reoW2Pp8YObscck.0VN0MDL5cTNxgzW}
Use ROP to trigger a multi-stage win function!
参见 Level 3.0。
Flag: pwn.college{o7dF6gbwKmwO-Xrdr1WGG5iKIQ-.0lN0MDL5cTNxgzW}
Leverage a stack leak while crafting a ROP chain to obtain the flag!
这题就是自由 ROP 自由日了,感觉还是用 chmod
最简单,问题在于如何传递第一个参数 const char *filename
。
嗯……自己构造 /flag
或者别的字符串未免也太麻烦了点,我们直接让 IDA 老婆看看程序本身有没有什么现成的好东西是我们可以直接利用的:
像这个 ret
看上去就很清秀了,我很喜欢~
说实话我感觉自己可能跑偏了,据说这题可以自己构造字符串,但是我太笨了没想出来怎么 leak stack……但是,你就说我这个方法是不是更简单吧 LMAO
刚打完 Level 7 的我敏锐的注意到了什么……既然 Level 7 是直接输出地址,那么……靠,Level 4 果然是把栈地址直接告诉我们了,但因为我没关注程序输出,so……好吧我本以为有什么神奇的构造字符串方法,那没事了,又结了一桩心头大患……
我日,为什么今天都 1.20 了,寒假过的真快,结束前能不能打完 FmtStr 都不好说了……
Flag: pwn.college{EXzWCvmQZIa9w6wrK_nx0PK1w3_.01N0MDL5cTNxgzW}
Leverage a stack leak while crafting a ROP chain to obtain the flag!
参见 Level 4.0。
Flag: pwn.college{g2fR-zCK75_60foo4wveIENcvF0.0FO0MDL5cTNxgzW}
Craft a ROP chain to obtain the flag, now with no stack leak!
这题才应该用我在 Level 4 用的方法吧,有时间我得好好研究下 Level 4 到底怎么泄漏地址了……
Flag: pwn.college{sAqwz2eKPKj_t1zS9diA-_sbUf0.0VO0MDL5cTNxgzW}
Craft a ROP chain to obtain the flag, now with no stack leak!
参见 Level 5.0。
Flag: pwn.college{Uer5U7c794jENBtWtFCgDEyLRsm.0FM1MDL5cTNxgzW}
Craft a ROP chain to obtain the flag, now with no syscall gadget!
没有 syscall
gadget 了,但是瞧瞧我发现了什么?
一次传参直接调用 force_import
肯定是不太方便的,但是既然内部有 open
和 sendfile
,那为何不分别调用它们呢?easy peasy!
Flag: pwn.college{0u8eS8EM1OTTXbPHdwgRj2FQ4m0.0VM1MDL5cTNxgzW}
Craft a ROP chain to obtain the flag, now with no syscall gadget!
参见 Level 6.0。
Flag: pwn.college{A-DtWrNucuvlqiQOl-yB1ARFcxt.0lM1MDL5cTNxgzW}
Utilize a libc leak to ROP with libc!
利用已经泄漏的 system
的地址减去 system
在 libc
中的偏移得到 libc
的基地址,然后通过 libc
基地址加上 chmod
在 libc
中的偏移就可以得到 chmod
的实际地址了。简简单单,都不需要想办法怎么泄漏地址,程序直接通过 dlsym((void *)0xFFFFFFFFFFFFFFFFLL, "system");
把地址告诉我们了……
另外,为了防止有人不知道怎么获取当前程序使用的 libc
,这里简单贴一下:
嗯……如果你不知道我讲的是什么,建议去补习一下 PLT 延迟绑定。推荐看《程序员的自我修养——链接、装载与库》,CSAPP 也可以看看,反正底层知识有空一定要多学学,非常重要。
哎不对,难道说 Level 4……
Flag: pwn.college{USImSejN9YmHN5CcyKDwtVScMO7.01M1MDL5cTNxgzW}
Utilize a libc leak to ROP with libc!
参见 Level 7.0。
Flag: pwn.college{g7Ura4DbaSnnNgm8JQPQ2XM6c_l.0FN1MDL5cTNxgzW}
ROP with libc, no free leak this time!
这次没有 Level 7 那么愚蠢的泄漏了,需要我们自己想办法获取 libc
基地址。
不过思路很简单,我们先获取 elf.got["puts"]
在全局偏移表中的地址,然后通过 puts
函数泄漏这个地址指向的 puts
在 libc
中的实际地址。之后用它减去 libc.symbols["puts"]
就得到了 libc
基地址。程序也随之结束了,不过我们可以再次返回到 _start
重启整个程序,利用我们得到的基地址计算出 chmod
的实际地址,然后调用它。
Flag: pwn.college{oFCQDFxRqfNOwKX3jCEn79BN5cC.0VN1MDL5cTNxgzW}
ROP with libc, no free leak this time!
参见 Level 8.0。
Flag: pwn.college{Q0NC7L0dteUeHqynz_c9HjT-w2R.0lN1MDL5cTNxgzW}
Perform a stack pivot to gain control flow!
这题得用栈迁移 (Stack pivot),不错,新技巧++。
直接看调试吧:
在执行 memcpy
之前有一个 read
,可以读取 0x1000
bytes 输入到数据段 0x4150e0 (data+65536)
。
虽然大小足够大,但因为是读到数据段,所以我们不能通过 read
覆盖返回地址,那么很显然,问题就出这个 memcpy
上了。我们注意到它把数据从 0x4150e0 (data+65536)
复制到 0x7fffc71bc828
,注意到这个地址正是我们的返回地址,但是它又限制了我们只能复制 0x18
bytes,也就是三条指令到 0x7fffc71bc828
,故我们要是想直接把完整的 payload 通过一次 memcpy
执行完是不可能的。如此限制,有何破解之法?当然是栈迁移!通过栈迁移我们可以得到一个更大的空间来发挥,而不用受限于这小小的 0x18
bytes 的空间。
简单来讲一下栈迁移原理。我们知道 leave
指令实际上做的是:
那我们只要控制 rbp
指向我们想到的新的栈的地址,就实现了栈迁移,之后所有 gadgets 的操作都基于新的栈。至于后面那条 pop rbp
倒是无所谓,我们重点关注的是 rsp
的地址,因为一系列 gadgets 都是通过 ret
连接的,ret
做的就是 pop rip
,这都有关于 rsp
。
当然,可以实现栈迁移的方法不止 leave; ret
一种,能改变 rsp
的都可以想办法用来做栈迁移,这个自己悟去吧。
那么现在的问题是,迁移到哪里?首先肯定得是 rw
区吧,不然怎么实现 ROP。一个比较常见的方法应该是迁移到 .bss
段。pwntools 还是很方便的,通过 elf.bss()
即可得到当前程序的 .bss
段地址。
通过上面的调试我们知道,执行完栈迁移后应该返回到 elf.bss() + 0x10038 + 0x18
处继续执行,+ 0x18
是为了跳过执行栈迁移的三条指令。
那么接下来就好办了,思路可以参考 Level 8。既然我们已经有了更大的栈空间存放 gadgets,那接下来就只要泄漏 libc
基地址,调用 chmod
就好了。
Flag: pwn.college{8pCSQF9tTLebDaqobUsXUt7T1Yp.01N1MDL5cTNxgzW}
Perform a stack pivot to gain control flow!
参见 Level 9.0。
Flag: pwn.college{8cUj1kJEP7UIyfDvbSd34M8nJI-.0FO1MDL5cTNxgzW}
Perform a partial overwrite to call the win function.
标绿的部分把 sub_2760
的地址映射到了 dest[0]
,并设置为 r-x
,read
可以覆盖返回地址,但这个程序开启了 PIE,我们需要通过部分写绕过它,执行 dest[0]
处保存的 sub_2760
。
比如我们知道一个 gadget 是 0x0000000000001313 : pop rbp ; ret
,因为有 PIE 所以我们只能得到它的偏移 0x313
,调试验证一下在已知页地址的情况下利用这个偏移得到的是否是 pop rbp ; ret
这个 gadget:
嗯,看来猜想没问题,那么盲猜页地址加覆盖低三 nibbles 就可以调用我们的 gadgets 了。
知道了怎么调用 gadgets,还需要思考整体攻击思路。我们有一个已泄漏的栈地址,把这个地址减八就是保存 sub_2760
函数的地址的地址了,我们可以通过栈迁移把 rsp
设置为泄漏的地址减十六,这样 ret
就执行 sub_2760
了。
我本想这样做,勿喷……:
后来发现低 3 nibbles 覆盖了后 pop_rbp_ret
什么的会接着覆盖别的,所以这么做行不通。后来我意识到 rbp
在 rip
之前,提前设置好它不就完了?
需要注意的是 leave ; ret
到底要 pop
什么以及 ret
到哪里:
Flag: pwn.college{g4EQhfj4W4pI_g9tDdWlqPAdtK2.0VO1MDL5cTNxgzW}
Perform a partial overwrite to call the win function.
参见 Level 10.0。
Flag: pwn.college{kq62r2gqRuS8CCZ5IsBJ4-OK_E-.0FM2MDL5cTNxgzW}
Perform a partial overwrite to call the win function.
一眼题目结构和上题类似?好像没有区别吧……
看看,好的思路和 exp 是可以拿来反复秒题的 LMAO
唯有一点我不太懂,你这是何必呢?
Flag: pwn.college{ogFd_c22L6ok_8m23oykWyxLfn9.0VM2MDL5cTNxgzW}
Perform a partial overwrite to call the win function.
参见 Level 10。
Flag: pwn.college{oSzRHWf3oNfOmzKglfN7pBOGmyY.0lM2MDL5cTNxgzW}
Creatively apply stack pivoting to call the win function.
根据 Description,我们可以推测出 CuB3y0nd 是一位非常有 Creativity 的 Hacker,直接复用 Level 10 的 exp 秒了 Level 11 和 Level 12。每道题修改 exp 不超过 4 bytes
写完一看 WTF! 这次 exp 跑了那么久没跑通……BRO MAKES ME SO MAD……你一定没看见上面那句话吧,你肯定看不见……
让我看看到底是怎么个事:
难怪打不通,原来是因为这个 Level 没有 challenge
函数了,这个 Level 只有一个 main
函数,所以会返回到 libc
中。而之前有 challenge
函数的时候会返回到 main
,所以我们才可以用 binary 的 gadgets
,但这次返回到 libc
了自然就不能用 binary 的 gadgets 了,必须找 libc
中可用的 gadgets。
嗯……理论上我们爆破 5 nibbles 必定可以成功,但实际上我们很幸运,在默认返回地址附近存在可用的 gadgets 来实现栈迁移,所以最终我们只需爆破 3 nibbles 就好了。
Flag: pwn.college{Yq8SiIRAxHtJeQWDahvT9Q5y0pE.01M2MDL5cTNxgzW}
Creatively apply stack pivoting to call the win function.
参见 Level 12.0。
Flag: pwn.college{4358dHEnXGJJC945avk1i2By5c6.0FN2MDL5cTNxgzW}
Perform ROP when the function has a canary!
这题不难吧,标绿部分模拟了一个泄漏,我们可以通过它把 canary 泄漏出来。之后我们想办法返回到 __libc_start_main
再次调用 main
函数,泄漏一个 libc
地址出来,计算 libc
基址,有了基址就可以去构造 ROP Chain 了。
Flag: pwn.college{Mw9zt3eeAB8_8it-_3_NRUzoHRA.0VN2MDL5cTNxgzW}
Perform ROP when the function has a canary!
参见 Level 13.0。
Flag: pwn.college{M-9WLFPzOoEolY4qgzOfZ2Xk3JW.0lN2MDL5cTNxgzW}
Perform ROP against a network forkserver!
forkserver
,典。
爆破 Canary、ret2main, leak libc 再 ROP 应该就好了。2.9 有个 Nu1L Junior 招新赛,我得去抽个热闹,万一选上了岂不是很爽,但是现在才学到 ROP,好在还有一点时间,得提前学点堆了,就怕到时候遇到堆题啥也不会就死了……
反正这章只剩下两道题,我就先鸽着了,看了下简介感觉都不难,感觉无非就是把所有知识综合起来罢了。
吗的 Heap 镇南,我被蹂躏的好惨,还是先把这两题打了稳固下 rank 吧。
重新总结下思路:因为是 forkserver,子进程会沿用父进程的内存布局,所以我们可以把 canary 和 retaddr 爆破出来。有了 retaddr 后我们就可以使用一些程序本身的 gadgets 了,但是这些 gadgets 中不包含 syscall 什么的,所以不能做一些很 powerful 的事情,但是 libc 里面一定有好东西,那么问题就在于怎么获得 libc 基地址了。因为这个程序使用了 puts
函数,所以我们可以构造两个 stage payload,第一个 stage 用 puts@plt
泄漏 __libc_start_main
的地址,第二个 stage 根据泄漏出来的地址计算 libc 基地址,然后构造 ROP Chain Do anything what you want to do!
Flag: pwn.college{wJF3GTTKHHSqaWXrhLMIUW4ocoT.01N2MDL5cTNxgzW}
Perform ROP against a network forkserver!
参见 Level 14.0。
Flag: pwn.college{IcjKEpDUZ1r43p0FESk4RB96-1s.0FO2MDL5cTNxgzW}
Perform ROP when the stack frame returns to libc!
与上一题的区别在于上一题会返回到 binary 的 main
中,而这题直接把所有逻辑都写在 main
里面了,那就会返回到 libc
。
因为上题会返回到 main
,里面有一些输出信息,所以我们很容易判断返回地址的下一字节是否爆破成功,但这题返回到 libc
,默认是要执行 exit()
的,不会产生任何输出信息,直接爆破肯定是得不到完整返回地址的。难点就在这里,怎么确定我的爆破是否成功了,以便继续爆破下一字节?
经实测发现,pwntools
在检测到 fork
出新的子进程后 debug log 会输出这样一段信息:
LMAO,看到这段美丽的 debug log 我就知道这题已经到手了!
所以我们只要根据上面这个 transferring control 信息就能判断程序是否成功返回到 fork()
了。然后实际编写 exp 的时候我还发现,在成功返回到 fork()
生成了新的 main
进程后后续的连接都会变得十分缓慢/碰运气,这是因为有两个进程同时监听来自指定端口的连接,产生了条件竞争,我们杀掉一个就好了。幸运的,pwntools 的 debug log 除了提示我们 fork 出了一个新的进程外,还把 fork 出来的子进程的 PID 也告诉我们了,所以我们直接把这个子进程 kill 掉就好了。这无疑为我们编写自动化脚本提供了极大的便利,否则就要手动 kill 了,我看 discord 社区还真有人这么干,显然是没发现 pwntools 的强大……
有关爆破返回地址这里其实还有一个坑,如果你看 got
表:
再看 __libc_start_main
的汇编:
真 TM 巧啊…… fork()
在 libc 中的固定字节正巧是 \xf0
(爆破返回地址这种事情我不习惯固定倒数第三个 nibble,不然我早就可以意识到后续的问题了)、__libc_start_main
中 \xf0
处的指令 jne
正巧不会跳转、下面继续执行的正巧是 fork()
。这一连串的巧合让我在分析的时候百思不得其解啊,为毛计算了 fork()
到 libc 之间的偏移就是 TMD 执行不了我的 ROP Chain……因为我在打 Level 14 的时候也碰到过 exp 没问题但就是打不通的问题,多打几次就好了……我!草!又 TM 是一个巧合……最后导致我在这上面耗了大概……好几个小时?一下午?无所谓了……
这说明了我对程序的细粒度还不够敏感,不过有了这次经验后以后就长记性了……毕竟我们的默认返回地址是返回到 __libc_start_main
中的,而我们又是从低位开始爆破,怎么着也不可能走到 libc 中的 fork()
吧……大概率会掉到 __libc_start_main
中的一个 call 里才是。确实,说不可能有点太绝对了,不够严谨。其实在满足了『天时、地利、人和』的情况下还是很有可能的,只是可能性有点小。
最后,我觉得一切的罪魁祸首是我 ret2fork 的灵感来源于 got
表,真不知道该谢谢你还是……如果说有人想浪费我的时间很困难……6,被你做到了,而且非常完美……
Flag: pwn.college{wOJjH27pmDvy68va0GzxQ3gHz6_.0VO2MDL5cTNxgzW}
Perform ROP when the stack frame returns to libc!
参见 Level 15.0。
Flag: pwn.college{Y_8emGu4QF43dsSwmrNIX6MC17Q.0FM3MDL5cTNxgzW}
让我算算这章又打了多久……唔,七天(减去了我中途学堆的三天)!这是不是我打的最快的一章?没印象,好像 shellcode 更快,只打了三天吧?懒得查记录了……