查看原文
其他

vmp 相关的问题

L0x1c 看雪学苑 2022-07-01


本文为看雪论坛精华文章
看雪论坛作者ID:L0x1c



1


搭建环境


主要的模拟环境是capstone和unicorn。
 
capstone:https://github.com/aquynh/capstone
 
unicorn:https://github.com/unicorn-engine/unicorn
 
VMProtect 3.5.0:https://down.52pojie.cn/Tools/Packers/
 
正常测试结果:
 


2


模拟环境


最开始的时候我们将优化关掉,之后黑盒子测试的时候会将优化打开看看加了vmp都有什么区别的。
 
 
样本代码:
#include <stdio.h>#include <stdlib.h>#include <Windows.h>#pragma warning(disable : 4996) char buf[1204]; void main(){ while (1) { scanf("%s", buf); if (!strcmp(buf, "123")) printf("ok\n"); else printf("fail\n");c } }

如果我们开启优化的话我们可以看到像strcmp这样的函数将会内嵌到main中:
 
 
但是如果我们不启用优化 就可以看到是进行call然后外平栈很传统的一个方式:
 
 
我们将样本加vmp壳:
 
 
我们拖到xdbg进行调试,定位到main以后看一下:
 
 
这里的push call相当于一个进入虚拟机的一个标志,相当于假call,我们在call下面进行断点的时候会发现,无法断下来,因为已经跑飞了。
 
 
测试就用CE来测试我们这个是假的call,可以看到我们程序跑起来了后我们这里没有断下来切这个位置是cc。
 
 
我们现在需要模拟环境,所以用模拟的环境来跑我们的vmp。
 
因为我们当前的EIP在.vmp0的节中,还有我们当前的线程堆栈的状态,我们需要dump下来,一个大小是0x8A000一个是5000。
 
 
需要修改我们的当前的寄存器的状态在我们的模拟环境中,和当前dump下来内存的大小和名字地址。
 
 
因为我们的模拟器是while(1)的,所以我们跑的时候一定会出现一定的异常比如说我们的scanf需要进内核但是我们没有内存映射所以就会产生异常而中断下来,这里面模拟器的一些参数比较有用:
 
这里可以看到我们模拟成功了,和我们这边的参数是一样的,address: 0x485b76相对应。
 
 
比较有用的就是我们这里的detail。
 
detail->x86->op_count :表示我们有多少个操作数。
 
operands:

detail中的regs_read这几个,代表了隐式读,隐式写。
 
隐式读,隐式写:举个例子,push ebp我们看到这个指令都知道我们要对ebp进行压栈的操作,所以对ebp有个读的操作,这个操作叫做显示读,但是我们也会对esp进行读写的操作,所以对于esp有个隐式读隐式写的操作,那么这个操作的作用就是有什么我们会对eflag也就是标志寄存器进行隐式读写的操作,这个到后面进行污点分析等等的有所用处。
 
 
我们现在让他跑起来,可以看到这里报了一个读到了一个没有映射地址的地方我们过去看一下0x473594。
 
 
这里稍微提一下eflags的TF位,我们的TF位是的变动和我们的断点的机制有关这个xdbg我们下断点跑起来的时候我们的TF位会变成1如果我们单步的话就不会有这个问题的 (可以自己稍微尝试一下),这里可以看到我们的403370是我们输入的buffer,4020108是我们的scanf的参数%s,47295d是我们的返回地址,4010c0是我们scanf的地址。
 
 
我们既然知道这些,我们就可以模拟scanf,因为像系统的一些dll等等的系统库,不可能vmp将所有的都加上了vmp所以我们当调用他们的时候是没有vmp的且模拟器会中断,需要自己模拟类似于scanf的这些函数去进行继续执行。
 
模拟scanf代码:我们当前的eip在0x473594我们断下来,让我们的buffer中的值等于我们想要的值,修改我们的eip是我们的返回地址,esp当前+8,修改到我们调用完scanf的地址即可,因为我们用到了data和rdata的位置我们需要dump下来。
case 0x473594:{ DWORD val = 0x00343332; uc_mem_write(uc, 0x403370, &val, 4); regs.regs.r_eip = 0x47295d; uc_reg_write(uc, UC_X86_REG_EIP, &regs.regs.r_eip); regs.regs.r_esp += 8; uc_reg_write(uc, UC_X86_REG_ESP, &regs.regs.r_esp);}

