- 缓冲区溢出
- 通过执行注入的代码,重写返回地址,执行另一个代码片段。
- ROP攻击
- 随机化、将保存栈的内存区域设置为不可执行等技术使得缓冲区溢出攻击失效。这时可以通过现有程序中的代码而不是注入新的代码实现攻击。利用gadgets和string组成注入的代码,具体来说是使用pop和mov指令加上某些常数来执行特定的操作。
实验要求
- 实验分为5个阶段,level1-3是通过缓冲区溢出方式进行攻击,level4-5则是通过ROP攻击方式进行攻击。
文件功能
- cookie.txt:存放你攻击用的标识符
- ctarget:执行code-injection攻击的程序
- rtarget:执行return-oriented-programming攻击的程序
- farm.c:gadget farm产生代码片段
- hex2raw:生成攻击字符串
其中,ctarget和rtarget会从标准输入读取字符串,保存在大小为BUFFER_SIZE的char数组中。
前置任务
- 对ctarget进行反汇编
1 | objdump -d ctarget > ctarget.txt |
- 对rtarget进行反汇编
1 | objdump -d rtarget > rtarget.txt |
- 确定getbuf的缓冲区大小
778行可知getbuf开辟了40(0x28)字节的栈空间,即buffer为40字节
level 1
不需要注入新的代码,只需要让程序重定向调用某个方法。
1 | void touch1() |
touch1函数中没有特别要求,只要运行进入该函数即可调用validate(1)
所以只需输入字符串第40字节后为touch1函数的地址即可以覆盖掉原函数返回地址进入touch1
- 查看汇编代码:
答案为:
1 | 00 00 00 00 00 00 00 00 |
level 2
需要注入一段代码
1 | void touch2(unsigned val) |
touch2中只有val==cookie后才能进入validate(2),说明除了进入该函数外,val必须等于cookie
- 查看汇编代码:
%edi中存放val,0x202ce2(%rip)(即0x6044e4)存放cookie - 调用gdb:
即%rdi需要修改为0x59b997fa
所以需要在buffer中注入代码,而为了运行注入的代码,需要跳转回栈顶地址 - 调用gdb:
得到栈顶地址为0x5561dc78,即输入字符串第40字节后为0x5561dc78即可以覆盖掉原函数返回地址跳回栈顶运行注入的代码
而注入的代码需要修改%rdi的内容并跳到touch2函数:
1 | mov $0x59b997fa %rdi //将%rdi内容修改为cookie |
gcc汇编并objdump反汇编后得到对应的16进制表示:
- 将注入代码和跳转地址结合即为答案:
1 | 48 c7 c7 fa 97 b9 59 68 //修改%rdi并跳到touch2 |
level 3
需要传入字符串
1 | /* Compare string to hex represention of unsigned value */ |
touch3中只有hexmatch(cookie,sval)==1后才能进入validate(3)
而hexmatch函数的作用为将cookie转成字符串并和sval比较,如果相等则返回1,说明除了需进入touch3函数外,*sval必须等于cookie的字符串形式
- 查看汇编代码:
%rsi(函数第二个参数)为char* sval,%edi为cookie,而877行将%rdi转入%rsi,说明初始状态下%rdi中存放着char* sval,即%rdi需要修改
所以需要在buffer中注入代码,而为了运行注入的代码,同Phase 2 一样需要跳转回栈顶地址0x5561dc78
注意到hexmatch函数中将%r12,%rbp,%rbx入栈,而这样会造成栈中原来输入的内容的覆盖
(三次push分别改变了0x5561dc90,0x5561dc88,0x5561dc80中的内容)
方法一
由于原函数返回地址更低的地方(即0x5561dca8及原栈帧中更低的地方)并不会被覆盖
所以可以将字符串(ASCII码形式)存入0x5561dca8并将sval指针(%rdi)置为0x5561dca8
objdump反汇编后代码:
- 该方法答案:
1 | 48 c7 c7 a8 dc 61 55 68 |
方法二(最先尝试的方法)
注意到0x5561dc88对应%rbp,0x5561dc90对应%r12,所以可以将字符串(ASCII码形式)存入%rbp,并将%r12清零(因为%r12中存着0x3,不清零会把3以ASCII码的形式打出来),将sval指针(%rdi)置为0x5561dc88,这样在hexmatch开头三次push后字符串就被存入了0x5561dc88中:
objdump反汇编后代码:
- 该方法答案:
1 | 48 c7 c7 88 dc 61 55 48 //修改%rdi |
ROP攻击不同于先前的攻击代码注入(注入代码从栈顶写入),所有的gadget都应该从返回地址开始依次写入,而每段gadget运行完ret后都会依次进入下一个gadget
level 4
由于该阶段要实现的效果和level 2一样,所以同样需要将%rdi内容修改为0x59b997fa并跳到touch2函数
由于只能使用mov(Resigter to Register),pop,ret和nop,所以不能直接将立即数转入寄存器中,而需要借助pop将栈中的立即数转入寄存器中
查询farm,发现除了0x58(pop %rax)外没有0x59~0x5f有关能作为gadget的代码
所以汇编代码只能为:
1 | pop %rax |
查询farm可知pop %rax+ret可以用两种gadget表示:(0x90=nop,可以忽略)
movq %rax,%rdi+ret可以用两种gadget表示:(0x90=nop,可以忽略)
而pop的内容(0x59b997fa)应该放在pop+retq指令之后,此时pop指令会将pop后对应位置的元素pop进对应的寄存器中
而touch2函数地址(0x4017ec)应该放在movq+retq指令之后,当ret指令运行完毕后之后的地址会充当返回地址进入touch2函数
- 答案:(所有gadget应该填充至1字节,否则会segmentation fault)
1 | 00 00 00 00 00 00 00 00 |
level 5
由于该阶段要实现的效果和Phase 3一样,所以同样需要将%rdi内容修改为cookie字符串对应地址并跳到touch3函数
由于每次栈都是随机开辟,存入字符串的地址并不固定,所以不能直接把地址赋值给%rdi,而需要通过读取栈顶地址%rsp加上一定的偏移量来获得字符串地址
而地址计算需要lea命令,查找farm:
该命令正好可作为一个栈地址偏移的gadget,%rdi和%rsi一个为栈顶地址,一个为偏移量
继续通过查找farm发现仅仅存在%eax->%edx->%ecx->%esi这样一条路径通过movl为%rdi赋值,而movl指令以寄存器作为目的时,会把该寄存器的高位4字节设置为0,即会损失高四字节的值。而栈顶地址经过gdb断点测试,都至少大于0x7ffffff00000:
而偏移量肯定小于0xfffffffff,所以地址只能存入%rdi中,而把偏移量存入%rsi中
此外由于偏移量必须为正数(高四字节置0),所以输入的字符串不能在前40个字节中(该位置地址比%rsp更小,且可能会被覆盖),只能在所有gadget之后写入(防止干扰栈中gadget)
所以ROP整体思路为:
1.将偏移量pop入%rax中
2.movl指令将偏移量以该顺序:%eax->%edx->%ecx->%esi移入%rsi中
3.movq指令将栈指针以该顺序:%rsp->%rax->%rdi移入%rdi中
4.lea指令计算字符串地址
5.计算结果%rax赋值给%rdi
6.调用touch3
汇编代码为:
1 | pop %rax |
1 | movl %eax,%edx |
1 | movl %edx,%ecx |
1 | movl %ecx,%esi |
1 | movq %rsp,%rax |
1 | movq %rax,%rdi |
1 | lea (%rdi,%rsi,1),%rax |
1 | movq %rax,%rdi |
注意到每次ret后%rsp都会加0x8,%rax内的地址和末尾字符串地址之间的差等于(两者之间的命令个数+1)*8
从movq %rsp,%rax算起有两个movq和一个lea,所以偏移量为(3+1)*8=32=0x20
调用gdb发现输入的字符串之后的字节为0xf4f4f4f4f4f4f400,末尾两位为0,由于字符串是从右向左输出,所以0xf4f4f4f4f4f4f400不会输出多余字符,也就无需将该字节清零
- 最终答案(答案不唯一):
1 | 00 00 00 00 00 00 00 00 |