SecuROM 脱壳分析,纯属陋文,为抛砖引玉而作
此文参考各位达人们的文章及其研究经验总结完善而成。
序言
SecuROM 以前一直想弄下,但是一直没机会,最近偶获WE2007版本,且使用OD可方便调试,故兴趣大增并分析之。
要开工,得先找到OEP,OD加载,突然想到以前论坛达人们的偷懒一法,直接中断 GetVersionExA 测试下。
中断后可从堆栈得返回地址,所以写了个脚本,查找在 004xxxxx 这个范围返回的CALL地址
故查到第一个接近目标的地址
0022FEA8 0041A483 /CALL 到 GetVersionExA 来自 0041A47D
0022FEAC 0022FEB0 \pVersionInformation = 0022FEB0
跳到返回地址往上看,爽,这个不就像VC的OEP么。
0041A45D > 6A 60 PUSH 60
0041A45F 68 68F3B700 PUSH 00B7F368
0041A464 E8 73340000 CALL 0041D8DC
0041A469 BF 94000000 MOV EDI,94
0041A46E 8BC7 MOV EAX,EDI
0041A470 E8 BB340000 CALL 0041D930
0041A475 8965 E8 MOV DWORD PTR SS:[EBP-18],ESP
0041A478 8BF4 MOV ESI,ESP
0041A47A 893E MOV DWORD PTR DS:[ESI],EDI
0041A47C 56 PUSH ESI
0041A47D FF15 04E2B700 CALL DWORD PTR DS:[<&KERNEL32.GetVersion>; kernel32.GetVersionExA
所以,这里可以DUMP一个比较干净的EXE,但是不是现在,因为还有很多工作以后要接触到。
到这个时候,看了下OD里面的段,还真有点多,不知道申请那么多内存来做什么,好我们接着看下他要用这些内存作什么。
跟到下面这个地址,大概就有点点明白了
0041DB96 - FF25 14D04A04 JMP DWORD PTR DS:[44AD014] ; 04517DD0
0041DB9C CC INT3
看,他通过这个JMP到内存段去执行代码,看来这里的代码是被SecuROM 抽掉了。我查了下JMP DWORD PTR DS:[]这样的有几十个,
任意看了1,2个,都是跳到一个公共地方先加密,然后再执行解密后的正确代码。这些代码存放的内存空间都是单独的,
看来都是VirtualAlloc出来的呢,记录下,等下专门跟下VirtualAlloc。
仔细看了下执行解密的代码:
040D531C FF70 09 PUSH DWORD PTR DS:[EAX+9]
040D531F FF33 PUSH DWORD PTR DS:[EBX]
040D5321 E8 FDFCFFFF CALL 040D5023
........
03D800CF - FF25 E8A62404 JMP DWORD PTR DS:[424A6E8] ; kernel32.GetSystemInfo
仔细看了下,他取系统信息作为解密KEY,对抽取代码进行还原,执行一次后代码就赤裸裸的存在了,就不细管这里的解密,后面我再讲.
好,重新装载启动,中断VirtualAlloc看下
恩,发现被壳调用了,呵呵,高兴,去看看他申请来做什么,我走啊走啊,晕倒,竟然分配的空间用来做SOFTICE,OD检查的,因为我的能跑起来,
呵呵,不看了略过
恩,继续看下面的,嘿嘿,发现申请空间的地址,恩,奇怪,他竟然是制定ADDR申请的,我观察了,发现一个小细节
03E2B557 0F31 RDTSC
03E2B559 A8 01 TEST AL,1
03E2B55B 74 05 JE SHORT we2007.03E2B562
.......
03E2B712 - FF25 55794704 JMP DWORD PTR DS:[<&KERNEL32.VirtualAlloc>] ; kernel32.VirtualAlloc
03E2B718 6C INS BYTE PTR ES:[EDI],DX ; I/O 命令
03E2B719 97 XCHG EAX,EDI
03E2B71A - E9 B605AD15 JMP 198FBCD5
03E2B71F EF OUT DX,EAX ; I/O 命令
03E2B720 09C0 OR EAX,EAX
03E2B722 ^ 0F84 2FFEFFFF JE we2007.03E2B557
呵呵,他竟然用RDTSC取得一个值,简单的换算下,作为申请空间的固定地址,目的明显是为了得到随机的空间地址.
下面简单说下过程,下面我说个方法可以略过这里,为了凑点字数,加上代码解密的过程
将本地数据COPY到分配的空间
03E1EAE7 F3:A4 REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
进行第一次解密码
03D7E63F 3008 XOR BYTE PTR DS:[EAX],CL
03D7E641 8A00 MOV AL,BYTE PTR DS:[EAX]
进行第二次解密码,得到初始数据
04242993 8A49 01 MOV CL,BYTE PTR DS:[ECX+1]
04242996 47 INC EDI
04242997 880F MOV BYTE PTR DS:[EDI],CL
04242999 3B5D D4 CMP EBX,DWORD PTR SS:[EBP-2C]
这里,就将代码还原了,后来才知道,这个不是被抽掉的代码,是VM的代码还原到内存中去.
这里VirtualAlloc的值可以被修改成自己的空间里面,这样就方便将零散的VM代码集中
如果不这样的话,估计有几十个这样的零散空间,够你手动还原的了.
下面是修改VM代码到自己空间的OD脚本:
data:
var virtualaddr
var virtuallen
var fixmemaddr
var fixmemlen
var incmemlen
start:
Pause
mov incmemlen,0
mov fixmemlen,7000000
alloc fixmemlen //申请自己的存空间
mov fixmemaddr, $RESULT
bp 7C809AB9 //VirtualAllocEx
code:
eob code //bpx
esto
cmp eip, 7C809AB9
je fixmem
jmp code
end:
MSG "脚本暂停"
Pause
ret
fixmem:
cmp eax,0
je code
mov virtualaddr,[esp+8]
mov virtuallen, [esp+c]
mov eax,fixmemaddr
add incmemlen, virtuallen
cmp incmemlen, fixmemlen
jae err
mov eax, fixmemaddr
add fixmemaddr, virtuallen
jmp code
err:
MSG "内存越界"
Pause
ret
这样,可以将VM申请空间都统一到一个内存空间里面,比一个一个DUMP下来再附加在EXE上方便多了,这样只附加一个即可
之前发现被抽掉的代码空间我们还没找到,中断VirtualAlloc看下
发现处理这段代码的位置
将本地数据COPY到分配的空间
03FB39B3 F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]
03FB39B5 FF2495 CC3AFB03 JMP DWORD PTR DS:[EDX*4+3FB3ACC]
解密出原始的抽掉的代码
040B1BEE 3008 XOR BYTE PTR DS:[EAX],CL
040B1BF0 B8 D338FBFF MOV EAX,FFFB38D3
......
040B1A59 3008 XOR BYTE PTR DS:[EAX],CL
040B1A5B B8 A987FDFF MOV EAX,FFFD87A9
对还原出来的数据,根据内存地址的不同,而修正CALL和PUSH
040B202E 0118 ADD DWORD PTR DS:[EAX],EBX
040B2030 8D6424 FC LEA ESP,DWORD PTR SS:[ESP-4]
......
040B2278 2918 SUB DWORD PTR DS:[EAX],EBX
040B227A 8D6424 FC LEA ESP,DWORD PTR SS:[ESP-4]
对修正后的数据,进行SYSTEMINFO和GetCurrentProcessId加密,为了保证代码唯一性
040B2893 300C03 XOR BYTE PTR DS:[EBX+EAX],CL
040B2896 83C7 01 ADD EDI,1
040B2899 83E7 07 AND EDI,7
这样被抽掉的代码处理地方就发现,这里有点问题还没搞明白,这里的VirtualAlloc不能替换成自己申请的空间,要报错
所以暂时只能硬DUMP下来附加
好,OEP和代码处理的地方基本就是这些,现在开始进行PE修复了
先是用脚本将VM的空间都指向自己的空间里面,然后中断到OEP处,这个时候DUMP出EXE,由于被抽掉的代码我没办法结合到一个空间
所以就只能全段DUMP下来,然后将所有的段拼接起来,然后修正OEP.只不过EXE文件有点大,哈哈,丢人啊.
代码段就到这里OK了,开始修复IAT,我看了下,这个EXE的IAT竟然是干净的,不用修复,爽,只要将IAT从指向壳的IAT修正到原来EXE的
IAT地址即可,这个很简单,这样IAT就修正完毕.
恩,将修正了段和IAT的用OD加载,晕倒,错误还满多的,还看不到画面就直接下课了,只能继续检查错误.
因为被抽掉的代码要用GetCurrentProcessId解密,因为GetCurrentProcessId每次启动不一样,所以的修复.不然被抽掉的还原不正确的,所以要报错阿
修正由于GetCurrentThreadId出错的地方,直接将址修改成你DUMP EXE时候那个那个ID即可
040D5049 FF15 B1794704 CALL DWORD PTR DS:[<&KERNEL32.GetCurrent>; kernel32.GetCurrentProcessId
恩,继续用OD装载修正后的EXE,OK,这次跑的稍微远点了,还是在某处报错了,跟进去看下
04024A8A 50 PUSH EAX
04024A8B FF15 78253D04 CALL DWORD PTR DS:[43D2578] ; we2007.04029F80
04024A91 35 809F0204 XOR EAX,4029F80
04024A96 3305 78253D04 XOR EAX,DWORD PTR DS:[43D2578] ; we2007.04029F80
04024A9C E9 8A000000 JMP we2007.04024B2B
04024AA1 50 PUSH EAX
04024AA2 FF15 7C253D04 CALL DWORD PTR DS:[43D257C] ; we2007.04029F90
04024AA8 35 909F0204 XOR EAX,4029F90
04024AAD 3305 7C253D04 XOR EAX,DWORD PTR DS:[43D257C] ; we2007.04029F90
04024AB3 EB 76 JMP SHORT we2007.04024B2B
04024AB5 50 PUSH EAX
04024AB6 FF15 80253D04 CALL DWORD PTR DS:[43D2580] ; we2007.04029FA0
04024ABC 35 A09F0204 XOR EAX,4029FA0
04024AC1 3305 80253D04 XOR EAX,DWORD PTR DS:[43D2580] ; we2007.04029FA0
04024AC7 EB 62 JMP SHORT we2007.04024B2B
04024AC9 50 PUSH EAX
04024ACA FF15 84253D04 CALL DWORD PTR DS:[43D2584] ; we2007.04029FD0
04024AD0 35 D09F0204 XOR EAX,4029FD0
04024AD5 3305 84253D04 XOR EAX,DWORD PTR DS:[43D2584] ; we2007.04029FD0
04024ADB EB 4E JMP SHORT we2007.04024B2B
04024ADD 50 PUSH EAX
04024ADE FF15 88253D04 CALL DWORD PTR DS:[43D2588] ; we2007.04029FF0
04024AE4 35 F09F0204 XOR EAX,4029FF0
04024AE9 3305 88253D04 XOR EAX,DWORD PTR DS:[43D2588] ; we2007.04029FF0
04024AEF EB 3A JMP SHORT we2007.04024B2B
04024AF1 50 PUSH EAX
04024AF2 FF15 8C253D04 CALL DWORD PTR DS:[43D258C] ; we2007.0402A050
04024AF8 35 50A00204 XOR EAX,402A050
04024AFD 3305 8C253D04 XOR EAX,DWORD PTR DS:[43D258C] ; we2007.0402A050
04024B03 EB 26 JMP SHORT we2007.04024B2B
04024B05 50 PUSH EAX
04024B06 FF15 90253D04 CALL DWORD PTR DS:[43D2590] ; we2007.0402A060
04024B0C 35 60A00204 XOR EAX,402A060
04024B11 3305 90253D04 XOR EAX,DWORD PTR DS:[43D2590] ; we2007.0402A060
04024B17 EB 12 JMP SHORT we2007.04024B2B
04024B19 50 PUSH EAX
04024B1A FF15 94253D04 CALL DWORD PTR DS:[43D2594] ; we2007.0402A0D0
04024B20 35 D0A00204 XOR EAX,402A0D0
04024B25 3305 94253D04 XOR EAX,DWORD PTR DS:[43D2594] ; we2007.0402A0D0
04024B2B 3345 0C XOR EAX,DWORD PTR SS:[EBP+C]
04024B2E 5D POP EBP
04024B2F C3 RETN
这里集合了好几种检查机制,抽2个说明下
04029F80 FF15 482F2D04 CALL DWORD PTR DS:[42D2F48] ; kernel32.GetCurrentProcessId
04029F86 034424 04 ADD EAX,DWORD PTR SS:[ESP+4]
04029F8A C2 0400 RETN 4
04029F90 FF15 4C2F2D04 CALL DWORD PTR DS:[42D2F4C] ; kernel32.GetVersion (0A280105)
04029F96 2B4424 04 SUB EAX,DWORD PTR SS:[ESP+4]
这个分别进入前2个CALL后的情况
很明显第一个又用GetCurrentProcessId来对压入的值进行变换,如果GetCurrentProcessId不一样得到不一样的值,就导致运算出错
第二个也是用GetVersion参与运算,呵呵,这里就一次将几个判断都修复,直接付DUMP exe时候,进程当前环境值即可
用OD加载继续跑,同样出错,呵呵,郁闷了,继续检查,发现这个地方
0049017C 8B0D 4C9F4A04 MOV ECX,DWORD PTR DS:[44A9F4C] ; 3th_17_d.05075B5D
00490182 1B0D 69164C04 SBB ECX,DWORD PTR DS:[44C1669] ; 3th_17_d.044AECC8
看到没有,他在效验刚才被我修改的代码段,呵呵,这里处理办法很多,你可以跳开检查的段,或者直接付值.
嘿嘿,这次用OD加载没报告错误了,嘿嘿,运行起来了,爽
看到游戏画面,只是踢球的时候报了错,非法了,郁闷,继续检查.
0047E1FF 8B3D 149F4A04 MOV EDI,DWORD PTR DS:[44A9F14] ; 3th_17_d.009D1DA5
0047E205 1B3D 68014000 SBB EDI,DWORD PTR DS:[400168]
0047E20B 9D POPFD
你看他在做什么,他在检查PE头的数据,呵呵,知道他在检查什么,就方便了,我这里处理的办法就是,将他的PE头保留,然后自己在他老的PE头
再加上自己的头信息,呵呵,OK就搞定了.
这次可以正常游戏了,至少本机没出现任何其他问题.对于跨机器的VM中的修正,下次再讲下吧,累了~
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)