我们继续跑,可以看到断下来的位置是我们代码中的strcmp:
 

这里有个小知识点,就是我们经过call等东西,eax、ecx、edx是易变寄存器,他们经过call的时候是可以改变的,很大几率而其他的寄存器一般在发生call之后的时候不会改变,叫不易改变寄存器。
 
模拟strcmp代码:
case 0x472ecc:{ regs.regs.r_eax = 1; err = uc_reg_write(uc, X86_REG_EAX, &regs.regs.r_eax); regs.regs.r_eip = 0x4854c1; err = uc_reg_write(uc, X86_REG_EIP, &regs.regs.r_eip); regs.regs.r_esp += 8; err = uc_reg_write(uc, X86_REG_ESP, &regs.regs.r_esp);}

跑起来后:
 
 
当然我们也可以模拟代码中修改eax的值变成0看看结果:
 


3


代码块


我们指令进行单步执行的时候,可以知道我们的jmp不会打扰我们的执行跳转 ( jmp 立即数 ) 可以把这个指令当作!啥也不是!没错直接给他干掉 其他的像ret、ja、je、jmp寄存器、call啦都是不一定的,所以我们将他们打印出来。
!strcmp(insn->mnemonic,"ret")||(!strcmp(insn->mnemonic,"jmp") && insn->detail->x86.operands[0].type != X86_OP_IMM)||!strcmp(insn->mnemonic,"call")||(insn ->mnemonic[0] == 'j' && insn->detaicl->regs_read_count == 1 && insn->detail->regs_read[0] == X86_REG_EFLAGS)

 
这里面的ret都相当于:
push regret

因为新版本的符合一个叫做寄存器轮转的问题所以他可能jmp ebp,jmp edi等等的。
 
 
这里的ja 都是一个地址,这个是因为他会判断自己的VM_STACK 虚拟栈,栈式虚拟机实现起来方便,膨胀倍数高,是虚拟机保护的首,虚拟栈就是临时进行数据交换,VMProtect 的 EBP 寄存器就是虚拟栈的栈顶指针,如果感觉膨胀到一定的时候需要提升栈的空间防止虚拟机空间溢出,所以会有很多ja的指令去判断,是不是到了规定的大小的值。



4


局部混淆


我们如果分析虚拟机的时候需要去处理一下局部混淆的问题,我们看一下我们call进去的虚拟机的样子指令,可以看到有一堆莫名其妙的指令,那么这些指令大部分都是我们的混淆指令:
 
 
像一个虚拟机中用特别多的jmp指令,我们就可以把这些jmp的指令当作不存在,因为jmp执行的时候仅仅是跳转而且这些jmp imm的时候我们可以明确知道他跳转到了哪里,分析vmp你可以看到我们的jmp几乎百分之99都是这种类型的jmp imm所以我们把这些jmp当作不存在,将跳转过去的代码和当权的代码块当作同一个代码块进行分析,需要ret call jcc等等的这些指令的时候我们当作是代码块的结束的位置。
 
 
因为我们现在是先分析局部混淆所以我们就先对这个局部混淆的位置进行分析,一直到0x40bc54,我们把这个代码都打出来从头:
 

