CSAPP Lab -- Attack Lab
出师不利啊,上来就碰到执行不了的错误。
参照网上的说法执行的时候添加-q
参数即可。
通过WriteUp文件可以得知,我们的目标为touchx函数。
Tips:
这里建议使用cgdb工具(版本至少为0.7.1),从0.7.1版本开始,cgdb允许使用反汇编窗口(通过按
esc
然后输入:set dis
启用)。指令码我们可以通过编写汇编程序,然后使用
gcc -c
对其汇编之后,再使用objdump -s -d
反汇编获得。
ctarget – CI
Touch 1
拿到手上先执行一下这个程序,随意提供一些输入可以看到关于注入失败的提示。
根据提示信息尝试搜索Getbuf函数,搜索结果如下。同时也得知了getbuf仅在test中被调用。
1 | 00000000004017a8 getbuf: |
然后根据WriteUp可以得知目标函数为touch1。从中可以得知,touch1仅做一次puts操作就调用校验函数。并且touch1函数不包含任何的输入。
1 | 00000000004017c0 touch1: |
可以看到共分配了40个字节给栈来存储输入,因此尝试输入40个字节和39个字节(由于字符串末尾存在一个\0
,因此实际上是输入了41和40个字节)看看。发现果然输入40个字节的事后发生了溢出。
因此在0x4017b4地址处设置断点,仅输入39个字符(未溢出),然后查看栈情况。
可以看到前40个字节是我们输入的数据,而之后的数据应该是getbuf函数的返回地址,这里我们打印一下看看。
对这个地址进行反汇编,可以发现于test函数中getbuf返回的地址一致。
我们的目标是跳转到touch1函数(地址0x00000000004017c0),因此构造payload:
1 |
|
对其编译并执行得到我们需要的payload字符。
将其提交通过!
Touch 2
用同样的方式进入touch2函数(地址0x00000000004017ec)看看。可以看到校验失败,说明touch2不能通过简单的跳转完成。
进一步分析touch2的代码。touch2对输入变量和2108642(%rip)
的值进行比较,如果不相等则打印”Misfire…”。
1 | 00000000004017ec touch2: |
2108642(%rip)
的值是一个不可访问的内存,但是根据gdb给出的信息来看,它应该是cookie的值。
首先做一个trick试试,将返回地址设为0x0000000000401804,即直接跳过判断语句。
依然校验失败,说明不能这么做,只能老老实实的想办法修改touch2的输入参数,将其修改为Cookie(0x59b997fa,别想着修改cookie文件,这个是没有用的)。
在调用getbuf时,rsp寄存器的值为0x5561dc78,并且rsp可以存储40个字节,因此可以想办法通过这里来注入自己的代码。
另外,Gets函数体内有一个save_char函数会将输入的字符放入到一个数组中。这个机制也可以利用,但是相比之下更为复杂。
要修改目标地址的值,那么就要设计指令movq $0x59b997fa, %rdi
。因此构造payload.c如下:
1 | // movq $0x59b997fa, %rdi |
这里方法不唯一,但是存在一个问题。经测试,如果采用继续覆盖的方式将第二个跳转地址写入栈,这个会引发segment fault错误;此外采用mov指令直接修改(%rsp)的值,再跳转也会引发segment fault错误。仅使用push的方式不会引发。猜测是由于上述的两类方式不符合栈的使用规则,从而导致的问题。
这里简单解释一个原因,首先我们使用了缓冲区溢出,覆盖了getbuf的返回地址,将其指向我们设计的指令的地址,然后再通过我们的指令修改rdi寄存器的值,并通过ret跳转到touch2函数。
getbuf执行完ret之后,跳转到了我们写入字节的起始位置,然后将目标地址压栈,并通过ret语句跳转到touch2函数。
将其提交,最后成功通过!
Touch 3
先来观察一下touch3的要求,(这里使用的ida工具进行的反汇编)。可以看到它是将输入的字节与cookie一起送入一个hexmatch的函数进行比较。
然后进一步查看hexmatch函数的内容,大致可以猜测其检查输入的16进制字符串是否与一个16进制数匹配。
因此构造payload,大致意思就是将一个字符串写入栈中,然后将该字符串的地址传给rdi寄存器。我们把字符串放在0x5561dc78的位置,并在栈中添加至少预留48个字节的空间来避免字符串被覆盖。
由于getbuf最后会给rsp增加40字节,并且之后执行ret指令将返回地址出栈。(相当于共执行pop 6次)。这就导致写入的字符串会位于未被分配的空间,从而造成字符串的覆盖。
此外还需要考虑栈对齐的问题,即使我们把字符串写在了返回地址之后,让它所在的空间不是未分配空间。但是如果栈指针不16字节对齐,sprintf函数会检查栈的对齐问题,从而导致出错。
1 | // mov $0x5561dc78, %rdi |
rtarget – ROP
这里因为明确说了,使用ROP攻击,而不是代码注入。因此我们的攻击代码为在程序中精心挑选的指令来实施攻击。这部分的实验,提供了一个精简的指令组farm.c,里面都是很简单的函数来供我们选择。
由于ROP攻击方式是可以绕过NX/DEP(内存区域不可执行)和ASLR(栈地址随机化)防护的,因此我们下面的实验也假设程序是设置过上述防护的。
Touch 1
这个与ctarget的touch 1完全一致,因此不考虑。
Touch 2
touch2的函数与ctarget一致。因此我们同样需要想办法修改rdi寄存器来让它与cookie的值相等。
因为farm提供函数均为操作rdi或者rax寄存器。farm函数只提供了对(%rdi)
的操作,而没有提供我们需要的直接修改rdi寄存器值的指令。
所以,要修改rdi寄存器的值,必然要找到popq
或movq <>, %rdi
指令。结合我们ctarget部分所学习到的代码注入,可以得知,需要执行的指令不一定是程序中存在的某一条指令,也可以是某个连续的字节码(即某条指令的一部分,或者多条指令的中间部分)恰好满足我们需要的指令的字节码。
因此利用gcc得到了下列指令,我们在farm中搜寻满足我们需要指令的字节。
1 | 0: 48 89 f7 mov %rsi,%rdi |
最终找到了addval_273函数包含所需的机器码48 89 c7 c3
。而且5f c3
并不存在,这样就没办法直接通过栈来给rdi寄存器赋值。
1 | 00000000004019a0 addval_273: |
这样就可以通过rax寄存器来给rdi寄存器赋值了。接着搜索58 c3
,利用pop指令来给rax寄存器赋值。但是这个值并没搜到,最接近一个值为58 90 c3
。
1 | 00000000004019ca getval_280: |
这里我编写了一个脚本来查看我们输入的字节码对应的指令是什么。大致作用就是利用capstone引擎,来实现从控制台读取16进制字符串,从而解析成对应的汇编指令。
1 | #!/usr/bin/env python3 |
利用python脚本对指令的解析结果如下。可以看到多出来的90
对应的是nop指令,它是一个空指令,因此可以忽视。
这样,我们同时获取到了popq %rax
和movq %rax, %rdi
指令。现在我们只需要将cookie的值送入栈中,并设置好返回地址即可完成。
构造payload:
1 | char buf[] = { |
Touch 3
根据ctarget的信息,我们要想办法把一个字符串写入栈中,并且将字符串的地址送入rdi寄存器。
首先我们要想办法,把栈中的一个地址送入寄存器。因为我们的字符串是存在栈中的,要获得栈某个位置的地址,要么对esp寄存器使用add或sub指令,然后再通过mov指令将地址送入寄存器;要么直接利用lea指令来计算一个有效地址,送入寄存器。
在farm中发现,add_xy函数的指令是满足我们需要的。
1 | 00000000004019d6 add_xy: |
但是我们还需要找到能够操作rsi寄存器的指令。由于这个指令在farm的字符中找不到,所以我们借助工具ROPgadget来搜索这个指令。通过工具在main函数的末尾找到了我们需要的这个指令。5e
代表的是popq %rsi
指令。
1 | 401382: 41 5e popq %r14 |
因为我假设了栈地址是随机化的,所以还需要找到获取rsp值的指令。所幸在farm中存在这么一个指令48 89 e0
对应的是movq %rsp, %rax
。
1 | 0000000000401aab setval_350: |
至此我们已经拥有了如下指令:
地址4019a2
1
2movq %rax,%rdi
ret地址4019cc
1
2
3popq %rax
nop
retq地址4019d6
1
2leaq (%rdi,%rsi), %rax
retq地址401383
1
2popq %rsi
retq地址401aad
1
2
3movq %rsp, %rax
nop
retq
因此根据上述信息构造我们的payload:
这里千万要注意,编写好的指令,最终进入touch3的时候,一定要满足栈指针是16Bytes对齐的,不然sprintf函数对栈进行check时会出错。
1 | /* |
最后成功通过!
问题
对于touch3,不管是哪种攻击方式都可能出现如下错误。就算反复确认传入的指令是正确的,并且传入touch3时一切如预期执行,但是依然会出现这个错误。
经过我的反复检查,最后发现是由于中间利用了ret指令,而ret指令会修改rsp的值(相当于执行了pop),这就导致了栈指针不一定是16 Bytes对齐的。
如果栈指针不是16 Bytes对齐的,在执行到hexmatch函数内部的sprintf函数时,会调用一个___sprintf_chk函数来确认栈。也正是它,在栈不是对齐的时候,会引发segment fault错误。
参考资料
[1] WriteUp
[2] 【技术分享】ROP技术入门教程
[3] ROPgadget
CSAPP Lab -- Attack Lab