借助Pin插桩技术解决CTF中的VMP问题
前提知识
- 插桩技术:在不改变程序原有运行顺序的基础下,在程序中插入一些用来进行信息记录的探针,通过探针的执行使程序自动抛出一些信息,再加以对这些信息的分析,获得程序控制流和数据流的信息。分为源代码插桩和二进制插桩。
- 源码插桩:修改源代码,插入额外的代码,我们日常调试程序中打log的方法就是源代码插桩。
- 二进制插桩:分为静态二进制插桩和动态二进制插桩。
- 静态二进制插桩:插入额外的汇编代码,生成一个新的可执行程序。
- 动态二进制插桩:用动态库的方式,运行时动态的注入代码,不会改变原有二进制程序。
- Pin:用于IA-32、x86-64和MIC指令集体系结构的动态二进制插桩框架,支持创建动态程序分析工具。
例题及测试环境
- 西湖论剑2020 RE flow
- IDA7.0(Mac)
- docker 镜像选择pwndocker:latest
- pin-3.16-gcc-linux
初期准备
前期黑盒测试
静态分析flow,首先file
~/Downloads/西湖论剑/REVERSE/2 » file flow 127 ↵ wangxiaocheng@MacBook-Pro
flow: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=03ec3130ef3e8a8f345c120e56d0b3073900a11e, for GNU/Linux 3.2.0, stripped
接下来按我的做逆向的思路,会先用脚本做个黑盒测试,用pin的指令计数,看看能不能爆破出来flag。但是实际的结果是不可取的,猜测程序内部没有按位比对或者按块比对判断的机制。
静态分析
加载到IDA64中,找到main函数,直接反汇编
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 result; // rax
char s; // [rsp+0h] [rbp-30h]
int i; // [rsp+2Ch] [rbp-4h]
__isoc99_scanf(&unk_402008, &s, a3);
if ( strlen(&s) == 32 )
{
str2hex(&s, (__int64)input); // tohex
for ( i = 0; i <= 49; ++i )
byte_40C420[i] = 0xCCu;
memcpy(dest, &off_40B280, 0x100uLL);
sub_4017ED(byte_40C420, 306);
puts("Here is your flag: retr0{retr0_yyds_orzzzzz_but_fake_flag}");
result = 0LL;
}
else
{
puts(buf);
result = 0LL;
}
return result;
}
可以看到程序的流程十分简单,输入为32位,然后经过一个str2hex的操作(手动改过名),后面还有一个输出假flag,很明显中间的程序会造成控制流的改变。
接着分析,先是填充了49个0xCC,0xCC一般用于debug版填充栈,猜测这里的0xCC是要做一种保护操作,然后接着两个memcpy,第一个写在了外面,另一个写在了一个函数里面,好家伙,看sub_4017ED
函数,当场来一个栈溢出,猜测是通过rop劫持了控制流。
详细分析
动态跟踪
通过动态跟踪,清楚的可以发现程序到这个地方会飞出去。
接着动态分析了以后程序块跳来跳去,但每个程序块最后都有一个0xF的systemcall,之前学过一点点PWN,所以这个地方是用SROP来保护软件,思路清奇。
后面动态继续分析,前面几个代码块比较简单,可以直接反编译,反编译的结果是RC4算法,后面接着4个字节成一组把加密结果写入到一块内存地址中。当时也就做到这里,后面的虚拟机实在是难提取操作码序列。
接下来程序会在几个代码块中不断的跳转,开始一步一步记录控制流,但是程序流程还是挺长的,所以放弃了这个思路。
分析VMP OP
下面的思路是分析这几个代码块,简单分析,会发现这些代码块有汇编的影子,并且只用了寄存器来运算,接着一个函数一个函数分析,将每个函数重命名后,可以确认这是一个基于寄存器的虚拟机,VMopcode存于栈上,用srop的方式形成控制流。
到这里解决的办法就很多了,动态提取栈上的数据,然后写脚本模拟VM执行;用gdb脚本在每个块前设置断点,打印控制流;用Pin打log的方式记录虚拟机运行流程
我选择用Pin插桩的方式打印运行流程
使用Pin解决VM
插桩粒度 |
API |
执行时机 |
指令级插桩 |
INS_AddInstrumentFunction |
执行一条新指令 |
轨迹级插桩 |
TRACE_AddInstrumentFunction |
执行一个新trace |
镜像级插桩 |
IMG_AddInstrumentFunction |
加载新镜像时 |
函数级插桩 |
RTN_AddInstrumentFunction |
执行一个新函数时 |
- 插桩粒度使用指令级插桩都可以,轨迹级插桩无法传入寄存器类参数
- 选择性插桩,加快执行,在插桩前进行判断,利用trace提供的API获取到指令地址,进行判断,若是我们虚拟机中的几个操作函数即进行插桩,并编辑格式打印出运行log,推荐使用c语言内联汇编格式,可以在十分复杂的情况下,用编译优化进行处理。
编写起来还是很方便的,只用关注指令地址即可。
编写工程中我们用PinTools的模版进行编写。
/*
* Copyright 2002-2020 Intel Corporation.
*
* This software is provided to you as Sample Source Code as defined in the accompanying
* End User License Agreement for the Intel(R) Software Development Products ("Agreement")
* section 1.L.
*
* This software and the related documents are provided as is, with no express or implied
* warranties, other than those that are expressly stated in the License.
*/
#include <stdio.h>
#include "pin.H"
FILE * trace;
VOID printip(ADDRINT * ip,ADDRINT *rax,ADDRINT *rdx,ADDRINT *rcx,ADDRINT *rbx) {
//打印虚拟机log
if((long int)ip == 0x004015F1){ //mov
if(rbx){
fprintf(trace, "%p\tMOV (%p)[%p],input[%p]\n", ip, rdx, rcx, rax);
}else{
fprintf(trace, "%p\tMOV (%p)[%p],ECX(%p)\n", ip, rdx, rax, rcx);
}
}else if ((long int)ip == 0x401594){ //add
if(rbx){
fprintf(trace, "%p\tADD RDX,input[%p]+input[%p]\n", ip, rax, rcx);
}else{
fprintf(trace, "%p\tADD RDX,input[%p]+%p\n", ip, rax, rcx);
}
}else if ((long int)ip == 0x401477){ //xor
if(rbx){
fprintf(trace, "%p\tXOR (%p)[%p],input[%p]\n", ip, rdx, rax, rcx);
}else{
fprintf(trace, "%p\tXOR (%p)[%p],%p\n", ip, rdx, rax, rcx);
}
}else if ((long int)ip == 0x401408){ //cmp
if(rbx){
fprintf(trace, "%p\tCMP (%p)[%p],(%p)[%p]\n", ip, rdx, rax, rdx, rcx);
}else{
fprintf(trace, "%p\tCMP (%p)[%p],%p\n", ip, rdx, rax, rcx);
}
}else if ((long int)ip == 0x401464){ //jmp
if(rdx){
fprintf(trace, "%p\tJNZ (%p) no \n", ip, rax);
}else{
fprintf(trace, "%p\tJNZ (%p)\n", ip, rax);
}
}else if ((long int)ip == 0x401535){ //shr
if(rbx){
fprintf(trace, "%p\tSHL (%p)[%p],(%p)[%p]\n", ip, rdx, rax, rdx, rcx);
}else{
fprintf(trace, "%p\tSHL (%p)[%p],%p\n", ip, rdx, rax,rcx);
}
}else if ((long int)ip == 0x4014D6){ //shl
if(rbx){
fprintf(trace, "%p\tSHL (%p)[%p],(%p)[%p]\n", ip, rdx, rax, rdx, rcx);
}else{
fprintf(trace, "%p\tSHL (%p)[%p],%p\n", ip, rdx, rax,rcx);
}
}
}
VOID Instruction(INS ins, VOID *v){
long int opList[] = {0x004015F1, 0x401594, 0x401477, 0x401408, 0x401464, 0x401535, 0x4014D6};//需要插桩的地址
long int ip = INS_Address(ins);
bool flag = false;
for (size_t i = 0; i < 7; i++){
if(ip == opList[i]){
flag = true;
break;
}
}//进行指令级别插桩
if(flag){//IPOINT_BEFORE在指令执行前插桩
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)printip,
IARG_INST_PTR,
IARG_REG_VALUE,REG_EAX,
IARG_REG_VALUE,REG_EDX,
IARG_REG_VALUE,REG_ECX,
IARG_REG_VALUE,REG_EBX,
IARG_END);
}
//以IARG_REG_VALUE,REG_EAX的形式将执行这条指令前的寄存器值传入
}
VOID Fini(INT32 code, VOID *v)
{
fprintf(trace, "#eof\n");
fclose(trace);
}
/* ===================================================================== */
/* Print Help Message */
/* ===================================================================== */
INT32 Usage()
{
PIN_ERROR("This Pintool log flow\n"
+ KNOB_BASE::StringKnobSummary() + "\n");
return -1;
}
/* ===================================================================== */
/* Main */
/* ===================================================================== */
int main(int argc, char * argv[])
{
trace = fopen("itrace.out", "w");
// Initialize pin
if (PIN_Init(argc, argv)) return Usage();
// Register Instruction to be called to instrument instructions
INS_AddInstrumentFunction(Instruction, 0);
// Register Fini to be called when the application exits
PIN_AddFiniFunction(Fini, 0);
// Start the program, never returns
PIN_StartProgram();
return 0;
}
分析VM log
正常运行,输入32个字符,观察输出文件。
由于这个地方的汇编比较简单,没有打印为C语言内联汇编形式,再去让编译器优化后反编译。
此时被虚拟机保护起来的控制流已经被恢复,直接阅读汇编代码即可,看到9e3449b9条件反射是tea类的加密,四个密钥就是0x79757361、0x79796473、0xdeadbeef、0xaa114514,结合jmp,将前段代码复制后全局搜索
可以看到这段重复了64次并且在文档的最中间有一段小不同,可以判断是进行了两次32次的循环,结合我们输入会转化为16个字节,4字节分为一组,可以判断这里是做了两次的TEA加密。汇编流程也很简单,就是TEA的加密过程。
解密
跟踪到最后,会找到比对的数据,提取出来先解密TEA再RC4解密即可。