Emulate i386 code00485B76 push 0x4cc4908400485B7B call 0x441d3300441D33 pushfd00441D34 stc00441D35 clc00441D36 push edi00441D37 rcl edi, 0x6b00441D3A xchg edi, edi00441D3C push edx00441D3D btr edi, edi00441D40 push ecx00441D41 btr di, ax00441D45 rol edi, 0xc00441D48 bswap cx00441D4B push eax00441D4C push esi00441D4D push ebx00441D4E clc00441D4F push ebp00441D50 bswap si00441D53 bts eax, ebx00441D56 mov ecx, 000441D5B cbw00441D5D push ecx00441D5E mov bl, 0x9900441D60 clc00441D61 mov esi, dword ptr [esp + 0x28]00441D65 btr edi, edx00441D68 not bp00441D6B bts edi, eax00441D6E ror esi, 100441D70 movzx eax, bp00441D73 bsf eax, esi00441D76 lea esi, [esi - 0x1394580a]00441D7C bswap esi00441D7E sal al, 0xe600441D81 sbb bx, bp00441D84 xor esi, 0x5bf674ed00441D8A movsx ebp, cx00441D8D btc ebx, ebx00441D90 not esi00441D92 adc bp, 0x66ec00441D97 stc00441D98 bswap esi00441D9A and ebp, 0xa934b3100441DA0 stc00441DA1 lea esi, [esi + ecx]00441DA4 lahf00441DA5 mov ebp, esp00441DA7 lea esp, [esp - 0xc0]00441DAE rcl ebx, cl00441DB0 mov edi, ecx00441DB2 mov ebx, esi00441DB4 xadd di, cx00441DB8 mov eax, 000441DBD xor edi, eax00441DBF sub ebx, eax00441DC1 or edi, 0x1a7d74b800441DC7 rcl di, 0xe500441DCB lea edi, [0x441dcb]00441DD1 rcr ecx, cl00441DD3 mov ecx, dword ptr [esi]00441DD5 stc00441DD6 add esi, 400441DDC xor ecx, ebx00441DDE jmp 0x42fdc30042FDC3 bswap ecx0042FDC5 jmp 0x47eef00047EEF0 dec ecx0047EEF1 stc0047EEF2 neg ecx0047EEF4 jmp 0x40bc450040BC45 add ecx, 0x29410830040BC4B clc0040BC4C test dl, 0x4c0040BC4F xor ebx, ecx0040BC51 add edi, ecx0040BC53 push edi0040BC54 ret
这里的局部混淆有个特点的总结,就是我们将所有的jmp都去掉,我们对寄存器的写操作,第二次写会覆盖第一次写的操作,所以第一次的写的命令就是混淆,这里有个小技巧就是我们假设要看ecx寄存器我们就可以在xdbg用按h点击ecx即可:
 
这里可以看到我们的ecx已经被ds:[esi]内存地址进行赋值了,我们的rcr的命令就无效了没有用了就相当于混淆指令可以去掉,但是上面的对ecx进行读取的操作所有不知道是不是混淆需要继续去分析的。
 
 
手动大概去除混淆的代码:
00441D33 pushfd00441D36 push edi00441D3C push edx00441D40 push ecx00441D4B push eax00441D4C push esi00441D4D push ebx00441D4F push ebp00441D56 mov ecx, 000441D5D push ecx00441D5E mov bl, 0x9900441D61 mov esi, dword ptr [esp + 0x28]00441D6E ror esi, 100441D76 lea esi, [esi - 0x1394580a]00441D7C bswap esi00441D84 xor esi, 0x5bf674ed00441D90 not esi00441D98 bswap esi00441DA1 lea esi, [esi + ecx]00441DA5 mov ebp, esp00441DA7 lea esp, [esp - 0xc0]00441DB2 mov ebx, esi00441DB8 mov eax, 000441DBF sub ebx, eax00441DCB lea edi, [0x441dcb]00441DD3 mov ecx, dword ptr [esi]00441DD6 add esi, 400441DDC xor ecx, ebx0042FDC3 bswap ecx0047EEF0 dec ecx0047EEF2 neg ecx0040BC45 add ecx, 0x29410830040BC4C test dl, 0x4c0040BC4F xor ebx, ecx0040BC51 add edi, ecx0040BC53 push edi0040BC54 ret

我们生成字节码去测试一下生成一个新的内存空间:
9c 57 52 51 50 56 53 55 b9 00 00 00 00 51 b3 99 8b 74 24 28 d1 ce 8d b6 f6 a7 6b ec 0f ce 81 f6 ed 74 f6 5b f7 d6 0f ce 8d 34 0e 8b ec 8d a4 24 40 ff ff ff 8b de b8 00 00 00 00 2b d8 3e 8d 3d cb 1d 44 00 8b 0e 83 c6 04 33 cb 0f c9 49 f7 d9 81 c1 83 10 94 02 f6 c2 4c 33 d9 03 f9 57 c3

