Hiew不用介绍了,不过Hiew也有缺点:不支持在跳转指令里输入VA,不知道用API函数名来调用,还有WIN2K下运行时屏幕缓冲区不匹配....
这次我们要做的就是改造Hiew,为它增加一些新功能!
最终的目标是:
1,增加va rva前缀(在相对跳转指令里可以输入VA,并修正跨段区跳转不正常的问题)
2,增加api前缀(允许用API的名称来调用API)
3,增加sinvoke,cinvoke指令(自动push参数和平栈)
4,增加asc指令(在Hiew里插入字符串)
5,增加粘贴代码的功能
6,增加标签的支持
7,增加代码段加解密的接口
8,修补WIN2K下的屏幕缓冲区问题
看起来好像要做很多工作,其实大部分功能都是在联系在一起的,实际的修改并不复杂
废话不多说了,目标程序:HIEW 6.81的Hiew32.exe(http://www.pediy.com/tools/Editors/HIEW/Hiewv6.81.rar )
开ProcDump或者CASPR或者ASprStripper将Hiew32.exe脱壳(附件中有一份脱壳后的Hiew32.exe)
先说下我的思路,首先8是最简单的,调用SetConsoleScreenBufferSize就可以设置缓冲区大小
而要实现123467我的想法是找到一个"拦截点",对指令做宏替换
想象一下,Hiew32在汇编时肯定建立了一个输入循环,不断获取键盘输入,当输入回车时将输入的字符串提交给汇编引擎来汇编
所以只要在Hiew提交字符串时拦截,让它跳到我们的代码里,这样就可以通过分析修改该指令来增加新功能
至于5,最初的想法是去写控制台的输入缓冲区,后来发现这样行不通
(估计控制台的粘贴功能就是去写输入缓冲区,所以Hiew里不能粘贴)
最后是用模拟按键的方法来实现自动输入,当然这里还要找到一个粘贴热键,所以还要找到获取输入的拦截点
最终处理的代码很多,我把它们放到DLL里,并修补Hiew,使它跳转到dll里
首先要做的就是找到2个拦截点:1个拦截键盘输入(为新增功能做热键),1个拦截汇编引擎(为了增加我们想要的伪指令)
具体这么调试:
开flyOD,加载脱壳后的Hiew32.exe,随便打开一个exe,切换到代码模式,F3,F2
现在看到Assembler的汇编窗口,切换到OD,F12中断,发现停在WaitForSingleObject:
00424244 . FF15 70204300 call dword ptr ds:[<&KERNEL32.Set>; \SetConsoleMode
0042424A . A1 C0414400 mov eax,dword ptr ds:[4441C0]
0042424F . 8B35 6C204300 mov esi,dword ptr ds:[<&KERNEL32.>
00424255 . 6A FF push -1 ; /Timeout = INFINITE
00424257 . 50 push eax ; |hObject => 00000008
00424258 . FFD6 call esi ; \WaitForSingleObject
0042425A . A1 C0414400 mov eax,dword ptr ds:[4441C0] //停在这里
0042425F . 8B3D 68204300 mov edi,dword ptr ds:[<&KERNEL32.>
00424265 . 8D4C24 18 lea ecx,dword ptr ss:[esp+18]
00424269 . BD 01000000 mov ebp,1
0042426E . 51 push ecx ; /pnRead
0042426F . 8D5424 24 lea edx,dword ptr ss:[esp+24] ; |
00424273 . 55 push ebp ; |nRecords => 1
00424274 . 52 push edx ; |Buffer
00424275 . 50 push eax ; |hConsole => 00000008
00424276 . FFD7 call edi ; \PeekConsoleInputA
00424278 . 66:396C24 20 cmp word ptr ss:[esp+20],bp
0042427D . 8B1D 64204300 mov ebx,dword ptr ds:[<&KERNEL32.>
这里显然是在等待输入的信息,然后调用PeekConsoleInputA来获得输入
不过感觉Hiew不会在自己的程序里调用这些底层的控制台API,它更应该调用gets或者scanf,这些库函数里再调用PeekConsoleInputA
(当然我们可以把PeekConsoleInputA当作一个拦截点,不过想想那个令人望而生畏的Record Buffer...我还是愿意继续去找更上层的拦截点)
现在运行到PeekConsoleInputA,然后按下F8不放,过了一段时间后中断在这里:
0042446B |> \B8 381E4400 |mov eax,UNPARKED.00441E38
00424470 |> 50 |push eax
00424471 |. E8 6A000000 |call UNPARKED.004244E0
00424476 |. 83C4 04 |add esp,4
00424479 |> E8 22000000 |call UNPARKED.004244A0 //停在这里
0042447E |. 50 |push eax
0042447F |. 56 |push esi
00424480 |. E8 8B030000 |call UNPARKED.00424810
00424485 |. 83C4 08 |add esp,8
00424488 |. 50 |push eax
这么做的原因是:假设WaitForSingleObject,PeekConsoleInputA是在库函数里,那么当我们输入点东西时,它就应该返回到更上层的代码中,因为在Hiew里肯定会有一个获取输入的循环,所以一直按下F8就应该会停在更高层代码的获取输入的CALL里,比如,现在中断在00424479,那么UNPARKED.004244A0里应该就会有WaitForSingleObject+PeekConsoleInputA来获得输入
(这里也许用IDA来分析会更好,可能这里就是个getc之类的库函数,可惜网吧里没有IDA,只好来笨办法了)
注意,上面一直是按F8的,当004244A0返回时OD就会中断下来,现在切换到Hiew,看看004244A0返回什么
只要动下鼠标就发现004244A0返回0了(eax=0)(因为WaitForSingleObject是发生鼠标键盘消息都会返回的!)
按下F8不放,再次来到004244A0,这回这样操作:把鼠标移开,按下Alt+Tab,切换到Hiew
(注意一定要先放Tab再放开Alt,因为不这么做可能让Hiew接受到按下Tab的消息)
现在004244A0没有返回,它在等待输入,随便按一个键,比如1,这时OD中断,eax=31!
显然004244A0就是获得键盘输入的函数,返回非0表示有键盘输入
测试一下会发现,不仅在汇编窗口里004244A0能中断下来,在所有状态下,只要在Hiew按了键盘004244A0都会中断,而且返回值很简单!
所以这里就是第一个拦截点,以后拦截它来处理粘贴热键
现在在Assembler中输入1212121212,在0042447E设中断,切换回Hiew,按回车
在0042447E停下,eax=0D,返回回车了,现在按F8跟踪,离开当前的CALL后来到这里:
004158A2 |. E8 49EB0000 call unparked.004243F0
004158A7 |. 8BD8 mov ebx,eax //返回到这里
004158A9 |. 25 FFFF0000 and eax,0FFFF
004158AE |. 83C4 04 add esp,4
004158B1 |. 83F8 0D cmp eax,0D //检查回车
004158B4 |. 0F8F 33020000 jg unparked.00415AED
004158BA |. 0F84 0C020000 je unparked.00415ACC //回车时跳转
004158C0 |. 83E8 08 sub eax,8 注意004158B1的cmp eax,0D 显然这里检查字符是否是回车,现在看下栈区:
0012FD18 00000000
0012FD1C 00000000
0012FD20 0012FDD9 ASCII "121212121212"
0012FD24 00000000
0012FD28 FFFFFFFF
这个更外层的CALL里保存了指向我们输入的字符串的指针!直觉告诉我马上就要找到拦截点了!
je unparked.00415ACC跳转到这里:
00415ACC |> \8B4424 40 mov eax,dword ptr ss:[esp+40] ; Case D of switch 004158B1
00415AD0 |. 55 push ebp
00415AD1 |. 50 push eax
00415AD2 |. E8 89160000 call unparked.00417160
00415AD7 |. 8B4424 18 mov eax,dword ptr ss:[esp+18]
注意00415AD0的push ebp,这里ebp->"1212121212",在这里设个端点,取消原来的所有断点
发现当回车时就会中断,所以这里就是一个拦截点,不过这个函数可能是个通用的获取输入的库函数
我们这样检验:切换到一般模式,按F5(Goto),输入一串字符,回车,发现中断了!
所以这里不是我们的目标,我们要找到的是专门提交汇编指令的地方
重新F3,F2跳出汇编窗口,输入继续1212121212,回车,在00415ACC中断下来
现在单步观察,并经常切换回Hiew,看那个"Illegal instruction"的红窗口什么时候跳出来
因为那个红窗口跳出时表示汇编出错了,感觉上它"应该"在调用汇编引擎失败后跳出来!
并且它会接受一个按键的消息来返回,所以我们会很容易发现他
两次retn后来到这里:
00410241 |. 8B4424 20 |mov eax,dword ptr ss:[esp+20]
00410245 |. 8B4C24 18 |mov ecx,dword ptr ss:[esp+18]
00410249 |. 50 |push eax
0041024A |. 56 |push esi
0041024B |. 51 |push ecx
0041024C |. 55 |push ebp
0041024D |. E8 CE000000 |call UNPACKED.00410320
00410252 |. 8BD8 |mov ebx,eax
00410254 |. 83C4 10 |add esp,10
注意在0041024D|call UNPACKED.00410320 这里看下当前栈区:
0067FBE4 0067FC51 ASCII "121212121212"
0067FBE8 0067FDC4
0067FBEC 00007FFF
0067FBF0 0002A010
而此时Hiew里显示的数据是这样:
---------------------------------------------------------------------------
test.exe FWO PE 0002A010 a32 <Editor> 263168橙iew 6.81 (c)SEN
?0002A010: 55 push ebp
?0002A011: 8BEC mov ebp,esp
?0002A013: 6AFF push 0FF
?0002A015: 6870214300 push 000432170 ;" C!p"
?0002A01A: 68C8E34200 push 00042E3C8 ;" B闳"
?0002A01F: 64A100000000 mov eax,fs:[00000000]
---------------------------------------------------------------------------
很明显,最后一个参数0002A010指向的就是当前地址!
现在按一下F8,发现Hiew进入运行状态了!call UNPACKED.00410320这个子函数没有立即返回!
切换到Hiew,看到这个东西:
--------------------------------------------------
赏屯屯屯屯屯屯屯屯?Hiew 屯屯屯屯屯屯屯屯屯?
? Illegal instruction ?
韧屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯屯图
--------------------------------------------------
现在想想call UNPACKED.00410320干了什么?
它传递了"1212121212"这个错误的指令,传递了当前地址,然后跳出汇编出错的窗口!
很显然,这里就是在调用汇编引擎!我们再来验证一下:
在0041024D|call UNPACKED.00410320设中断,并取消原来所有的断点
输入"push eax",这时中断在0041024D,栈区:
0067FBE4 0067FC51 ASCII "push eax"
0067FBE8 0067FDC4
0067FBEC 00007FFF
0067FBF0 0002A010
在转存里跟随0067FDC4,然后F8一下,发现:
0067FDC4 50 00 00 00 P...
第一个字节变成50,而50正是push eax的机器码!
call UNPACKED.00410320返回了1,有正好是一个字节!
这样我们更有理由认为UNPACKED.00410320的功能就是汇编指令
进一步尝试会发现汇编正确时UNPACKED.00410320返回汇编后的机器码的字节数,汇编的机器码放在它的第3个参数指定的缓冲区里
而错误时它弹出Illegal instruction并返回0或负数
呵呵,这正是我们希望看到的,第二个拦截点也找到了!
跟进UNPACKED.00410320看一下我们还能发现一些有用的东西,比如,肯定会发现产生"Illegal instruction"的CALL
汇编窗口里输入1212121212,回车,中断在UNPACKED.00410320,F7跟进去,来到这里:
0041035A |. 8D4424 22 |lea eax,dword ptr ss:[esp+22]
0041035E |. 52 |push edx ; /Arg4
0041035F |. 8D4C24 14 |lea ecx,dword ptr ss:[esp+14] ; |
00410363 |. 50 |push eax ; |Arg3
00410364 |. 51 |push ecx ; |Arg2
00410365 |. 57 |push edi ; |Arg1
00410366 |. C703 00000000 |mov dword ptr ds:[ebx],0 ; |
0041036C |. E8 BF000000 |call UNPACKED.00410430 ; \UNPACKED.00410430
00410371 |. 8BF0 |mov esi,eax
call UNPACKED.00410430时的栈区:
0067FB88 0067FC51 |Arg1 = 0067FC51 ASCII "1212121212"
0067FB8C 0067FBA8 |Arg2 = 0067FBA8
0067FB90 0067FBBA |Arg3 = 0067FBBA
0067FB94 0067FBCC \Arg4 = 0067FBCC
其实这里才是真正的汇编引擎,F8发现它返回了0,而"Illegal instruction"没有跳出来,在往下F8,很快来到这里:
004103C2 |. 8B02 |mov eax,dword ptr ds:[edx]
004103C4 |. 50 |push eax
004103C5 |. E8 F6450100 |call UNPACKED.004249C0
004103CA |. 83C4 04 |add esp,4
call UNPACKED.004249C0时的栈区:
0067FB94 004362D4 ASCII "Illegal instruction"
F8过call UNPACKED.004249C0,发现红窗口跳出来,而call UNPACKED.004249C0没有返回
我们可以把UNPACKED.004249C0当作MessageBox来输出我们的消息,从add esp,4看出它是C调用方式
好了,现在拦截点全部找到,开始修补Hiew,并开始写我们的DLL
先来看看WIN2000/XP的下缓冲区问题
在Hiew6.81的FAQ(q_and_a.txt)里有这么段话:
----------------------------------------------------------------------------------
Q02. I was over to Win2k/WinXP and was very surprised that I can't run the HIEW.
A02. (Ruslan Kantorovych)
So what is a problem, the HIEW works up 120x50 mode, but the setting for
every DOS mode window on Win2k/WinXP is 80x300.
I solved this problem by setting the default mode to 80x25 for every
window.
How you can do it:
Open the DOS mode window;
Click on the picture of the DOS mode window in the left up angle of
the window;
Choose "Default" (it's important);
After that choose "Properties";
Open "Layout" dialog;
In the "Screen Buffer Size" area put the number 25 into "Height"
control (by default it's 300);
Press OK button it's all :)
(大意是Hiew工作在缓冲区为80x25的屏幕下,而WIN2K/XP的默认缓冲区是80x300,所以我们要手工设置缓冲区大小)
----------------------------------------------------------------------------------
我不知道Hiew的开发者为什么要写这么一段话而不是调用SetConsoleScreenBufferSize把缓冲区设置为80x25
现在我们这样修补它:
查看Hiew的导入表,发现有GetConsoleScreenBufferInfo这个函数,Hiew很可能就是调用它来获取缓冲区信息
用OD加载Hiew,在GetConsoleScreenBufferInfo设中断,F9运行,发现这里:
00401AA7 |> \8B15 BC414400 mov edx,dword ptr ds:[4441BC]
00401AAD |. 68 A0414400 push UNPACKED.004441A0 ; /pInfo = UNPACKED.004441A0
00401AB2 |. 52 push edx ; |hConsole => NULL
00401AB3 |. FF15 08204300 call dword ptr ds:[<&KERNEL32.GetC>; \GetConsoleScreenBufferInfo
00401AB9 |. A1 A0414400 mov eax,dword ptr ds:[4441A0]
00401ABE |. 33C9 xor ecx,ecx
用VC建一个DLL工程,添加这个函数并导出它(完整的源代码在附件中)
BOOL WINAPI FixBufferSize(HANDLE hConsoleOutput,PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo)
{
hConsoleOut=hConsoleOutput;
COORD size;
size.X=80;
size.Y=25;
SetConsoleScreenBufferSize(hConsoleOutput,size);
return GetConsoleScreenBufferInfo(hConsoleOutput,lpConsoleScreenBufferInfo);
}
这个函数调用SetConsoleScreenBufferSize把缓冲区设为80x25,然后返回Hiew希望调用的GetConsoleScreenBufferInfo
复制Hiew并改名为myHiew.exe,用LordPE打开myHiew,为它添加导入函数FixBufferSize
用另一份Hiew打开myHiew.exe,换到代码模式,F5,输入".00401AB3"
修改这个CALL,把GetConsoleScreenBufferInfo的thunk换成FixBufferSize的thunk,运行myHiew,发现正确运行了
(实际上我发现即使缓冲区是80x300,只要欺骗Hiew是80x25,那么它仍然能正确运行)
现在拦截汇编引擎和输入热键,刚才已经找到0041024D是汇编指令的拦截点,00424479是获取输入的拦截点
用Hiew打开myHiew,跳到0041024D,往下面看,发现0410422有一片空白,可以在这里加入我们的代码
在0041024D加入一个短跳转jmp .000410422(这里要输入文件偏移),因为短跳转和CALL API的机器码都是5个字节,所以不用修补其他指令
在我们的DLL里加入一个函数int myAsmEng(char*srcins,char*buf,int unknow,int srcaddr)并导出
在0410422加入下面的代码
00410422: FF152C604400 call myAsmEng ;myHiew.dll
00410428: E925FEFFFF jmp .000410252 ----- (4)
这里调用DLL中的函数,然后跳回0041024D的下一条指令
call myHiew1.00410320下面跟着一条add esp,10,这说明它是C的调用方式,所以myAsmEng也要定义为C方式
对输入拦截也用相同的做法
00424479 |> E8 22000000 |call myHiew_f.004244A0 ------------------>获取输入
0042447E |. 50 |push eax
0042447F |. 56 |push esi
00424480 |. E8 8B030000 |call myHiew_f.00424810
00424485 |. 83C4 08 |add esp,8
注意call myHiew_f.004244A0里获取输入,所以要在它后面做拦截,这里拦截00424480|call myHiew_f.00424810
00431320刚好有一片空白,就在000431320加入拦截代码,修改00424480为
00424480: E99BCE0000 jmp .000431320 ----- (2)
在000431320加入下面的代码
00431320: 50 push eax
00431321: FF1562604400 call GetUserInput ;myHiew.dll
00431327: 89442404 mov [esp][04],eax
0043132B: E8E034FFFF call .000424810 ----- (1)
00431330: E95031FFFF jmp .000424485 ----- (2)
然后在DLL里定义int WINAPI GetUserInput(int key)
上面的代码调用DLL后CALL原函数再跳回去
到这里拦截已经设置好了,下面只要修改myAsmEng,GetUserInput就可以了
这里我得方法是myAsmEng里处理新加入的指令,然后再定义一个ConvertIns里处理跳转指令和API调用
因为跳转指令很多,无法在myAsmEng里把没个指令都拿来重新分析,所以我准备用这样的格式来处理跳转:
va 单目运算符 虚拟地址
而用这样的格式来处理API调用:
api 单目运算符 dllname.apiname 或者 api 单目运算符 apiname
这里的va和api相当于指令的前缀,va告诉myHiew后面的地址是虚拟地址而且这个指令是相对跳转指令
例如,假设401000的文件偏移是400,那么va je 401000在myAsmEng里被ConvertIns转换为je 400,然后送给原来的汇编引擎去汇编
当然这样做显得很别扭,不过我们就不必去分析每个相对跳转的单目运算符,直接把该运算符复制就行
(暂时没想出什么好办法)
下面是部分实现代码,不过我没有学过编译原理也没学过STL(甚至C也没有正式学过),写出来的代码非常烂,哪位兄弟有兴趣帮我改一下
(完整源代码在附件中)
#define ifins(a) if(!strcmp(ins,a))
int myAsmEng(char*srcins,char*buf,int unknow,int srcaddr)
这里的参数上面分析拦截点时已经提到了
srcins就是要汇编的指令,buf是存放汇编后的机器码的地址,unknow我也不知道,好像每次都是7FFF,srcaddr是当前指令的文件偏移
int (*AsmEng)(char*srcins,char*buf,int unknow,int srcaddr);
AsmEng=(int (__cdecl *)(char *,char *,int,int))0x0410320;
int MyRet=-1;
char srcins_bak[100];
strcpy(srcins_bak,srcins);
char*path=*(char**)(0x441D44);
if(!path)goto invalid;
if(!ReadPE(path))
{
ClosePE();
goto invalid;
}
ConvertIns(srcins,srcaddr);
strcpy(srcins_bak,srcins);
上面这部分定义了原来的汇编引擎AsmEng,然后调用ReadPE来读取当前正在编辑的文件的PE头,然后调用ConvertIns处理指令前缀
接下来处理字符串,分别提取操作符和操作数,然后是若干个ifins宏构成的处理伪指令的代码,比如这个asc指令:
ifins("asc")
{
int lenoperand=strlen(operand);
if(!lenoperand)goto invalid;
strcpy(buf,operand);
MyRet=strlen(operand)+1;
goto end;
}
这个伪指令简单的复制字符串,然后返回(不调用正版的汇编引擎)
最后关闭PE文件:
invalid: //如果分析不正确就直接调用汇编引擎
ClosePE();
return AsmEng(srcins_bak,buf,unknow,srcaddr);
end:
ClosePE();
return MyRet;
}
下面看看读取PE头的代码
#define getdata(buf,pos,len) {if(fseek(fh,pos,0))return 0;if(fread(buf,1,len,fh)!=len)return 0;}
#define getvalue(var,pos) getdata(&var,pos,sizeof(var))
#define getstring(buf,pos) {if(fseek(fh,pos,0))return 0;if(!fgets(buf,sizeof(buf),fh))return 0;}
//定义一些宏来读取文件
static IMAGE_DOS_HEADER DOSHeader;
static IMAGE_NT_HEADERS NTHeader;
static IMAGE_SECTION_HEADER*pSecHeader;//段区头数组的指针,在ReadPE里动态分配,在ClosePE里释放
static FILE*fh;
BOOL ReadPE(char*path)
{
pSecHeader=NULL;
fh=NULL;
fh=fopen(path,"rb");
if(!fh)return 0;
getvalue(DOSHeader,0);//读DOS头
getvalue(NTHeader,DOSHeader.e_lfanew);//读PE头
int nSec=NTHeader.FileHeader.NumberOfSections;
pSecHeader=new IMAGE_SECTION_HEADER[nSec];
getdata(pSecHeader,sizeof(IMAGE_NT_HEADERS)+DOSHeader.e_lfanew,sizeof(IMAGE_SECTION_HEADER)*nSec);//读取所有的段头
return 1;
}
BOOL ClosePE()//这里关闭文件
{
if(pSecHeader)delete []pSecHeader;
if(fh)fclose(fh);
return 1;
}
注意myAsmEng打开文件时路径是char*path=*(char**)(0x441D44);
这个0x441D44保存了指向当前正在编辑的文件的路径字符串的指针,这个拦截CreateFileA很容易就能分析出来
现在看ConvertIns
BOOL ConvertIns(char*srcins,int srcaddr)
srcins:当前指令,srcaddr当前指令的文件偏移
最开始处理指令,分别提取 前缀,操作符,操作数
char tmp[100];
char sign[20];//前缀(这个单词不会拼,所以换成了sign,标记)
char ins[20];//操作符(也是不会拼,用指令代替)
char operand[50];//操作数(不知拼对了没有...)
接下来是很多个if(!strcmp(sign,"?????"))这样开头的代码,用来处理前缀 现在实现va和api前缀
我发现Hiew里跨段区的相对跳转即使输入也不正确
分析一下发现Hiew仅仅只是把两个段区的文件偏移相减,没有考虑段区在内存里和文件的对齐方式不同
为了这个va能一定正确,我们必须再犯一个错误,让这个错误和Hiew的错误相互抵消,得出正确的结果!
假设相对偏移是k,那么
正确的计算方法是:vs-vd=k1
Hiew的计算方法是:fs-fd=k2
其中vs是原指令虚拟地址,vd是目标指令虚拟地址,fs是原指令文件偏移,fd是目标指令文件偏移
令k1=k2,计算fd(这是Hiew要求我们输入的)
fd=fs-vs+vd=vd-(vs-fs)
假设vps是原指令所在段区的段区内存基址,fps是原指令所在段区的段区文件基址,ImageBase是PE内存映射基址
那么vs-fs=ImageBase+vps-fps
所以fd=vd-(ImageBase+vps-fps)
这样计算出来的错误的fd提交给Hiew算出的相对值反而是正确的
下面这个VAtoFixedOfs实现计算fd的功能:
DWORD VAtoFixedOfs(DWORD VA,DWORD srcaddr)
{
DWORD addr=VA;
IMAGE_SECTION_HEADER*pSrcSecHeader=GetSecHeaderByFO(srcaddr);
if(!pSrcSecHeader)return 0xFFFFFFFF;
if(!GetSecHeaderByVA(addr))return 0xFFFFFFFF;
addr=addr-(pSrcSecHeader->VirtualAddress+NTHeader.OptionalHeader.ImageBase-pSrcSecHeader->PointerToRawData);
return addr;
}
其中用到一些获得段区头的函数:
IMAGE_SECTION_HEADER*GetSecHeaderByRVA(DWORD RVA)
{
int nSec=NTHeader.FileHeader.NumberOfSections;
for(int i=0;i<nSec;i++)
{
if((RVA>=pSecHeader[i].VirtualAddress)&&(RVA<(pSecHeader[i].VirtualAddress+pSecHeader[i].SizeOfRawData)))
{
return &pSecHeader[i];
}
}
return 0;
}
IMAGE_SECTION_HEADER*GetSecHeaderByFO(DWORD FO) //File Offset
{
int nSec=NTHeader.FileHeader.NumberOfSections;
for(int i=0;i<nSec;i++)
{
if((FO>=pSecHeader[i].PointerToRawData)&&(FO<(pSecHeader[i].PointerToRawData+pSecHeader[i].SizeOfRawData)))
{
return &pSecHeader[i];
}
}
return 0;
} IMAGE_SECTION_HEADER*GetSecHeaderByVA(DWORD VA)
{
DWORD RVA=VA-NTHeader.OptionalHeader.ImageBase;
return GetSecHeaderByRVA(RVA);
}
段区的大小都是由SizeOfRawData确定,SizeOfRawData是文件中区段对齐后的,这样在段区间隙加入的代码也能被识别
(不过传说Borland的链接器会把VirtualSize和SizeOfRawData颠倒了,这可能会导致一些问题)
然后在ConvertIns里实现va前缀
if(!strcmp(sign,"va"))
{
int j=strlen(operand);
for(i=0;i<j;i++)
{
if(!isxdigit(operand[i]))goto invalid;
}
DWORD addr;
sscanf(operand,"%x",&addr);
addr=VAtoFixedOfs(addr,srcaddr);
if(addr=0xFFFFFFFF)goto invalid;
sprintf(tmp,"%s 0%x",ins,addr);
goto valid;
}
api前缀实现起来相对简单,这里有两种格式就是api call dllname.apiname或者api call apiname,分别处理
if(!strcmp(sign,"api"))
{
char*ptoken=strchr(operand,'.');
char dll[50];
char api[50];
DWORD thunk;
if(ptoken) //带dllname
{
if(strchr(ptoken+1,'.'))goto invalid;
*ptoken=0;
strcpy(dll,operand);
strcpy(api,ptoken+1);
thunk=GetThunkByName(dll,api);
if(thunk)
{
sprintf(tmp,"%s d,[0%x]",ins,thunk+NTHeader.OptionalHeader.ImageBase);
goto valid;
}
}
else //不带dllname
{
thunk=GetThunkByName(NULL,operand);
if(thunk) //找到api的thunk就转换,否则不处理
{
sprintf(tmp,"%s d,[0%x]",ins,thunk+NTHeader.OptionalHeader.ImageBase);
goto valid;
}
}
goto invalid;
}
这样处理后api call apiname将被转换为call d,[api的thunk值]
其中GetThunkByName用来获取API的THUNK值
DWORD GetThunkByName(char*dll,char*api)
{
strlwr(api);
IMAGE_IMPORT_DESCRIPTOR IID;
int nIID=0;
getvalue(IID,RVAtoOfs(NTHeader.OptionalHeader.DataDirectory[1].VirtualAddress));
while(IID.Name)
{
char dllname[50];
getstring(dllname,RVAtoOfs(IID.Name));
strlwr(dllname);
if(dll)
{
if(strstr(dllname,".dll"))
*strstr(dllname,".dll")=0;
strlwr(dll);
if(strcmp(dll,dllname))
{
nIID++;
getvalue(IID,RVAtoOfs(NTHeader.OptionalHeader.DataDirectory[1].VirtualAddress)+sizeof(IMAGE_IMPORT_DESCRIPTOR)*nIID);
continue;
}
}
DWORD thunk;
int nThunk=0;
getvalue(thunk,RVAtoOfs(IID.FirstThunk+nThunk*4));
if(RVAtoOfs(thunk+2)==0xFFFFFFFF)
getvalue(thunk,RVAtoOfs(IID.OriginalFirstThunk+nThunk*4));
while(thunk)
{
char apiname[50];
if(!(thunk&IMAGE_ORDINAL_FLAG32))
{
getstring(apiname,RVAtoOfs(thunk+2));
strlwr(apiname);
if(!strcmp(api,apiname))return IID.FirstThunk+nThunk*4;
}
nThunk++;
getvalue(thunk,RVAtoOfs(IID.FirstThunk+nThunk*4));
if(RVAtoOfs(thunk+2)==0xFFFFFFFF)
getvalue(thunk,RVAtoOfs(IID.OriginalFirstThunk+nThunk*4));
}
nIID++;
getvalue(IID,RVAtoOfs(NTHeader.OptionalHeader.DataDirectory[1].VirtualAddress)+sizeof(IMAGE_IMPORT_DESCRIPTOR)*nIID);
}
return 0;
}
穷举每个IID来找API
现在来实现标签,为了方便,利用map模板类来建立标签和VA的映射关系
using namespace std;
map <string,DWORD> LableMap;
定义LableMap映射表,键值string是标签名称,DWORD是标签所表示位置的虚拟地址
在ConverIns里实现ofslb,valb前缀,ofslb表示使用标签的相对值,valb表示使用虚拟地址
if(!strcmp(sign,"ofslb"))
{
if(!strlen(operand))goto invalid;
strlwr(operand);
map<string,DWORD>::iterator mi=LableMap.find(operand);
if(mi==LableMap.end())goto invalid;
DWORD addr=mi->second;
addr=VAtoFixedOfs(addr,srcaddr);
if(addr==0xFFFFFFFF)goto invalid;
sprintf(tmp,"%s 0%x",ins,addr);
goto valid;
}
if(!strcmp(sign,"valb"))
{
if(!strlen(operand))goto invalid;
strlwr(operand);
map<string,DWORD>::iterator mi=LableMap.find(operand);
if(mi==LableMap.end())goto invalid;
sprintf(tmp,"%s 0%x",ins,mi->second);
goto valid;
}
在myAsmEng里实现deflb伪指令,用来把当前地址定义成一个标签
ifins("deflb")
{
char msg[100];
if(!strlen(operand))goto invalid;
if(!GetSecHeaderByFO(srcaddr))
{
strcpy(msg," CurrentAddress is invalid! Lable defined faild! ");
MsgOut(msg);
goto invalid;
}
if(strchr(operand,' '))*strchr(operand,' ')=0;
strlwr(operand);
LableMap[string(operand)]=srcaddr-GetSecHeaderByFO(srcaddr)->PointerToRawData+GetSecHeaderByFO(srcaddr)->VirtualAddress+NTHeader.OptionalHeader.ImageBase;
wsprintf(msg," Lable(\"%s\")=0x%X defined successfully!!! ",operand,LableMap[operand]);
MsgOut(msg);
MyRet=-1;
goto end;
}
注意上面的MsgOut,这个其实就是最开始分析拦截点找到的那个产生红窗口的函数
void (*MsgOut)(char*msg)=(void(*)(char*))0x4249C0;
接下来专门处理mov指令,为它加入标签支持
ifins("mov")
{
......
}
最后为查看标签做一个界面,原理非常简单,但实现起来相当麻烦
先在GetUserInput里添加查看的热键
int WINAPI GetUserInput(int key)
{
switch(key)
{
case 0x0FF85: //F11
LableOut();
key=0;
break;
case 0x0FF86: //F12=PasteClipData
InputClipData();
key=0;
break;
}
return key;
}
F11调用LableOut查看标签,F12负责处理粘贴
LableOut内部建立一个输入循环处理对标签的操作,F1删除当前标签,F2删除所有标签,F3复制标签的VA
InputClipData()处理粘贴,上面已经提到了写输入缓冲区的方法是无效的,这里利用keybd_event来模拟键盘输入,就想按键精灵那样
#define assert(a) if(!a){MsgOut("myHiew.dll Error:" #a " return FALSE");return 0;}
BOOL InputClipData()
{
assert(OpenClipboard(NULL));
HANDLE hMem=GetClipboardData(CF_TEXT);
assert(hMem);
char*ptr=(char*)GlobalLock(hMem);
assert(ptr);
strcpy(cliptext,ptr);
assert(GlobalUnlock(hMem));
assert(CloseClipboard());
CreateThread(NULL,0,KeyInput,cliptext,NULL,NULL);
return 1;
}
这里定义一个断言assert防止出错时非法操作,然后新建一个线程来模拟输入
CreateThread是必须的,如果不建一个新线程会导致当前拦截的输入函数无法返回,实际上和写输入缓冲区一样不能正确的输入
char cliptext[1000];
DWORD WINAPI KeyInput(void*string)
{
char*ptr=(char*)string;
SHORT VkKey;
while(*ptr)
{
if(*ptr=='\n')
{
ptr++;
continue;
}
Sleep(50);
VkKey=VkKeyScan(*ptr);
if(HIBYTE(VkKey)&1)
{
keybd_event(VK_SHIFT,0,0,0);
keybd_event(LOBYTE(VkKey),0,0,0);
keybd_event(VK_SHIFT,0,KEYEVENTF_KEYUP,0);
}
else
{
keybd_event(LOBYTE(VkKey),0,0,0);
}
ptr++;
}
return 1;
}
这里模拟按键,VkKeyScan将字符转换为虚拟键盘码(在低8位),而它的最高位表示输入该字符是否需要按Shift
这个方法只能模拟输入英文字母和数字,看起来非常别扭
不过有个好处,就是可以成段的复制输入代码(因为keybd_event会把回车也模拟出来)
if(*ptr=='\n')的作用是回车换行("\r\n")时只输入回车,这样可以避免跳过两行 下面看cinvoke的处理(sinvoke和这个相似),原理很简单,逐个读取参数,然后push参数,call函数,平栈
指令的格式是cinvoke proc/p1/p2/p3.....
proc可以是子函数的VA地址也可以是API或者标签,p1,p2,p3是参数,可以是一般的参数或者标签
ifins("cinvoke")
{
char paramtbl[10][20];
int nParam=0;
char*param=strtok(operand,"/");
while(param&&(nParam<10))
{
strcpy(paramtbl[nParam],param);
param=strtok(NULL,"/");
nParam++;
}
nParam--;
if(nParam==0)goto invalid;
if(!GetCallAddr(paramtbl[0],srcaddr))goto invalid;
int npush=nParam;
int ndata=0;
char curins[100];
int AsmEngRet;
map<string,DWORD>::iterator mi;
while(nParam)
{
strlwr(paramtbl[nParam]);
switch(paramtbl[nParam][0])
{
case '@':
mi=LableMap.find(¶mtbl[nParam][1]);
if(mi==LableMap.end())
sprintf(curins,"push %s",paramtbl[nParam]);
else
sprintf(curins,"push 0%x",mi->second);
break;
case '*':
mi=LableMap.find(¶mtbl[nParam][1]);
if(mi==LableMap.end())
sprintf(curins,"push %s",paramtbl[nParam]);
else
sprintf(curins,"push d,[0%x]",mi->second);
break;
default:
sprintf(curins,"push %s",paramtbl[nParam]);
break;
}
AsmEngRet=AsmEng(curins,buf+ndata,0x7FFF,srcaddr+ndata);
if(AsmEngRet>0)
ndata+=AsmEngRet;
else
return AsmEngRet;
nParam--;
}
sprintf(curins,"call %s",paramtbl[0]);
AsmEngRet=AsmEng(curins,buf+ndata,0x7FFF,srcaddr+ndata);
if(AsmEngRet>0)
ndata+=AsmEngRet;
else
{
MyRet=AsmEngRet;
goto end;
}
if(npush>1)
{
sprintf(curins,"add esp,0%x",npush*4);
AsmEngRet=AsmEng(curins,buf+ndata,0x7FFF,srcaddr+ndata);
if(AsmEngRet>0)
ndata+=AsmEngRet;
else
{
MyRet=AsmEngRet;
goto end;
}
}
return ndata;
}
参数允许是标签,以@开头的标签表示取标签的VA,而以*开头表示取值(会被转换为"d,[VA]"),每个参数都以/分隔(没有想到什么好方法...)
GetCallAddr这里是转换出CALL的地址,可以是VA API函数名 标签
到这里大部分的处理都完成了,最后只剩下文件的加解密,Hiew里只能进行简单的文件接密
不过我们已经拦截了汇编引擎,所以可以在myAsmEng里添加C语言写的加解密代码
这里以xor这种简单的加密作为例子,在myAsmEng里加入这样一段代码
ifins("codexor") //格式:codexor len,key
{
if(!strlen(operand))goto invalid;
if(!strchr(operand,','))goto invalid;
char*ptok=strchr(operand,',');
*ptok=0;
DWORD len=0x200;
DWORD key=0;
sscanf(operand,"%X",&len);
sscanf(ptok+1,"%X",&key);
if((len>0x100)||(!len))goto invalid;
len=len/4;
if(fseek(fh,srcaddr,0))goto invalid;
DWORD*databuf=new DWORD[len];
if(fread(databuf,4,len,fh)!=len)
{
delete []databuf;
goto invalid;
}
int i;
for(i=0;i<len;i++)
*((DWORD*)buf+i)=databuf[i] ^ key;
MyRet=len*4;
goto end;
}
这段代码将当前的代码和key异或,不过可能只能处理0x100左右的字节(好象Hiew一此只会打开这么多) 上面完成了所有的功能,但实际上myHiew仍然有问题,如果输入"asc 12345678901234567890"会发现出现非法操作,出错地址是0x33343536
这个地址正好是3456的ASC码,感觉上是因为myAsmEng返回的数据过多导致栈区溢出了!
重新用OD加载unparked.exe,在0041024D这里(调用汇编引擎的地方)设中断,F3,F2,输入1212121212\r
中断下来,看一下栈区
0012FD6C 0012FDD9 ASCII "12121212121212121212"
0012FD70 0012FF4C
0012FD74 00007FFF
0012FD78 0002A010
存放机器指令的buffer=0012FF4C,显然这个地址也在堆栈里!查看0012FF4C
0012FF4C 00000000
0012FF50 0000FF3D
0012FF54 00000000
0012FF58 0040B286 返回到 unpacked.0040B286 来自 unpacked.004243F0
0012FF5C 0040B2F0 返回到 unpacked.0040B2F0 来自 unpacked.0040F370
原来Hiew只在堆栈里分配了12个字节来储存机器码!所以asc指令插入的字符串长度超过12时就会溢出!
我们必须修改机器码缓冲区,否则invoke这些伪指令都可能出错
注意0012FF4C附近
0012FF0C 00430000 unpacked.00430000
0012FF10 0040F6E5 返回到 unpacked.0040F6E5 来自 unpacked.004100A0
0012FF14 0012FF4C -------->机器码缓冲区,作为参数传递给了另一个函数
0012FF18 00007FFF
0012FF1C 0002A010
0012FF20 00930770
0012FF24 00000000
0012FF28 00000009
0012FF2C 00000000
0012FF30 00000000
0012FF34 00000000
0012FF38 00000003
0012FF3C 00000000
0012FF40 FFFFFFFF
0012FF44 00434B08 unpacked.00434B08
0012FF48 0000000E
0012FF4C 00000000
0012FF50 0000FF3D
0012FF54 00000000
显然这个CALL里为机器码分配了12字节的缓冲区,然后作为参数调用了另一个函数
注意0040F6E5的代码:
0040F6CC |. 8D4C24 24 lea ecx,dword ptr ss:[esp+24] //ecx指向机器码缓冲区 拦截点1
0040F6D0 |. 50 push eax
0040F6D1 |. 8B049D A0364400 mov eax,dword ptr ds:[ebx*4+4436A> ----------->拦截后跳回地址
0040F6D8 |. 56 push esi
0040F6D9 |. 50 push eax
0040F6DA |. 68 FF7F0000 push 7FFF
0040F6DF |. 51 push ecx //缓冲区
0040F6E0 |. E8 BB090000 call unpacked.004100A0
0040F6E5 |. 8BF0 mov esi,eax
0040F6E7 |. 83C4 18 add esp,18
修改0040F6CC这里就可以改变缓冲区了,在DLL里加入下面的函数并导出
char CodeBuffer[0x100];
void*GetCodeBuffer()
{
return CodeBuffer;
}
然后修改0040F6CC,让它跳到一个空白区,然后调用GetCodeBuffer来获得缓冲区,让ecx指向缓冲区,然后跳回去
这里跳转指令会占用5个字节,所以要先push eax,然后跳回0040F6D1
当然后面肯定还会有读取机器码的地方,在0040F6E5设中断,输入一个正确的指令,回车,中断下来,F8跟踪
0040F725 |. 8B3D 5C3D4400 mov edi,dword ptr ds:[443D5C]
0040F72B |. 8B2C9D A0364400 mov ebp,dword ptr ds:[ebx*4+4436A0]
0040F732 |. 8BCE mov ecx,esi
0040F734 |. 8B82 32010000 mov eax,dword ptr ds:[edx+132]
0040F73A |. 8D7424 20 lea esi,dword ptr ss:[esp+20] //机器码暂存 拦截点2
0040F73E |. 2BF8 sub edi,eax
0040F740 |. 8BC1 mov eax,ecx ------------>拦截后跳转点
0040F742 |. 03FD add edi,ebp
0040F744 |. C1E9 02 shr ecx,2
0040F747 |. F3:A5 rep movs dword ptr es:[edi],dword ptr ds:[esi] ---->保存机器码
0040F749 |. 8BC8 mov ecx,eax
0040F74B |. 83E1 03 and ecx,3
很快发现0040F73A处esi也是指向机器码缓冲区,修改方法和上面都一样
这样处理以后myHiew就可以正常工作了
我修改的结果:
(今天运气不好,附件怎么也传不上来,明天再传好了)
---------------------------------------------------------------
修改说明
将myHiew.exe和myHiew.dll复制到原来Hiew6.81的目录,运行myHiew.exe
1,前缀:
格式是 "前缀 单目操作符 操作数"
va 表示操作数是虚拟地址 要将它转换为相对跳转值
rva 表示操作数是相对虚拟地址 要将它转换为相对跳转值
api 表示操作数是导入函数名 要将它转换为该函数的thunk值
ofslb 表示操作数是标签 要将它转换为标签的相对跳转值
valb 表示操作数是标签 要将它转换为标签的虚拟地址
2,伪指令:
codexor len,key
从当前代码开始每4个字节和key异或,处理的代码长度为len
(len和key都要用十六进制表示)
asc string
在当前代码处插入字符串string
cinvoke proc/p1/p2.......
调用proc,参数是p1,p2...调用方式为cdecl
proc:可以是API函数名 标签 虚拟地址(API函数名可以是dllname.apiname 或者 只是apiname)
p1,p2,p3:参数,可以是 "@标签" "*标签" 或者常数,寄存器
"@标签"表示压入标签的VA地址
"*标签"表示压入标签处的DWORD值(转换为"push d,[标签虚拟地址]")
sinvoke proc/p1/p2.......
和cinvoke类似,调用方式是stdcall
deflb string
定义当前位置为标签,标签名=string
3,修改的指令:
mov des,src
des,src:可以是 "@标签" "*标签",意义和cinvoke一样
4,查看标签:
在任何时候按下F11打开查看标签的窗口
该窗口中可以用上下键选择标签,F1删除当前标签,F2删除所有标签,F3复制标签的虚拟地址到剪贴版,ESC返回
5,复制剪贴版的文本:
按F12模拟键入剪贴版的文本,只能是英文字母或数字符号
6,其他修改:
WIN2000/XP下会自动调整屏幕缓冲区为80x25 指令大多数都要求当前地址必须在一个有效的Section内,所有指令都不区分大小写
如果不喜欢上面的格式,可以在myHiew_dll里修改,重新编译DLL就行
-------------------------------------------------------------
最后举个例子结束我的满篇废话
用myHiew加密notepad.exe
(附件中有修改的结果,修改前的程序是notepad_src.exe,修改后的程序是notepad_fixed.exe)
目标程序:Win2000 SP4 上的notepad.exe
工具:资源编辑器(这里用VC),LordPE,WinHex,myHiew
先用LordPE打开notepad.exe,发现3个段.text .data .rsrc
.rsrc后面还有一些数据,是调试信息
打开目录将"调试"这一项的"RVA"和"大小"都设为00000000,然后开WinHex将.rsrc后的调试信息Remove掉
开VC给notepad.exe添加资源:
Dialog ID:255 X:200,Y:150(这样刚好在屏幕中间)
Edit ID:256
Button ID:257 Caption:OK
Button ID:258 Caption:Cancel
开LordPE增加导入函数
user32.dll GetDlgItemInt
kernel32.dll ExitProcess
把.text段的属性改为可写
增加一个新的段区.test(VOffset:11000,VSize:1000,ROffset:C800,RSize:1000)
开WinHex,在文件末尾先将文件补齐到C800,再添加4096(0x1000)的空白数据
用myHiew打开notepad.exe,切换为代码模式,按F8,F5跳到程序入口点
复制下面的代码,然后F12进去:
-----------------------------
deflb start
codexor 50,499602D2
-----------------------------
F9(Updata),F5(Goto)输入".1011000" (.表示跳转到虚拟地址)
复制下面的代码,然后F12进去:
---------------------------------
deflb cmdok
sinvoke GetDlgItemInt/[ebp+8]/100/0/0
mov ebx,@start
mov ecx,14
deflb decrypt
xor [ebx],eax
add ebx,4
ofslb loop decrypt
sinvoke EndDialog/[ebp+8]/1
or eax,1
pop ebp
ret 10
deflb exit
sinvoke EndDialog/[ebp+8]/0
or eax,1
pop ebp
ret 10
deflb command
mov eax,[ebp+10]
and eax,0FFFF
cmp eax,101
ofslb je cmdok
cmp eax,102
ofslb je exit
xor eax,eax
pop ebp
ret 10
deflb DlgProc
push ebp
mov ebp,esp
mov eax,[ebp+0C]
cmp eax,0111
ofslb je command
cmp eax,010
ofslb je exit
xor eax,eax
pop ebp
ret 10
deflb Entry
sinvoke GetModuleHandleA/0
sinvoke DialogBoxParamW/eax/0FF/0/@DlgProc/0
cmp eax,1
ofslb je start
sinvoke ExitProcess/0
---------------------------------
F9(Update),F11,选择entry这个标签,然后F3复制标签地址,用LordPE打开,修改入口点为复制的值减去1000000(基地址)
运行,出现对话框,输入1234567890,OK,正确运行记事本,输入其他数字则出现非法操作
解释一下:
上面那段代码前面的空格不能省略,因为deflb会弹出一个红窗口,空格刚好可以把它关掉,另外Hiew里翻页时也必须多键入一个字符
这些代码要从下往上看,因为必须先定义标签再使用,所以要写出一段能直接F12的代码就只好倒着写了
--------------
start:
codexor 50,499602D2 //把0x50个字节和0x499602D2求xor (0x499602D2=1234567890,就是密码)
--------------
cmdok: //解密
sinvoke GetDlgItemInt/[ebp+8]/100/0/0
mov ebx,@start
mov ecx,14 // 50/4=14
decrypt:
xor [ebx],eax
add ebx,4
ofslb loop decrypt
sinvoke EndDialog/[ebp+8]/1
or eax,1
pop ebp
ret 10
exit:
sinvoke EndDialog/[ebp+8]/0
or eax,1
pop ebp
ret 10
command:
mov eax,[ebp+10]
and eax,0FFFF
cmp eax,101 //处理OK按钮
ofslb je cmdok
cmp eax,102 //处理cancel按钮
ofslb je exit
xor eax,eax
pop ebp
ret 10
DlgProc: //对话框过程
push ebp
mov ebp,esp
mov eax,[ebp+0C]
cmp eax,0111 //WM_COMMAND
ofslb je command
cmp eax,010 //WM_CLOSE
ofslb je exit
xor eax,eax
pop ebp
ret 10
Entry: //新入口点
sinvoke GetModuleHandleA/0
sinvoke DialogBoxParamW/eax/0FF/0/@DlgProc/0 //打开对话框
cmp eax,1
ofslb je start //解密后跳到原来的入口点
sinvoke ExitProcess/0 //Cancel就结束进程
这里只要按了OK,不管密码对不对都解密,而错误的密码会导致解密后的代码出现非法操作
*****************************************************************************
终于写玩了
欢迎继续修改这个代码,修改后也请给我一份(Email:DQuixote@qq.com)
最后感谢你耐心地看到这里
--------------------------------------------------------
更新了附件,请管理员帮忙删一下旧的附件,地址是http://bbs.pediy.com/upload/file/2004/11/myHiew.rar_529.rar http://bbs.pediy.com/upload/file/2004/11/myHiew.rar_614.rar
bbs.pediy.com/upload/file/2004/11/myHiew.rar_175.rar
bbs.pediy.com/upload/file/2004/11/myHiew.rar_306.rar
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)