vmp 相關的問題
搭建環境
主要的模擬環境是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/
正常測試結果:

模擬環境
最開始的時候我們將優化關掉,之后黑盒子測試的時候會將優化打開看看加了vmp都有什么區別的。

樣本代碼:
#include #include #include #pragma warning(disable : 4996) char buf[1204]; void main(){ while (1) { scanf("%s", buf); if (!strcmp(buf, "123")) printf("ok"); else printf("fail");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, ?s.regs.r_eip); regs.regs.r_esp += 8; uc_reg_write(uc, UC_X86_REG_ESP, ?s.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, ?s.regs.r_eax); regs.regs.r_eip = 0x4854c1; err = uc_reg_write(uc, X86_REG_EIP, ?s.regs.r_eip); regs.regs.r_esp += 8; err = uc_reg_write(uc, X86_REG_ESP, ?s.regs.r_esp);}
跑起來后:

當然我們也可以模擬代碼中修改eax的值變成0看看結果:

代碼塊
我們指令進行單步執行的時候,可以知道我們的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的指令去判斷,是不是到了規定的大小的值。
局部混淆
我們如果分析虛擬機的時候需要去處理一下局部混淆的問題,我們看一下我們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應該可以實現出很好的去除混淆代碼的方式。
函數調用界面
這封圖很好的解釋了函數調用界面的類型的方式(只針對于vmp來說)。

第一個主要的就是我們的為進行優化版本的函數調用界面,因為他們系統用的函數需要走進內核以及走出,所以已經會進行出虛擬機,進虛擬機的操作。
第二個函數調用界面,可以理解成我們自己寫的函數比如就是圖上的mystrcmp函數他進行了開啟了優化,相當于將自定義函數進行了內聯,加上vmp就是相當于第二張圖的形式。
第三張圖函數調用界面一寫debug版的編譯會出現這個狀況,我們會有出虛擬機,再進虛擬機,再出再進的一個標識在。
第四張圖就是如果不開優化,我們的正常的加vmp的一個形式,將我們的main和我們自定義的函數都加上vmp。
測試代碼:
#include #include #include #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"); else printf("fail"); } }

因為我們分析可以發現進入虛擬機的標識就是push call 的標識,我們可以用這個call來定位分析我們的進入虛擬機的位置。

00401068這個地址是我們的printf的系統的那邊的函數:

那么主要的流程就是:
00449668 call 0x46af7e ;進入虛擬機0043FC17 call 0x46af7e ;scanf后進入虛擬機00467D9E call 0x47608d ;mystrcmp后進入虛擬機
還會有個比較有意思的事情就是:
正常我們的cmp函數入口的位置是0x401100,但是我們f9他永遠都不運行,因為入口的位置也是虛擬化中的一部分。

我們也可以通過ebp來找我們想要找的進入虛擬機的位置,假設我們現在再scanf之后的位置看一下ebp:

scanf的進入:

進入虛擬機:

進入虛擬機:


再出虛擬機:

滿足了上面流程圖的特點。
黑盒測試
我們首先主要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 #include #include typedef void (*CALL)(); void p1(){ printf("in p1");} void p2(){ printf("in p2");}#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 #include #include typedef void (*CALL)(); void p1(){ printf("in p1");} void p2(){ printf("in p2");} #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

側信道攻擊
我們可以把我們的程序的流程指令的個數都打印出來,我們如果遇見像分支一類的情況下,我們就可以發現指令的個數會有明顯的一個改變的形式,其實這個東西也不是很重要,因為如果我們作為正向的開發肯定原則不會這么去寫代碼,一定會加上一些加密或者是其他的原則讓這種方式不會成功。
但是還是要看一下測信道攻擊是什么情況:

如果第一個不是正確的數值路徑就會變少:

所以可以通過這種方式來側信道攻擊從而達到一個確定最后的結果是什么的情況,但是這種問題很容易被加密的算法等等所干掉,讓這種辦法不可以進行,因為計算機的運算速度不滿足(只是說一下可以有這個方式)。
污點分析
被污染的數據,在代碼的執行指定的時間點上面,因為輸入的數據的不同而可能產生不同取值的數據。
簡要的來說,因為我們的輸入去導致了一個數值的修改,我們的這個數值就叫做被污染,那么這個污染的數值如果對另一個數值進行寫的操作,那么另一個數值也就被污染了,如果這個數值被其他數值寫操作了,那么這個數值就解除污染,這個有點像游戲分析中的數據追蹤,去獲取誰訪問了該數值,然后追蹤找到最后的基址的方式是差不多的。
污染傳播的一般原則:
在一個指令中,如果有至少一個讀位置是污染的,就將所有的寫位置污染。
在一條指令中,如果所有的讀位置都是非污染的,那么就將所有的寫位置去污染。
但是比如這些原則來說針對于匯編的一些指令,有些是不滿足這個規則的,比如說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 }}