我们先分配一个页的内存:
 
 
用ce去获取字节码赋值到内存区域修改call再跑起来:
 
 
直接f9看是否成功满足我们说的原理,可以看到是对的,如果还想继续很细致的恢复去混淆的话,就需要自己调试的时候细致去分析了。
但是如果想自动化去除混淆的话,需要细分寄存器的读写和eflags的读写规则(lea指令是不访问内存的)。
  • 指令的功能在于写
  • 对于同一个位置的连续两次写,第一次写是无效的
([mem]操作数中有对base和index的隐式读)
 
假设例子:
 
由于本人excel不知道怎么了,开启了发飙模式,用不了,直接用简陋的语言叙述来表示了。

正常我们的指令,进行操作的时候,可能会对 寄存器的低8位 16-8位 32 - 16位进行分组,像一些eflags寄存器也需要分组,因为如果我们不分组细致分ZF,OF,AF这些位的时候,我们如果有两个指令对eflags进行写的操作,但是其中最重要的一个eflags的位的地方被第二次抹去了,就会导致最后的程序的错误。

规则就是如果我们对一个地址,寄存器,eflags进行读写操作的时候,我们把他们标识成rw / r / w 的标识,正常的如果假设以ecx位序列的一列,总结出的标识序列应该是rwrwrwrwrwrwrw....这种形式 假设如果出现 wwr 我们就要把第一个w去掉,如果所有寄存器的一行都是r那么该指令无效是混淆指令,我们可以通过这样的方式去去除相应的混淆。

//未完成的计划:代码还没有实现,但是根据capstone应该可以实现出很好的去除混淆代码的方式。


5


函数调用界面


这封图很好的解释了函数调用界面的类型的方式(只针对于vmp来说)。
第一个主要的就是我们的为进行优化版本的函数调用界面,因为他们系统用的函数需要走进内核以及走出,所以已经会进行出虚拟机,进虚拟机的操作。


第二个函数调用界面,可以理解成我们自己写的函数比如就是图上的mystrcmp函数他进行了开启了优化,相当于将自定义函数进行了内联,加上vmp就是相当于第二张图的形式。


第三张图函数调用界面一写debug版的编译会出现这个状况,我们会有出虚拟机,再进虚拟机,再出再进的一个标识在。


第四张图就是如果不开优化,我们的正常的加vmp的一个形式,将我们的main和我们自定义的函数都加上vmp。


测试代码:
#include <stdio.h>#include <stdlib.h>#include <Windows.h>#pragma warning(disable : 4996) char buf[1204]; bool mystrcmp(char* p1, const char* p2){ while (*p1 & *p2) { if (*p1 != *p2) return false; p1++; p2++; } if (*p1 || *p2) return false; return true;} void main(){ while (1) { scanf("%s", buf); if (mystrcmp(buf, "123")) printf("ok\n"); else printf("fail\n"); } }

 
因为我们分析可以发现进入虚拟机的标识就是push call 的标识,我们可以用这个call来定位分析我们的进入虚拟机的位置。
 
 
00401068这个地址是我们的printf的系统的那边的函数:
 
 
那么主要的流程就是:
00449668 call 0x46af7e ;进入虚拟机0043FC17 call 0x46af7e ;scanf后进入虚拟机00467D9E call 0x47608d ;mystrcmp后进入虚拟机

还会有个比较有意思的事情就是:
 
正常我们的cmp函数入口的位置是0x401100,但是我们f9他永远都不运行,因为入口的位置也是虚拟化中的一部分。
 
 
我们也可以通过ebp来找我们想要找的进入虚拟机的位置,假设我们现在再scanf之后的位置看一下ebp:
 
 
scanf的进入:
 
 
进入虚拟机:
 
 
进入虚拟机:
 
 
 
再出虚拟机:
 
 
满足了上面流程图的特点。



6


黑盒测试


