VMPwn学习笔记
前言
从ByteCTF第一次接触vmpwn以来一直这类题一直做不好,假期选了几道典型的做了一下,总结一下做题的基本思路。
D^3CTF babyrop
程序分析
vm类题目一般都是模拟一个虚拟机,最关键的地方就是逆指令,这道题涉及到的寄存器较少,逆一下发现基本就是在模拟栈的push、pop、mov等操作,一般来说我们的思路是找到一个已知的libc地址,通过add offset把它改造成one_gadget,再用mov等指令移动到rip的位置。这道题就是这样。
开始程序在bss上找了块区域存放我们的指令和数据,在vm里会对0x202040数据进行处理。main_func里给了一堆switch case,我们可以看到*global_addr2
起的应该是栈指针的作用,通过它的增减来模拟栈的增长或减少。*(_QWORD *)global_addr3 = &v7;
使得*(_QWORD *)global_addr3
存储栈地址,实际上是对这个栈进行操作,*(_DWORD *)(global_addr3 + 0x10) = 10;
即global_addr3[4]=10
这里的值10其实就是10*8=0x50,表示栈空间的大小。在后面的分支函数中被用来控制函数是否能成功调用。
1 | void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
在这里有几个比较关键的函数,在逆向的时候就可以考虑到,首先是这个AddStackPointer
可以往下增加栈指针,达到溢出的效果,我们的输入地址为rbp-0x60
,这个函数一次将栈指针增加0x50,因此我们不能一次就达到目的,需要执行两次,但是这里有限制!a1[4]
的时候会失败,而我们执行一次会让a1[4] -= 10
初始值为10,所以要想办法改一下这个值,看看函数就会发现还是有很多,减到0再加个1即可。这样就突破了栈空间的限制。
1 | signed __int64 __fastcall AddStackPointer(_DWORD *a1, _QWORD *a2) |
漏洞利用
有了上面的函数只能算是有了第一步,我们还得构造出one_gadget出来,调试一下看栈空间,是我们第一次加0x50后的栈内容,0xbd8为返回地址,我们调用两次这个stack_pointer_add到0xc10。在其上方0x*bf8有个libc相关的地址通过PushLongLong
函数让栈指针上移,push一个offset
到这个libc所在栈+8的位置再用AddEsp_Esp_1
将其相加得到one_gadget
地址,最后多次执行DoSth
把其换到retn_addr
所在位置(注意我们使用的gaeget的条件是rsp+0x30=NULL
所以中间有一步mov [rsp+8], 0
是为了让gadget满足条件)
1 | signed __int64 __fastcall PushLongLong(_DWORD *a1, __int64 a2) |
exp.py
exp来自官方wp
1 | from pwn import * |
CISCN2019 Virtual
前言
剩下两篇是看0xC4m3l师傅在先知发的博客学习的,此为第一篇
程序分析
程序在堆上分配了几块内存用来存放数据指令和输出,其中指令以空格区分开,在提取指令的函数里给了我们实现功能的提示,这里是将ptr
的输入指令流提取到ins
里。对照着我们可以在main_method
里确定函数
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
漏洞分析
因为写博客的时候距离做题过去了很久函数什么的已经弄不太清了所以函数就不怎么细讲了,这里直接看产生漏洞的函数,res是我们可控的输入数据,这里没有检查,因此可以越界写数据。基本思路是越界写把output
的node
的chunk_addr
改成puts@got
,再计算system和puts的实际地址,将差值通过add
加回去,最终push回去,从而hijack got表地址,在puts程序名的时候执行system函数。这个题困扰我的是调整栈平衡Blabla总之简单的exp写了很久。
1 | signed __int64 __fastcall Save(node *output) |
exp.py
1 | #coding=utf-8 |
OGEEK Final OVM
程序分析
是OGEEK线下决赛的一道vmpwn,bss上分配了一块区域放寄存器,其中reg[13]
为sp
,reg[15]
为pc
寄存器。execute将输入数据(四字节)分成1234四个字节,最高字节表示commnd
,之后是reg[three]和reg[two]以及reg[one]的运算,其中有输出寄存器内容的地方。
在choice分别为0x60和0x70的时候没有检查值的范围导致越界读写,我们可以通过sub等功能输入负数进去从而存储got表的值到reg里最终泄露出Libc地址,需要注意的是我们操作的单位都是4字节,因此我们需要泄露两组reg,之后通过add或者sub加上offset得到__free_hook-8
的地址写到comment[0]
,read的时候输入”/bin/sh\x00+system_addr”最后sendmsg会free(comment[0])即可get shell。
1 |
|
exp.py
1 | #coding=utf-8 |
RedHat线下粤湾银行
程序分析
32位程序,new就分配堆空间存储数据,每次分配0x2c的空间来做各种计算,play就取数据分析,free就释放分配的空间,这里有double free。
1 | 00000000 |
new的node大概如上面所示。play是主要函数。在0x10可以读写idx3,在0x40/0x43处可以越界设置idx3的值,我们设置为put@got之后putchar泄露低位,再使got地址自增1继续泄露,最终泄露得到libc,同理可以用putchar设置free@got的值为system,设置ptr->idx0为”/bin”,ptr->idx为”/sh\x00”,最后free的时候触发system(“/bin/sh\x00”)
1 | node *new() |
exp.py
1 | #coding=utf-8 |
粤湾中心
前言
发了之后发现红帽还有道VmPwn,今天做完补一下
程序逻辑
输入esp/eip/code,开始有seccomp禁止执行execve,welcome里把flag读了出来并且fddup拷贝到了0x233
,这就很像19年信安竞赛的题,我们想办法改stdin的filno为0x233,在最后say goodbye调用printf的时候会输出flag。
1 |
|
主要的功能函数相对来说比较简单,主要是对8个寄存器的值进行计算以及一个malloc到bss上的堆地址的赋值
1 |
|
我们可以通过一些加减操作构造reg[i]
为任意值,而其中有几个函数的reg[i]
类型为signed int,因此会前溢越界,先把stdin的低4字节拷贝到reg里加0x6c到fileno的位置-4再把它放回到reg_addr的位置,SetReg_0即可往里写0x2331
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17unsigned __int64 __fastcall ArbRead(signed int *a1, signed int *a2)
{
unsigned __int64 v2; // ST18_8
v2 = __readfsqword(0x28u);
regs[*a1] = dword_203080[*a2]; // 越界
return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 __fastcall SetReg_0(_DWORD *a1)
{
unsigned __int64 v1; // ST18_8
v1 = __readfsqword(0x28u);
*(_DWORD *)(reg_addr + 4LL * ++_esp) = *a1;
return __readfsqword(0x28u) ^ v1;
}
exp.py
1 | #coding=utf-8 |
总结
VmPwn对于逆向要求的更多,要理清楚各个函数对应操作的指令。不太懂的时候可以动态地调试查看各种操作造成的结果。虽然慢,但是慢慢来也会理清楚,现在看到的大多数都是越界读写造成的问题,有时候也不需要泄露libc地址,直接通过mov等指令结合add/sub计算得到system或one_gadget。最终getshell。