UPX脱壳全过程分析
本帖最后由 Blackk 于 2014-11-29 22:06 编辑小菜也只能拿这样简单的壳来练手啦 ^ ^。
简单说下个人认为的UPX壳解码时的流程,然后我就按流程一点一点写吧。
1.先初始化
2.进行代码还原
3.进行CALL修复
4.进行函数表还原
5.节表初始化
6.解码完毕飞向程序入口
第一步:初始化,这个好像没什么好解释的呃,看代码。
0049B4D0 > $60 pushad ;//保存现场 //寄存器入栈 //ESP=12FFC4
0049B4D1 .BE 00204600 mov esi, 00462000 ;//ESI指向UPX1区段 //ESI=462000 //ESP=12FFA4
0049B4D6 .8DBE 00F0F9FF lea edi, dword ptr ;//指向要还原的首地址UPX0区段 //EDI=401000
0049B4DC .57 push edi ;//首地址入栈
0049B4DD .83CD FF or ebp, 0xFFFFFFFF ;//初始化EBP //EBP用来判断第二次循环还原方式
0049B4E0 .EB 10 jmp short 0049B4F2 ;//初始化完毕
第二步:代码还原。
接下来UPX壳进行代码还原,还原的方式我个人分为两种。
一种是UPX壳无法压缩的代码,UPX老老实实的从UPX1区段将代码还原到UPX0区段;
另一种就是UPX0中的重复指令,UPX壳根据UPX1中的KEY值找到UPX0中重复指令的位置,再进行的代码还原。具体过程如下:
0049B4E8 > /8A06 mov al, byte ptr ;//依次取UPX1区段的数据进行还原
0049B4EA . |46 inc esi ;//指向下一个要还原的数据 //ESI=ESI+1
0049B4EB . |8807 mov byte ptr , al ;//还原数据
0049B4ED . |47 inc edi ;//EDI=EDI+1
0049B4EE > |01DB add ebx, ebx ;//用EBX做判断,是否继续还原
0049B4F0 . |75 07 jnz short 0049B4F9
0049B4F2 > |8B1E mov ebx, dword ptr ;//取循环变量KEY //存放在EBX=0xFF36DEEF
0049B4F4 . |83EE FC sub esi, -0x4 ;//ESI=ESI+4
0049B4F7 . |11DB adc ebx, ebx ;//用EBX做判断,是否开始进行还原代码操作
0049B4F9 >^\72 ED jb short 0049B4E8
0049B4FB .B8 01000000 mov eax, 0x1 ;//EAX=1
0049B500 >01DB add ebx, ebx
0049B502 .75 07 jnz short 0049B50B
0049B504 .8B1E mov ebx, dword ptr
0049B506 .83EE FC sub esi, -0x4
0049B509 .11DB adc ebx, ebx
0049B50B >11C0 adc eax, eax ;//使用EAX判断还原方式
0049B50D .01DB add ebx, ebx
0049B50F .73 0B jnb short 0049B51C
0049B511 .75 28 jnz short 0049B53B
0049B513 .8B1E mov ebx, dword ptr
0049B515 .83EE FC sub esi, -0x4
0049B518 .11DB adc ebx, ebx
0049B51A .72 1F jb short 0049B53B
0049B51C >48 dec eax
0049B51D .01DB add ebx, ebx
0049B51F .75 07 jnz short 0049B528
0049B521 .8B1E mov ebx, dword ptr
0049B523 .83EE FC sub esi, -0x4
0049B526 .11DB adc ebx, ebx
0049B528 >11C0 adc eax, eax
0049B52A .^ EB D4 jmp short 0049B500
0049B52C >01DB add ebx, ebx
0049B52E .75 07 jnz short 0049B537
0049B530 .8B1E mov ebx, dword ptr
0049B532 .83EE FC sub esi, -0x4
0049B535 .11DB adc ebx, ebx
0049B537 >11C9 adc ecx, ecx
0049B539 .EB 52 jmp short 0049B58D
0049B53B >31C9 xor ecx, ecx ;//循环变量清零 //ECX=0
0049B53D .83E8 03 sub eax, 0x3 ;//EAX=0xFFFFFFFF为单字节代码还原 //EAX!=0xFFFFFFFF为多字节还原代码
0049B540 .72 11 jb short 0049B553
0049B542 .C1E0 08 shl eax, 0x8 ;//EAX左移8位 //EAX清零
0049B545 .8A06 mov al, byte ptr ;//取多字节循环还原地址KEY
0049B547 .46 inc esi ;//ESI+1
0049B548 .83F0 FF xor eax, 0xFFFFFFFF ;//EAX和0xFFFFFFFF异或 //循环出口
0049B54B .74 75 je short 0049B5C2
0049B54D .D1F8 sar eax, 1 ;//EAX右移一位
0049B54F .89C5 mov ebp, eax ;//EBP=EAX //EBP的值为:~(KEY/2)
0049B551 .EB 0B jmp short 0049B55E
0049B553 >01DB add ebx, ebx
0049B555 .75 07 jnz short 0049B55E
0049B557 .8B1E mov ebx, dword ptr
0049B559 .83EE FC sub esi, -0x4
0049B55C .11DB adc ebx, ebx
0049B55E >^ 72 CC jb short 0049B52C
0049B560 .41 inc ecx
0049B561 .01DB add ebx, ebx
0049B563 .75 07 jnz short 0049B56C
0049B565 .8B1E mov ebx, dword ptr
0049B567 .83EE FC sub esi, -0x4
0049B56A .11DB adc ebx, ebx
0049B56C >^ 72 BE jb short 0049B52C
0049B56E >01DB add ebx, ebx
0049B570 .75 07 jnz short 0049B579
0049B572 .8B1E mov ebx, dword ptr
0049B574 .83EE FC sub esi, -0x4
0049B577 .11DB adc ebx, ebx
0049B579 >11C9 adc ecx, ecx
0049B57B .01DB add ebx, ebx
0049B57D .^ 73 EF jnb short 0049B56E
0049B57F .75 09 jnz short 0049B58A
0049B581 .8B1E mov ebx, dword ptr
0049B583 .83EE FC sub esi, -0x4
0049B586 .11DB adc ebx, ebx
0049B588 .^ 73 E4 jnb short 0049B56E
0049B58A >83C1 02 add ecx, 0x2 ;//循环变量递增
0049B58D >81FD 00FBFFFF cmp ebp, -0x500
0049B593 .83D1 02 adc ecx, 0x2 ;//循环变量递增
0049B596 .8D142F lea edx, dword ptr ;//以EDI作为代码还原的基地址,用EBP指向已还原的代码,将相同代码进行还原
0049B599 .83FD FC cmp ebp, -0x4 ;//再次判断循环还原的方式
0049B59C .76 0E jbe short 0049B5AC ;//如果还原处和解码处相距距离小于4个字节则进行单字节还原
0049B59E >8A02 mov al, byte ptr ;//相同字节单字节还原循环
0049B5A0 .42 inc edx ;//EDX+1
0049B5A1 .8807 mov byte ptr , al ;//循环还原
0049B5A3 .47 inc edi ;//EDI+1
0049B5A4 .49 dec ecx ;//判断循环是否结束 //ECX
0049B5A5 .^ 75 F7 jnz short 0049B59E
0049B5A7 .^ E9 42FFFFFF jmp 0049B4EE
0049B5AC >8B02 mov eax, dword ptr ;//多字节循环还原
0049B5AE .83C2 04 add edx, 0x4
0049B5B1 .8907 mov dword ptr , eax
0049B5B3 .83C7 04 add edi, 0x4
0049B5B6 .83E9 04 sub ecx, 0x4 ;//判断循环是否结束 //ECX
0049B5B9 .^ 77 F1 ja short 0049B5AC
0049B5BB .01CF add edi, ecx
0049B5BD .^ E9 2CFFFFFF jmp 0049B4EE
0049B5C2 >5E pop esi ;//还原完毕 //首地址出栈入ESI
整个过程中,ECX一般作为循环变量,EBX是绝对的王者,它掌控整个还原的节奏。
关于EBX是怎样计算出来的,现在技术不够逆不出来。
所以现在分析的都只不过是皮毛,UPX壳压缩的核心还是没能出来。
第三步:CALL修复
0049B5C3 .89F7 mov edi, esi
0049B5C5 .B9 73250000 mov ecx, 0x2573 ;//循环变量 //ECX=0x2573
0049B5CA >8A07 mov al, byte ptr
0049B5CC .47 inc edi
0049B5CD .2C E8 sub al, 0xE8
0049B5CF >3C 01 cmp al, 0x1 ;//判断是否为0xE8
0049B5D1 .^ 77 F7 ja short 0049B5CA
0049B5D3 .803F 13 cmp byte ptr , 0x13 ;//判断0xE8后面是否为0x13 //判断是否为CALL
0049B5D6 .^ 75 F2 jnz short 0049B5CA
0049B5D8 .8B07 mov eax, dword ptr
0049B5DA .8A5F 04 mov bl, byte ptr ;//使用BL判断下一个指令是否是CALL
0049B5DD .66:C1E8 08 shr ax, 0x8 ;//AX右移8位 //AL清零,AL=AH
0049B5E1 .C1C0 10 rol eax, 0x10 ;//EAX左移循环
0049B5E4 .86C4 xchg ah, al ;//AH和AL交换
0049B5E6 .29F8 sub eax, edi
0049B5E8 .80EB E8 sub bl, 0xE8
0049B5EB .01F0 add eax, esi
0049B5ED .8907 mov dword ptr , eax ;//CALL地址还原
0049B5EF .83C7 05 add edi, 0x5
0049B5F2 .88D8 mov al, bl
0049B5F4 .^ E2 D9 loopd short 0049B5CF ;//循环还原CALL地址
关于CALL修复。在第二步代码还原时已经把CALL的特征码还原了,这里只是对CALL的地址做进一步的修正。
它的算法我还是有点不明白。我看到在还原代码时,CALL后面的四个字节标志了CALL地址KEY,通过上面的CALL算法将KEY还原成正确地址。请问这个将KEY还原的代码用高级语言应该怎样表示呢?
第四步:函数表还原,没什么好解释的,看代码。
0049B5F6 8DBE 00800900 lea edi, dword ptr ; //获取函数表还原基地址
0049B5FC 8B07 mov eax, dword ptr ; //获取函数表还原偏移
0049B5FE 09C0 or eax, eax
0049B600 74 45 je short 0049B647 ; //函数表还原完了进入下一个模块的跳转
0049B602 8B5F 04 mov ebx, dword ptr ; //获取函数存放位置偏移
0049B605 8D8430 B0C80900 lea eax, dword ptr ; //获取函数所在DLL名称
0049B60C 01F3 add ebx, esi
0049B60E 50 push eax
0049B60F 83C7 08 add edi, 0x8
0049B612 FF96 B4C90900 call dword ptr ; //载入DLL到内存
0049B618 95 xchg eax, ebp
0049B619 8A07 mov al, byte ptr
0049B61B 47 inc edi
0049B61C 08C0 or al, al
0049B61E^ 74 DC je short 0049B5FC ; //还原DLL后进入下一个DLL的还原跳转
0049B620 89F9 mov ecx, edi
0049B622 79 07 jns short 0049B62B
0049B624 0FB707 movzx eax, word ptr
0049B627 47 inc edi
0049B628 50 push eax
0049B629 47 inc edi
0049B62A B9 5748F2AE mov ecx, 0xAEF24857
0049B62F 55 push ebp
0049B630 FF96 B8C90900 call dword ptr ; //获取函数所在地址
0049B636 09C0 or eax, eax
0049B638 74 07 je short 0049B641
0049B63A 8903 mov dword ptr , eax ; //进行函数表地址还原
0049B63C 83C3 04 add ebx, 0x4
0049B63F^ EB D8 jmp short 0049B619 ; //循环还原函数表
第五步:节表初始化,看代码。
0049B641 FF96 C8C90900 call dword ptr
0049B647 8BAE BCC90900 mov ebp, dword ptr ; //指向VirtualProtect函数地址
0049B64D 8DBE 00F0FFFF lea edi, dword ptr ; //指向文件头位置
0049B653 BB 00100000 mov ebx, 0x1000 ; //文件头大小
0049B658 50 push eax
0049B659 54 push esp
0049B65A 6A 04 push 0x4 ; //PAGE_EXECUTE_READWRITE
0049B65C 53 push ebx
0049B65D 57 push edi
0049B65E FFD5 call ebp ; 使用VirtualProtect把文件头设置为PAGE_EXECUTE_READWRITE,获得文件头的写权限
0049B660 8D87 0F020000 lea eax, dword ptr
0049B666 8020 7F and byte ptr , 0x7F
0049B669 8060 28 7F and byte ptr , 0x7F ; //完成节表的初始化
0049B66D 58 pop eax
0049B66E 50 push eax
0049B66F 54 push esp
0049B670 50 push eax
0049B671 53 push ebx
0049B672 57 push edi
0049B673 FFD5 call ebp ; //用VirtualProtect恢复文件头属性
最后:在0x0049B684飞向程序入口。
0049B675 58 pop eax
0049B676 61 popad
0049B677 8D4424 80 lea eax, dword ptr
0049B67B 6A 00 push 0x0
0049B67D 39C4 cmp esp, eax
0049B67F^ 75 FA jnz short 0049B67B
0049B681 83EC 80 sub esp, -0x80
0049B684- E9 C89AFAFF jmp 00445151
至此UPX壳解码分析完毕了。第一次逆向壳,写下文章留个记录。
以上文章只是个人见解,难免有误,还希望各路友人帮忙指正。
UPX分析都这么多好麻烦 看不懂
不错,大牛作品,必属精品 不错,大牛作品,必属精品 谢谢你的分析,我来看看 本帖最后由 Blackk 于 2014-10-2 14:29 编辑
按照国际惯列,沙发自己坐。试炼程序:
大神求VMP脱壳全分析 坐等人肉还原 来学习一下的说 不错哦~ 大牛的作品,小白暂时看不透!