我们首先主要dump,自定义比较函数的cmp的内存的位置到printf,因为在正常的比较函数中会有我们相应的一个jcc的一个跳转的形式,我们通过黑盒测试去模拟出一个相应的指令的方式,这是原始代码的一个形式。
 
 
主要的模拟就是我们的这个je加了vmp是什么样子。
 
中途自己测试的时候发现一个好玩的事情,我们的模拟器的TF位置是因为我们下断点之后就会断下来,所以TF位置会置1,但是我们的正常的执行流程的下来TF的位置没有变成1,如果我们把eflags加上了TF位置的值,我们模拟器会自动认为,该句有断点所以只执行当前的一句代码,就不会向下执行了。
 
因为是bool类型只跟我们al有关,所以将al置1的时候就是fail,如果置0的时候就是ok,所以我们可以确定当前的jcc的跳转的格式在我们的虚拟机的代码之中。
 
 
如果我们不确定jcc的类型在虚拟机里都有什么跑的时候,我们打印一下看一下,可以发现大部分都是ja做虚拟机栈看是否溢出的一个保护措施。
所以我们就要去想虚拟机中是怎么去实现je这一个效果的方法,自己模拟一下代码来实现一下:
 
原理(使用pushfd的情况):
#include <stdio.h>#include <stdlib.h>#include <Windows.h> typedef void (*CALL)(); void p1(){ printf("in p1\n");} void p2(){ printf("in p2\n");}#define IS_ZERO(x) 1-((x)|(-x))>>31void nojcc(int a, int b, CALL f1, CALL f2){ __asm { mov eax,a //输入的参数1 到eax sub eax,b //看是否是等于0 pushfd //减法会打扰eflag的值 pop eax //把eflag的值传给eax and eax,0x40 //看第6位是否等于1 即ZF是否等于1 shr eax,6 //左移6位看是否变成了1 mov ecx,1 //ecx置1 sub ecx,eax //将ecx和eax相减,如果eax等于0的情况就是不相等,如果等于1的情况就是相等 neg eax //eax如果是1 取反等于0xffffffff 如果不是就是0 neg ecx //同上 and eax,f1 //eax和f1进行相与,如果他相等了就是1那么neg就是0xffffffff 结果就是f1 and ecx,f2 //同上 add eax,ecx //因为有一个是0 所以一定是有值的 call eax //call最后的结果即可 }}void main(){ nojcc(1, 1, p1, p2); system("pause");}

不使用pushfd的情况:
#include <stdio.h>#include <stdlib.h>#include <Windows.h> typedef void (*CALL)(); void p1(){ printf("in p1\n");} void p2(){ printf("in p2\n");} #define IS_ZERO(x) 1-((x)|(-x))>>31 void nojcc(int a, int b, CALL f1, CALL f2){ __asm { mov eax,a //eax = a sub eax,b //eax - b 看是否等于0 mov ecx,eax //ecx = eax neg eax //eax 取反 or eax,ecx //如果进行抑或不是0的话他们就会等于-1 shr eax,31 //eax留下符号位,因为-1的符号位是1,所以只有a=b的时候shr eax 31 = 0 mov ecx,1 //ecx = 1 sub ecx,eax //ecx = ecx - eax mov eax,ecx //eax = ecx mov ecx,1 //ecx = 1 sub ecx,eax //ecx = ecx - eax neg eax //同上了 neg ecx and eax,f1 and ecx,f2 add eax,ecx call eax }}void main(){ nojcc(1, 1, p1, p2); system("pause");}

但是我们的vmp没有这么极限,他还是使用了pushfd的操作的,但是在vmp里是没有减法的,这个eflag的标志位的改变是比较复杂的。
推理: a-bNOT(NOT(a)+b) = a-bNOT(a) = -a -1 (视为有符号)NOT(-a-1+b) = a + 1 - b - 1 = a - b

结果标志位: Z , S...
 
过程标志位: C , O...
 
虽然说假设我们知道结果可以判断一个Z或者S位的东西,但是我们的过程标志是有可能发成改变的,假设0+1 = 0xFFFFFFFF + 2
 
针对于一个无符号的( 0 - 2^32 )我们溢出的时候一般都看C位,有符号的就是看O位了,在有无符号的情况下,NOT(a) + b 产生的C/O位与a - b产生的C位总是保持一致的。
//无符号的状态NOT(a) = 2^32 - a - 12^32 - a - 1 + b > = 2 ^ 32 如果大于等于2^32的时候就溢出了 开始置C位b >= a + 1b > aa - b 置C
//有符号的状态NOT(a) = -a - 1(a>=0 b<0 a-b >= 2^31) / (a<=0 b>0 a-b < - 2^31)(-a<=0 b<0 -a-1+b < -2^31) / (-a-1>=-1 b>0 -a-1+b >= 2^31)(-a - 1<0 b<0 -a-1+b < -2^31) / (-a-1>0 b>0 -a-1+b >= 2^31)等价于NOT(a) + b 置O

VMP中 eflag实现的过程:
 
因为上面确定的原则所以如果想要产生真正的eflags我们需要进行两次的pushfd的操作,第一次去取过程化的eflags的操作,第二次去取结果化的eflags的操作,两次的结果进行OR就是我们需要的真正的eflags,所以我们可以对pushfd这个指令进行追踪,这里对eflags的保存还有一个指令就是lahf。
所以我们把pushfd和lahf都打出来,但是在vmp中一般的lahf都是做混淆的,很少使用大部分的情况就是pushfd。
 
比如说这样的情况我们lahf做完操作之后对我们的eax进行重新的赋值,那么lahf就没有意义了。
 
 
输出的结果:
看到这么多的pushfd主要是因为有加减运算的那些,就会产生一次pushfd,但是我们只关心产生z位的那个pushfd。
00482F8B pushfd00436418 pushfd0046639B pushfd0041F1F4 pushfd00420A7E pushfd00429CF9 pushfd0048A175 pushfd0046EBD4 pushfd00421087 pushfd0042B5BE pushfd00464DE9 pushfd0047D936 pushfd00421729 pushfd0048CBC9 pushfd004335A8 pushfd0048C880 pushfd00484710 pushfd

测试一下,是对的,vmp主要根据pushfd进行操作。
 
 
通过二分法可以定位一下影响我们zf的那个pushfd在哪里,修改后的结果:所以pushfd可以控制结果的流程。
 
 
因为像pushfd很多,我们如果出现很多的情况的时候,就不能这么二分法慢慢去找,很浪费时间,所以我们可以根据规则,比如我们pushfd之后我们需要 ,与操作,移位的操作,假设这个指令是and eax,0x40 但是我们不知道第二个操作数他是立即数还是寄存器还是内存,或者说他是第一个操作数还是第二个操作数我们是未知的,所以我们要写一个规则通用的:
//判断是什么寄存器DWORD get_reg(x86_reg reg) { switch (reg) { case X86_REG_EAX: return regs.regs.r_eax; case X86_REG_AX: return regs.regs.r_eax & 0xffff; case X86_REG_AH: return (regs.regs.r_eax >> 8) & 0xff; case X86_REG_AL: return regs.regs.r_eax & 0xff; case X86_REG_ECX: return regs.regs.r_ecx; case X86_REG_CX: return regs.regs.r_ecx & 0xffff; case X86_REG_CH: return (regs.regs.r_ecx >> 8) & 0xff; case X86_REG_CL: return regs.regs.r_ecx & 0xff; case X86_REG_EDX: return regs.regs.r_edx; case X86_REG_DX: return regs.regs.r_edx & 0xffff; case X86_REG_DH: return (regs.regs.r_edx >> 8) & 0xff; case X86_REG_DL: return regs.regs.r_edx & 0xff; case X86_REG_EBX: return regs.regs.r_ebx; case X86_REG_BX: return regs.regs.r_ebx & 0xffff; case X86_REG_BH: return (regs.regs.r_ebx >> 8) & 0xff; case X86_REG_BL: return regs.regs.r_ebx & 0xff; case X86_REG_ESP: return regs.regs.r_esp; case X86_REG_SP: return regs.regs.r_esp & 0xffff; case X86_REG_EBP: return regs.regs.r_ebp; case X86_REG_BP: return regs.regs.r_ebp & 0xffff; case X86_REG_ESI: return regs.regs.r_esi; case X86_REG_SI: return regs.regs.r_esi & 0xffff; case X86_REG_EDI: return regs.regs.r_edi; case X86_REG_DI: return regs.regs.r_edi & 0xffff; case X86_REG_EIP: return regs.regs.r_eip; case X86_REG_EFLAGS: return regs.regs.r_efl; default: __asm int 3 }} DWORD read_op(cs_x86_op op){ switch (op.type) { case X86_OP_IMM: //立即数 return op.imm; case X86_OP_REG: //寄存器 return get_reg(op.reg); case X86_OP_MEM: //内存地址 { DWORD addr = get_mem_addr(op.mem); DWORD val = 0; uc_mem_read(uc, addr, &val, op.size); return val; } }}
!strcmp(insn->mnemonic,"pushfd")||(!strcmp(insn->mnemonic,"and") && (read_op(insn->detail->x86.operands[0])) == 0x40 || (read_op(insn->detail->x86.operands[1])) == 0x40)||(!strcmp(insn->mnemonic,"shr") && read_op(insn->detail->x86.operands[1]) == 0x6)
00429CF9 pushfd0046EBCF and ecx, eax00421082 shr eax, cl

可以看到现在的这个位置ecx相当于我们说的一个zf(0x40)的位置的一个判断,eax是我们的eflags。
 
 
shr eax,cl cl是0x6。
 
 
开启优化的版本:
 
 
dump scanf到printf的位置,因为我们的cmp的自定义的函数已经内敛到我们的main中所以定位不到入口和出口的位置。
0046B6F3 pushfd00464E7F pushfd00439BD1 pushfd0043EB08 pushfd0041EFCF pushfd004818A8 pushfd0041F922 cmp esp, edx0041F927 and edx, ecx00476992 pushfd00421D22 shr eax, cl00421D2D pushfd0043F7E5 pushfd00426A2B mov dl, al00433BAB pushfd0048C281 pushfd00469322 pushfd0044F6AF pushfd00461644 pushfd00406D91 pushfd00432DDE pushfd00484E50 pushfd0043E3D4 pushfd0047D76A pushfd004292FE pushfd00424EF5 pushfd004847BE pushfd00489C89 pushfd00484F54 rol al, cl004422F1 pushfd00410E26 pushfd0046308B mov dx, ax0041E2FA pushfd0041C103 pushfd00451729 and edx, ecx004725D1 mov dword ptr [esi + 4], edx004725D4 pushfd00483E8C mov eax, dword ptr [esi]00483E8E add cl, al00483E9B shr eax, cl00483EA9 pushfd0046D448 pushfd0041074E pushfd00470720 pushfd0043A841 pushfd0043A284 pushfd004313A6 rcr cx, cl00451B58 pushfd0041D33F pushfd0040AFE2 pushfd0040DC15 pushfd004448CD pushfd0042FC1E pushfd0041DB6F pushfd004730D6 pushfd00448E3F pushfd0048A934 pushfd0044F92B and eax, ecx0044F939 pushfd00454033 shr edx, cl004548C6 pushfd0047305F pushfd00476B19 pushfd0044F99F pushfd004473A7 pushfd00483B58 pushfd004092C0 pushfd00458B9F pushfd00428600 pushfd004811BF movzx ecx, byte ptr [ebp]
0041F927 and edx, ecx00421D22 shr eax, cl 00451729 and edx, ecx00483E9B shr eax, cl 0044F92B and eax, ecx00454033 shr edx, cl

下断跑一下,看一下加了优化后的流程,总结一下流程图第一次,但是我们不知道0x421d22的分支上是否还有别的jcc。
004695F5 and ecx, eax0043CB41 shr eax, cl
00473A47 and ecx, edx0046B07D shr eax, cl
0040A211 and ecx, eax00431FB2 shr eax, cl


7


侧信道攻击


我们可以把我们的程序的流程指令的个数都打印出来,我们如果遇见像分支一类的情况下,我们就可以发现指令的个数会有明显的一个改变的形式,其实这个东西也不是很重要,因为如果我们作为正向的开发肯定原则不会这么去写代码,一定会加上一些加密或者是其他的原则让这种方式不会成功。
 
但是还是要看一下测信道攻击是什么情况:
 
 
如果第一个不是正确的数值路径就会变少:
 
 
所以可以通过这种方式来侧信道攻击从而达到一个确定最后的结果是什么的情况,但是这种问题很容易被加密的算法等等所干掉,让这种办法不可以进行,因为计算机的运算速度不满足(只是说一下可以有这个方式)。



8


污点分析


被污染的数据,在代码的执行指定的时间点上面,因为输入的数据的不同而可能产生不同取值的数据。
 
简要的来说,因为我们的输入去导致了一个数值的修改,我们的这个数值就叫做被污染,那么这个污染的数值如果对另一个数值进行写的操作,那么另一个数值也就被污染了,如果这个数值被其他数值写操作了,那么这个数值就解除污染,这个有点像游戏分析中的数据追踪,去获取谁访问了该数值,然后追踪找到最后的基址的方式是差不多的。
 
污染传播的一般原则:

在一个指令中,如果有至少一个读位置是污染的,就将所有的写位置污染。

在一条指令中,如果所有的读位置都是非污染的,那么就将所有的写位置去污染。


但是比如这些原则来说针对于汇编的一些指令,有些是不满足这个规则的,比如说xchg eax,ecx 我们对两个寄存器都进行了一个读的操作,并且两个寄存器都有写操作,按照我们的原则来说我们的两个寄存器就都会进行污染,但实际上其实就是一个寄存器被污染了,所以像这些指令来说,我们需要拿出来单独的做规则。
 
假设一个例子代码
 
我们的push指令,push eax为例子,我们对eax进行读的操作,对堆栈的内存进行写的操作,要写不同的指令的污点的代码。
if (!strcmp(insn->mnemonic, "push")){//push cs_x86_op op = x86.operands[0]; do_taint_sp_push(op); return g_taint_handled;} /* ------------------------------------------------------------------- */inline static void do_taint_sp_push(cs_x86_op& op){ DWORD esp_after = regs.u[reg_transfer_table[X86_REG_ESP]] - 4; switch (op.type) { case X86_OP_MEM: { DWORD addr = get_mem_addr(op.mem); if (is_addr_tainted(addr) || is_addr_tainted(addr + 1) || is_addr_tainted(addr + 2) || is_addr_tainted(addr + 3)) { for (int i = 0; i < 4; i++) taint_addr(esp_after + i); } else { for (int i = 0; i < 4; i++) untaint_addr(esp_after + i); } } break; case X86_OP_REG: { x86_reg reg = op.reg; if (is_reg_tainted(reg)) { for (int i = 0; i < 4; i++) taint_addr(esp_after + i); } else { for (int i = 0; i < 4; i++) untaint_addr(esp_after + i); } } break; case X86_OP_IMM: for (int i = 0; i < 4; i++) untaint_addr(esp_after + i); break; default: __asm int 3 }}

污点分析最好的一个好处就是因为我们对一个污染源做完操作之后他一定会有一个终止的位置,使用的最佳的位置,比如说我要比较cmp举例子到最后的测试的时候看到大部分都是进行要给 movzx xxx,xxx 这样的指令去写入寄存器 (最后一次的时候) 进行比较。



 


看雪ID:L0x1c

https://bbs.pediy.com/user-home-873515.htm

*本文由看雪论坛 L0x1c 原创,转载请注明来自看雪社区





# 往期推荐

1.记一次头铁的病毒样本分析过程

2.通过CmRegisterCallback学习注册表监控与反注册表监控

3.Android APP漏洞之战——权限安全和安全配置漏洞详解

4.详细分析CVE-2021-40444远程命令执行漏洞

5.通过对PsSetCreateProcessNotifyRoutineEx的逆向分析得出的结果来实现反进程监控

6.分享一个基本不可能被检测到的hook方案



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存