解决了chixiaojie在帖子“
ETU-Dasm v2.35 ALPHA 中文乱码问题!(替代 W32Dasm 的另一重量级静态反汇编工具) ”中提到的问题。感谢chixiaojie提供这个工具,同时也感谢yjd333提供了"Hacker's Disassembler 1.06"的信息。
chixiaojie曾在
UpK的帖子 “
eXeScope, XNResourceEditor & IDR 之Form中文显示补丁 ”中提到能否解决这个问题,因为当时在整理“
Restorator 2007注册的RSA算法分析及RCData中文处理补丁 ”(如果对RSA算法感兴趣,强烈建议读读这个帖子。),就下载了
英文原版 初步试用了一下,发现这个软件确实不错,便回复到时看看。
英文版改好后的效果(以打开Windows XP Pro. SP3 CHS的记事本为例):
"Hacker's Disassembler 1.06"最新的版本为"
HDasm 1.06 rev C (Jan 29 2007, 592 kb) ",用Borland C++ 2002编写。英文版的HDasm能够正确处理菜单资源里的中文字符:
但是字符串资源的处理还是有问题(对比上面修改过的ETU-Dasm):
总的说来,HDasm还是不错滴,用户多一个选择。基本上与W32Dasm(它同样不能正确处理中文的菜单参考和字符串参考)相似。个人感觉ETU-Dasm要强于HDasm和W32Dasm。比如,反汇编代码的地址常量、调用和跳转的目标地址都用不同的颜色区分,并且提供右键菜单。当鼠标移到地址上时会出现一个信息框,显示目标地址附近的内容,可使用Tab键在数据模式和代码模式间切换,这个功能显得非常的人性和智能:
ETU-DASM从2001年1月开始开发,2005年12月发布了最后这个版本,编程采用"Borland C++ 4.02"。古董级的东西,直接导致象"EMS Source Rescuer"和DelphiDecompiler等工具无法使用,因为那时它的"Form"还在标准的Dialog资源里面,没有在RCData里面。
顺便说一下,有个工具"Delphi and C++Builder Decompiler" v3.42或者v8.11,是“山寨”"EMS Source Rescuer"的,它运行时会在%TEMP%文件夹下生成一个隐藏的临时文件,实际上就是REVENGE小组Jacky对"EMS Source Rescuer"的一个破解。
下面大致说一下解决问题的大致过程,使用工具:OD和Hiew。
问题的原因 :菜单资源和字符串资源里的文本是unicode编码的,每个字符是定长的,用一个字表示。ETU-DASM在处理时,简单地按单字节字符处理,即取每个unicode字符的低字节,这在纯西文时没有问题,但在象中文等多字节字符时其效果是将两个字合并为一个字,结果必然就不可识别。所以,应该算是一个Bug。
解决方法 :在显示输出前,通过Windows的API函数WideCharToMultiByte将unicode字符转换到ANSI字符。但是ETU-DASM的输入表中缺少这个函数,我们得为它添加一个。
用代码的方法来实现基本上是LoadLibraryA和GetProcAddress的组合,用完后还需要FreeLibrary,比较繁琐。这里我们采用修改输入表的方法,让操作系统在载入ETU-DASM时为我们取得函数WideCharToMultiByte的实际入口地址。
在输入表里添加函数WideCharToMultiByte可以用一些工具,比如Stud_PE,但是它会整出一大堆不需要的东西,而且还增加一个区段。这里我们用手工来修改,也好复习一下输入表的相关概念。
在一个PE文件中,当调用另一个模块中的函数(比如USER32.DLL中的GetMessage)时,编译器产生的CALL指令通常不会直接将控制传到DLL里的这个函数(见下图),而是将CALL指令转到同是本代码区段的一条指令:"JMP DWORD PTR [XXXXXXXX]"。
JMP指令间接地用到输入表中一个DWORD变量,这个变量为操作系统中该函数的真实入口地址。这样做的好处是,操作系统在载入程序时不需要去调整每一个调用这个函数的CALL指令地址,PE loader唯一要做的事情就是将目标函数的正确地址填入这个DWORD内,CALL指令的地址不需修改。
输入表(Import Table)的一个作用就是为PE loader提供进行Thunk(形实替换)的信息。输入表的开始是多个IMAGE_IMPORT_DESCRIPTOR的列表,每个PE文件需要隐式链接的DLL对应一个IMAGE_IMPORT_DESCRIPTOR,没有字段用来表示IMAGE_IMPORT_DESCRIPTOR的个数,而是将最后一个IMAGE_IMPORT_DESCRIPTOR的所有字段置NULL(0)来表示列表的结束。
IMAGE_IMPORT_DESCRIPTOR的结构为:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 该union的命名非常糟糕。0 表示输入描述表结束。
DWORD OriginalFirstThunk; // 含有输入名表(Import Name Table - INT - 有时又叫hint-name表)的RVA,或原始未联编IAT的RVA(PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 表示没有联编到输入DLL。
// -1 新式联编(new BIND)。
// 旧式联编(Old BIND)为输入DLL的时间戳(自1/1/1970 GMT以来的秒数)
DWORD ForwarderChain; // 适用于旧式联编,为第一个转发API的索引。无转发为-1。
DWORD Name; // 输入DLL名称ASCII字符串的RVA。
DWORD FirstThunk; // 含有输入地址表(Import Address Table - IAT)的RVA。若已联编IAT内为实际地址。
} IMAGE_IMPORT_DESCRIPTOR;
注意它的大小为0x14字节,每个IMAGE_IMPORT_DESCRIPTOR中OriginalFirstThunk和FirstThunk典型地指向两个基本上相同的IMAGE_THUNK_DATA结构组成的表,这两个表同样没有字段来表明它的大小,用一个DWORD的NULL来结束表。
IMAGE_THUNK_DATA的结构为:
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString; // 指向转发字符串的RVA。
PDWORD Function; // 输入函数的内存地址
DWORD Ordinal; // 输入API的Ordinal值(序号)
PIMAGE_IMPORT_BY_NAME AddressOfData; // 指向含输入API名的IMAGE_IMPORT_BY_NAME结构的RVA。
} u1;
} IMAGE_THUNK_DATA32;
IMAGE_IMPORT_BY_NAME结构很简单:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 为输入函数的Ordinal值。正如它自身的意思,仅仅是"Hint",不象NE文件,其值并不要求准确。
BYTE Name[?]; // 函数名字符串 + NULL(0)。
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
为什么会有两个并行的IMAGE_THUNK_DATA列表?
这里有一个联编(Binding)的问题,我们不深入讨论它,会扯得很远。简单地说,OriginalFirstThunk指向的INT是查找输入函数的唯一依据,它永远不会被修改。
在联编的PE文件中,比如使用Bind程序,或者Windows安装程序的BindImage操作,FirstThunk指向的IAT已经含有输入函数的实际地址,PE loader不需要再进行Thunk。
在未联编的情况下,FirstThunk字段指向的IMAGE_THUNK_DATA列表会被PE loader重写,loader重复每一个表项:用它找到的函数的实际地址覆盖指向IMAGE_IMPORT_BY_NAME的指针。指令JMP DWORD PTR [XXXXXXXX]中的[XXXXXXXX]部分实际上指向FirstThunk列表中的一项,每一项都是loader Thunk的结果。正因为FirstThunk列表中被loader重写的指针为每个输入函数的实际地址,所以被称为输入地址表(IAT)。
INT对执行文件的加载不是必须的,但是没有它执行文件就不能被联编。Microsoft的连接器似乎总会生成INT,而Borland早期的连接器(TLINK)一直都没有生成过INT。
这段文字比较抽象哈,我们现在用实例来理解。用Hiew打开"Windows XP Pro. SP3 CHS"的记事本NOTEPAD.EXE。
NOTEPAD.EXE是已联编的程序,在Hiew中按Enter切换到HEX模式,再F8、F10调出IMAGE_OPTIONAL_HEADER中的DataDirectory(为IMAGE_DATA_DIRECTORY结构的列表):
Name RVA Size
Export 00000000 00000000
Import 00007604 000000C8
Resource 0000B000 00007F20
Exception 00000000 00000000
Security 00000000 00000000
Fixups 00000000 00000000
Debug 00001350 0000001C
Description 00000000 00000000
MIPS GP 00000000 00000000
TLS 00000000 00000000
Load config 000018A8 00000040
Bound Import 00000250 000000D0
Import Table 00001000 00000348
Delay Import 00000000 00000000
COM Runtime 00000000 00000000
(reserved) 00000000 00000000
IMAGE_DATA_DIRECTORY的结构:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
在"Import"项按Enter来到输入表:
01007600: 90 79 00 00.FF FF FF FF.FF FF FF FF
01007610: AC 7A 00 00.C4 12 00 00
...
这是第一个IMAGE_IMPORT_DESCRIPTOR,OriginalFirstThunk=00007990,TimeDateStamp=FFFFFFFF,ForwarderChain=FFFFFFFF,Name=00007AAC,FirstThunk=000012C4:
01007990: 7A 7A 00 00.5E 7A 00 00.9E 7A 00 00.50 7A 00 00
...
01007A10: 00 00 00 00.04 00 43 6F.6D 6D 44 6C.67 45 78 74
...
01007A70: 61 63 65 54.65 78 74 57.00 00 0F 00.50 61 67 65 aceTextW Page
01007A80: 53 65 74 75.70 44 6C 67.57 00 0A 00.47 65 74 4F SetupDlgW GetO
...
01007AA0: 50 72 69 6E.74 44 6C 67.45 78 57 00.63 6F 6D 64 PrintDlgExW comd
01007AB0: 6C 67 33 32.2E 64 6C 6C.00 00 03 01.53 68 65 6C lg32.dll Shel
...
010012C0: 00 00 00 00.06 49 34 76.CE 85 33 76.84 9D 34 76
...
从Name处01007AAC看到,是输入comdlg32.dll中的函数,为新式联编、无转发。OriginalFirstThunk的第一项为00007A7A,其IMAGE_IMPORT_BY_NAME中的Hint=000F,Name[]="PageSetupDlgW"。FirstThunk的第一项为76344906,就是函数PageSetupDlgW的实际入口地址。
下面开始分析我们的目标程序etu_v235.exe,先看看JMP DWORD PTR [XXXXXXXX]的例子。例如,00420896处调用KERNEL32.dll里的函数SetFilePointer:
...
00420896: E8EFC60600 call SetFilePointer ; Hiew中ALT+F11显示为:call 00048CF8A
...
00048CF8A处为一系列JMP DWORD PTR [XXXXXXXX]的开始:
...
0048CF89: C3 retn
0048CF8A: FF25A0004D00 jmp SetFilePointer ; jmp d,[0004D00A0]
0048CF90: FF25A4004D00 jmp VirtualQuery ; jmp d,[0004D00A4]
0048CF96: FF25A8004D00 jmp WriteFile ; jmp d,[0004D00A8]
0048CF9C: FF25AC004D00 jmp InitializeCriticalSection ; jmp d,[0004D00AC]
0048CFA2: FF25B0004D00 jmp CreateMutexA ; jmp d,[0004D00B0]
0048CFA8: FF25B4004D00 jmp FindNextFileA ; jmp d,[0004D00B4]
...
0048D638: FF252C054D00 jmp DragQueryFileA ; jmp d,[0004D052C]
0048D63E: FF2530054D00 jmp DragFinish ; jmp d,[0004D0530]
0048D644: FF2534054D00 jmp DragAcceptFiles ; jmp d,[0004D0534]
0048D64A: 0000 add [eax],al
0048D64C: 0000 add [eax],al
0048D64E: 0000 add [eax],al
...
定位到输入表(它的.idata区段同样指向这里):
004D0000: 00 00 00 00.00 00 00 00.00 00 00 00.3C 05 0D 00 ; RVA: 000D053C,VA: 004D053C指向"KERNEL32.dll"
004D0010: A0 00 0D 00 ; FirstThunk的RVA=000D00A0,VA=004D00A0
004D0010: 00 00 00 00.00 00 00 00.00 00 00 00 ; USER32.dll的IMAGE_IMPORT_DESCRIPTOR
004D0020: 49 05 0D 00.F0 01 0D 00 ; FirstThunk的RVA=000D01F0,VA=004D01F0
004D0020: 00 00 00 00.00 00 00 00 ; GDI32.dll
004D0030: 00 00 00 00.54 05 0D 00.EC 03 0D 00
004D0030: 00 00 00 00 ; ADVAPI32.dll
004D0040: 00 00 00 00.00 00 00 00.5E 05 0D 00.F4 04 0D 00
004D0050: 00 00 00 00.00 00 00 00.00 00 00 00.6B 05 0D 00 ; comdlg32.dll
004D0060: 0C 05 0D 00
004D0060: 00 00 00 00.00 00 00 00.00 00 00 00 ; WINMM.dll
004D0070: 78 05 0D 00.24 05 0D 00
004D0070: 00 00 00 00.00 00 00 00 ; SHELL32.dll
004D0080: 00 00 00 00.82 05 0D 00.2C 05 0D 00
004D0080: 00 00 00 00 ; IMAGE_IMPORT_DESCRIPTOR的所有字段为NULL
004D0090: 00 00 00 00.00 00 00 00.00 00 00 00.00 00 00 00
004D00A0: 8E 05 0D 00.A0 05 0D 00.B0 05 0D 00.BC 05 0D 00 ; KERNEL32.dll的FirstThunk指向这里,IMAGE_THUNK_DATA表
004D00B0: D8 05 0D 00.E8 05 0D 00
...
004D01E0: DE 0A 0D 00.F6 0A 0D 00.00 0B 0D 00.00 00 00 00 ; [004D01EC]=0,表示KERNEL32.dll的IMAGE_THUNK_DATA表结束
004D01F0: 0E 0B 0D 00.22 0B 0D 00.32 0B 0D 00.42 0B 0D 00 ; 004D01F0: USER32.dll的FirstThunk指向这里
...
004D0530: 44 18 0D 00.52 18 0D 00.00 00 00 00.4B 45 52 4E D R KERN
004D0540: 45 4C 33 32.2E 64 6C 6C.00 55 53 45.52 33 32 2E EL32.dll USER32.
004D0550: 64 6C 6C 00.47 44 49 33.32 2E 64 6C.6C 00 41 44 dll GDI32.dll AD
004D0560: 56 41 50 49.33 32 2E 64.6C 6C 00 63.6F 6D 64 6C VAPI32.dll comdl
004D0570: 67 33 32 2E.64 6C 6C 00.57 49 4E 4D.4D 2E 64 6C g32.dll WINMM.dl
004D0580: 6C 00 53 48.45 4C 4C 33.32 2E 64 6C.6C 00 00 00 l SHELL32.dll
004D0590: 53 65 74 46.69 6C 65 50.6F 69 6E 74.65 72 00 00 SetFilePointer
004D05A0: 00 00 56 69.72 74 75 61.6C 51 75 65.72 79 00 00 VirtualQuery
004D05B0: 00 00 57 72.69 74 65 46.69 6C 65 00.00 00 49 6E WriteFile In
004D05C0: 69 74 69 61.6C 69 7A 65.43 72 69 74.69 63 61 6C itializeCritical
004D05D0: 53 65 63 74.69 6F 6E 00.00 00 43 72.65 61 74 65 Section Create
004D05E0: 4D 75 74 65.78 41 00 00.00 00 46 69.6E 64 4E 65 MutexA FindNe
004D05F0: 78 74 46 69.6C 65 41 00.00 00 46 72.65 65 52 65 xtFileA FreeRe
...
004D1850: 00 00 00 00.44 72 61 67.41 63 63 65.70 74 46 69 DragAcceptFi
004D1860: 6C 65 73 00.00 00 00 00.00 00 00 00.00 00 00 00 les
这里有8个IMAGE_IMPORT_DESCRIPTOR,对应7个DLL及一个NULL结构表示列表的结束。Borland的连接器没有生成INT,未联编,只有Name字段和FirstThunk字段有值。第一个IMAGE_IMPORT_DESCRIPTOR的Name指向004D053C,该处为字符串"KERNEL32.dll";FirstThunk指向004D00A0。004D00A0是IMAGE_THUNK_DATA表的开始,第一项的值为RVA 000D058E。VA 004D058E指向IMAGE_IMPORT_BY_NAME结构。
这个结构的Hint字段为0,Name字段为字符串"SetFilePointer"。联系到上面0048CF8A处:
0048CF8A: FF25A0004D00 jmp SetFilePointer ; jmp d,[0004D00A0]
注意这是Thunk之前,记住DWORD [0004D00A0]的值为000D058E。我们用OD载入etu_v235.exe,再来看它的值:
...
0048CF8A $-FF25 A0004D00 JMP [DWORD DS:<&KERNEL32.SetFilePointer>] ; kernel32.SetFilePointer
[DS:004D00A0]=7C810C2E (kernel32.SetFilePointer)
...
Thunk后,DWORD [0004D00A0]的值由000D058E变为7C810C2E,即loader OD将其改写为KERNEL32.dll中函数SetFilePointer的实际入口7C810C2E。
下面我们来添加一个KERNEL32.dll的WideCharToMultiByte函数。一个办法是在KERNEL32.dll的IMAGE_THUNK_DATA表末尾再插入一项,因为KERNEL32.dll的IMAGE_IMPORT_DESCRIPTOR结构在输入表的头部,这会导致所有表项的指针及前面那一长串JMP的地址需要全部改写,工作量太大。
为了使修改量最小,我们采取为KERNEL32.dll增加一个IMAGE_IMPORT_DESCRIPTOR结构的办法。
把上面004D008C~004D009F的空IMAGE_IMPORT_DESCRIPTOR变为第二个KERNEL32.dll的IMAGE_IMPORT_DESCRIPTOR,004D00A0~004D00B3共5个IMAGE_THUNK_DATA项变为空IMAGE_IMPORT_DESCRIPTOR。这样会在第一个KERNEL32.dll的IMAGE_IMPORT_DESCRIPTOR中损失开始的5个函数:SetFilePointer、VirtualQuery、WriteFile、InitializeCriticalSection和CreateMutexA,需要在第二个KERNEL32.dll的IMAGE_IMPORT_DESCRIPTOR补回来。即第一个IMAGE_IMPORT_DESCRIPTOR改为:
004D0000: 00 00 00 00.00 00 00 00.00 00 00 00.3C 05 0D 00 ; RVA: 000D053C,VA: 004D053C指向"KERNEL32.dll"
004D0010: B4 00 0D 00 ; FirstThunk的RVA=000D00B4,[VA=004D00B4]=000D05E8->FindNextFileA
其中,第一个函数由SetFilePointer变为FindNextFileA,第二个KERNEL32.dll的IMAGE_IMPORT_DESCRIPTOR:
004D0080: 00 00 00 00
004D0090: 00 00 00 00.00 00 00 00.3C 05 0D 00.7C 18 0D 00 ; 000D053C还是指向"KERNEL32.dll",FirstThunk=000D187C指向函数SetFilePointer
004D00A0: 00 00 00 00.00 00 00 00.00 00 00 00.00 00 00 00 ; 004D00A0: 空IMAGE_IMPORT_DESCRIPTOR
004D00B0: 00 00 00 00
...
在整个输入表的尾部004D1864处增加:
004D1860: 00 00 57 69.64 65 43 68.61 72 54 6F WideCharTo
004D1870: 4D 75 6C 74.69 42 79 74.65 00 00 00.8E 05 0D 00 MultiByte
004D1880: A0 05 0D 00.B0 05 0D 00.BC 05 0D 00.D8 05 0D 00
004D1890: 64 18 0D 00.00 00 00 00
这里004D1864~004D1879为新增函数WideCharToMultiByte的IMAGE_IMPORT_BY_NAME,004D187C~004D188F为5个需要补回的函数,004D1890为新增的Thunk。末尾004D1894置0表示此IMAGE_THUNK_DATA表结束。
还要记住修改一下前面0048CF8A处前5个函数的[XXXXXXXX]:
0048CF8A: FF257C184D00 jmp SetFilePointer ; jmp d,[0004D187C]
0048CF90: FF2580184D00 jmp VirtualQuery ; jmp d,[0004D1880]
0048CF96: FF2584184D00 jmp WriteFile ; jmp d,[0004D1884]
0048CF9C: FF2588184D00 jmp InitializeCriticalSection ; jmp d,[0004D1888]
0048CFA2: FF258C184D00 jmp CreateMutexA ; jmp d,[0004D188C]
在这些个JMPs的末尾添加对WideCharToMultiByte函数的调用:
0048D64A: FF2590184D00 jmp WideCharToMultiByte ; jmp d,[0004D1890]
这样我们在需要用到WideCharToMultiByte函数时,就写一句CALL 0048D64A。
最后需要修改一下IMAGE_OPTIONAL_HEADER里DataDirectory中输入表的大小:文件偏移00000184处64 18 00 00改为94 18 00 00。
至此WideCharToMultiByte函数的添加工作完成。接下来开始进行错误修正(Bugfix)。
通过分析发现菜单命令"Resources | Show menus"下对菜单资源的显示处理部分在调用004319A3里,其中有两处需要修改。第一处为处理菜单的POPUP项,在:
...
00431C1D |> 8B45 E4 |/MOV EAX,[DWORD SS:EBP-1C] ; [DWORD SS:EBP-1C]: current input ptr
00431C20 |. 31DB ||XOR EBX,EBX
00431C22 |. 66:8B10 ||MOV DX,[WORD DS:EAX] ; get one char in unicode each time
00431C25 |. 83C0 02 ||ADD EAX,2
00431C28 |. 66:8BDA ||MOV BX,DX
00431C2B |. 8945 E4 ||MOV [DWORD SS:EBP-1C],EAX ; update the current input ptr
00431C2E |. 66:8995 F4FEFFFF ||MOV [WORD SS:EBP-10C],DX ; [WORD SS:EBP-10C]: unicode of current char
00431C35 |. 85DB ||TEST EBX,EBX ; is unicode null?
00431C37 |. 74 22 ||JE SHORT etu_v235.00431C5B
00431C39 |. 8D85 28FFFFFF ||LEA EAX,[DWORD SS:EBP-D8] ; EAX: output buffer base
00431C3F |. 8A95 F4FEFFFF ||MOV DL,[BYTE SS:EBP-10C] ; here: only the low byte of the unicode used
00431C45 |. 881407 ||MOV [BYTE DS:EDI+EAX],DL ; EDI: index point to the output buffer
00431C48 |. 8B85 24FFFFFF ||MOV EAX,[DWORD SS:EBP-DC] ; [DWORD SS:EBP-DC]:top of input buffer
00431C4E |. 8B55 E4 ||MOV EDX,[DWORD SS:EBP-1C]
00431C51 |. 47 ||INC EDI ; increase index by 1 byte
00431C52 |. 39C2 ||CMP EDX,EAX
00431C54 |. 73 05 ||JNB SHORT etu_v235.00431C5B ; all input processed?
00431C56 |. 83FF 4B ||CMP EDI,4B ; 4B: output buffer size
00431C59 |.^7C C2 |\JL SHORT etu_v235.00431C1D
...
第二处为处理菜单的MENUITEM项,在:
...
00431CDC |> 8B45 E4 |/MOV EAX,[DWORD SS:EBP-1C]
00431CDF |. 31DB ||XOR EBX,EBX
00431CE1 |. 66:8B10 ||MOV DX,[WORD DS:EAX]
00431CE4 |. 83C0 02 ||ADD EAX,2
00431CE7 |. 66:8BDA ||MOV BX,DX
00431CEA |. 8945 E4 ||MOV [DWORD SS:EBP-1C],EAX
00431CED |. 66:8995 E8FEFFFF ||MOV [WORD SS:EBP-118],DX ; [WORD SS:EBP-118]: unicode of current char
00431CF4 |. 85DB ||TEST EBX,EBX
00431CF6 |. 74 22 ||JE SHORT etu_v235.00431D1A
00431CF8 |. 8D85 28FFFFFF ||LEA EAX,[DWORD SS:EBP-D8]
00431CFE |. 8A95 E8FEFFFF ||MOV DL,[BYTE SS:EBP-118]
00431D04 |. 881407 ||MOV [BYTE DS:EDI+EAX],DL
00431D07 |. 8B85 24FFFFFF ||MOV EAX,[DWORD SS:EBP-DC]
00431D0D |. 8B55 E4 ||MOV EDX,[DWORD SS:EBP-1C]
00431D10 |. 47 ||INC EDI
00431D11 |. 39C2 ||CMP EDX,EAX
00431D13 |. 73 05 ||JNB SHORT etu_v235.00431D1A
00431D15 |. 83FF 4B ||CMP EDI,4B
00431D18 |.^7C C2 |\JL SHORT etu_v235.00431CDC
...
可以发现,它抛弃了每个unicode字符的高字节。两段结构完全一样,唯一的区别是取出的unicode字符分别临时保存在[WORD SS:EBP-10C]和[WORD SS:EBP-118]。我们在代码区段的末尾0048D650处增加少许代码来转换unicode字符:
0048D64A $-FF25 90184D00 JMP [DWORD DS:<&KERNEL32.WideCharToMultiByte>] ; 这里是上面新增的函数
0048D650 /$ 6A 00 PUSH 0 ; /pDefaultCharUsed = NULL
0048D652 |. 6A 00 PUSH 0 ; |pDefaultChar = NULL
0048D654 |. 6A 02 PUSH 2 ; |MultiByteCount = 2
0048D656 |. 8D0438 LEA EAX,[DWORD DS:EAX+EDI] ; |
0048D659 |. 50 PUSH EAX ; |MultiByteStr
0048D65A |. 6A 01 PUSH 1 ; |WideCharCount = 1
0048D65C |. 52 PUSH EDX ; |WideCharStr
0048D65D |. 6A 00 PUSH 0 ; |Options = 0
0048D65F |. 6A 00 PUSH 0 ; |CodePage = CP_ACP
0048D661 |. E8 E4FFFFFF CALL <JMP.&KERNEL32.WideCharToMultiByte> ; \WideCharToMultiByte, 0048D64A
0048D666 |. 83F8 02 CMP EAX,2 ; EAX: 转换后的字符长度
0048D669 |. 75 01 JNZ SHORT etu_v235.0048D66C ; SingleByteStr
0048D66B |. 47 INC EDI ; 如果转换的是MultiByte,再加1
0048D66C |> 8B85 24FFFFFF MOV EAX,[DWORD SS:EBP-DC]
0048D672 \. C3 RETN
函数WideCharToMultiByte的调用参数,具体说明请查MSDN:
int WideCharToMultiByte(
UINT CodePage,
DWORD dwFlags,
LPCWSTR lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCSTR lpDefaultChar,
LPBOOL lpUsedDefaultChar
);
把上面第一处的00431C3F~00431C4D改为:
00431C3F |. 8D95 F4FEFFFF ||LEA EDX,[DWORD SS:EBP-10C]
00431C45 |. E8 06BA0500 ||CALL etu_v235.0048D650
00431C4A |. 90 ||NOP
00431C4B |. 90 ||NOP
00431C4C |. 90 ||NOP
00431C4D |. 90 ||NOP
把上面第二处的00431CFE~00431D0C改为:
00431CFE |. 8D95 E8FEFFFF ||LEA EDX,[DWORD SS:EBP-118]
00431D04 |. E8 47B90500 ||CALL etu_v235.0048D650
00431D09 |. 90 ||NOP
00431D0A |. 90 ||NOP
00431D0B |. 90 ||NOP
00431D0C |. 90 ||NOP
"Show menus"命令就修改完毕。下面解决菜单命令"Resources | Show strings"的问题。字符串资源的显示处理部分在调用00432475里:
00432475 /$ 55 PUSH EBP
...
004327D3 |. 31C0 |||XOR EAX,EAX
004327D5 |. 8985 ACFEFFFF |||MOV [DWORD SS:EBP-154],EAX ; [DWORD SS:EBP-154]: output index
004327DB |. EB 1D |||JMP SHORT etu_v235.004327FA
004327DD |> 8B85 ACFEFFFF |||/MOV EAX,[DWORD SS:EBP-154]
004327E3 |. 8D95 C3FEFFFF ||||LEA EDX,[DWORD SS:EBP-13D] ; [DWORD SS:EBP-13D]: output buffer base
004327E9 |. 8BC8 ||||MOV ECX,EAX
004327EB |. 01C9 ||||ADD ECX,ECX ; ECX: input index
004327ED |. 8A0C0E ||||MOV CL,[BYTE DS:ESI+ECX] ; ESI: input source base
004327F0 |. 880C10 ||||MOV [BYTE DS:EAX+EDX],CL ; here again: only the low byte of the unicode used
004327F3 |. 40 ||||INC EAX
004327F4 |. 8985 ACFEFFFF ||||MOV [DWORD SS:EBP-154],EAX ; update output index
004327FA |> 31C0 ||| XOR EAX,EAX
004327FC |. 8B95 ACFEFFFF ||||MOV EDX,[DWORD SS:EBP-154]
00432802 |. 66:8B45 B0 ||||MOV AX,[WORD SS:EBP-50] ; input source unicode string length
00432806 |. 39D0 ||||CMP EAX,EDX
00432808 |. 7E 0D ||||JLE SHORT etu_v235.00432817
0043280A |. 8B85 ACFEFFFF ||||MOV EAX,[DWORD SS:EBP-154]
00432810 |. 3D EA000000 ||||CMP EAX,0EA ; 0EA: output buffer size
00432815 |.^7C C6 |||\JL SHORT etu_v235.004327DD
00432817 |> 8D85 C3FEFFFF |||LEA EAX,[DWORD SS:EBP-13D]
0043281D |. 8B8D ACFEFFFF |||MOV ECX,[DWORD SS:EBP-154]
00432823 |. 8D9D B0FEFFFF |||LEA EBX,[DWORD SS:EBP-150]
00432829 |. C60401 00 |||MOV [BYTE DS:ECX+EAX],0 ; set ANSI string ending NULL
...
同样,它抛弃了每个unicode字符的高字节。改写004327DB~00432801之间的代码为:
004327DB . 6A 00 PUSH 0 ; /pDefaultCharUsed = NULL
004327DD > 6A 00 PUSH 0 ; |pDefaultChar = NULL
004327DF . 68 EA000000 PUSH 0EA ; |MultiByteCount = EA (234.)
004327E4 . 8D85 C3FEFFFF LEA EAX,[DWORD SS:EBP-13D] ; |
004327EA . 50 PUSH EAX ; |MultiByteStr
004327EB . 0FB745 B0 MOVZX EAX,[WORD SS:EBP-50] ; |
004327EF . 50 PUSH EAX ; |WideCharCount
004327F0 . 56 PUSH ESI ; |WideCharStr
004327F1 . 6A 00 PUSH 0 ; |Options = 0
004327F3 . 6A 00 PUSH 0 ; |CodePage = CP_ACP
004327F5 . E8 50AE0500 CALL <JMP.&KERNEL32.WideCharToMultiByte> ; \WideCharToMultiByte
004327FA . 8985 ACFEFFFF MOV [DWORD SS:EBP-154],EAX
00432800 . EB 15 JMP SHORT etu_v235.00432817
至此,英文版的问题得到解决,我们再看看汉化版的情况。
chixiaojie的附件中"ETU-DASM 汉化版.exe"被汉化者加了壳,"Exeinfo PE"显示是"MoleBox Pro 2.3640 prod.ver 2.7.0"(跟踪的结果表明为"2.7.0.3570")。代码、数据和资源等区段都被加密压缩,得解开才好修改。
文件末尾有0xD200字节的Overlay,试了Resty、Sh4DoVV等的Script和Extractor,无解。只好老老实实按CCDebuger和wynney等的帖子在OD里手工解决。OD配置:OllyDbg v1.10英文原版+StrongOD 0.4.6.816,忽略所有异常。
OD载入目标后停在入口:
005133D3 > $ E8 00000000 CALL ETU-DASM.005133D8
005133D8 $ 60 PUSHAD
005133D9 . E8 4F000000 CALL ETU-DASM.0051342D
...
PUSHAD提示在005133D9处HR ESP会很快将我们带到OEP。果然,经F7,F7,HR ESP,F9后:
00512B0E |. 58 POP EAX
00512B0F |. 58 POP EAX
00512B10 |. FFD0 CALL EAX ; ETU-DASM.00410000
...
OEP处:
00410000 A1 8D134B00 MOV EAX,DWORD PTR DS:[4B138D]
00410005 C1E0 02 SHL EAX,2
00410008 A3 91134B00 MOV DWORD PTR DS:[4B1391],EAX
0041000D 57 PUSH EDI
0041000E 51 PUSH ECX
0041000F 33C0 XOR EAX,EAX
00410011 BF 00004B00 MOV EDI,ETU-DASM.004B0000
00410016 B9 34134B00 MOV ECX,ETU-DASM.004B1334 ; ASCII "Borland C++ - Copyright 1993 Borland Intl."
...
结果与前面分析的英文原版一致。通过前面的分析,我们清楚地知道输入表在004D0000:
004D0000 00 00 00 00 FE FF FF FF 00 00 00 00 3C 05 0D 00
004D0010 A0 00 0D 00 00 00 00 00 FE FF FF FF 00 00 00 00
004D0020 49 05 0D 00 F0 01 0D 00 00 00 00 00 FE FF FF FF
004D0030 00 00 00 00 54 05 0D 00 EC 03 0D 00 00 00 00 00
004D0040 FE FF FF FF 00 00 00 00 5E 05 0D 00 F4 04 0D 00
004D0050 00 00 00 00 FE FF FF FF 00 00 00 00 6B 05 0D 00
004D0060 0C 05 0D 00 00 00 00 00 FE FF FF FF 00 00 00 00
004D0070 78 05 0D 00 24 05 0D 00 00 00 00 00 FE FF FF FF
004D0080 00 00 00 00 82 05 0D 00 2C 05 0D 00 00 00 00 00
004D0090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
004D00A0 00523F90 ETU-DASM.00523F90
004D00A4 7C80B9D1 kernel32.VirtualQuery
004D00A8 7C810D87 kernel32.WriteFile
004D00AC 7C809EF1 kernel32.InitializeCriticalSection
004D00B0 7C80E93F kernel32.CreateMutexA
004D00B4 00522FD0 ETU-DASM.00522FD0
004D00B8 7C8260C2 kernel32.FreeResource
004D00BC 7C8111DA kernel32.GetVersion
004D00C0 00523AC0 ETU-DASM.00523AC0
004D00C4 00522FA0 ETU-DASM.00522FA0
004D00C8 7C832EB1 kernel32.LocalUnlock
...
004D01E4 7C86136D kernel32.WinExec
004D01E8 00523BE0 ETU-DASM.00523BE0
004D01EC 00000000
004D01F0 77D18F9D user32.GetSystemMetrics
...
这里我们可以发现,到达OEP时"MoleBox Pro"已经完成了Thunk过程,大部分IMAGE_THUNK_DATA项已经指向了API的实际入口地址,但是某些API,比如KERNEL32.dll的第一项SetFilePointer(004D00A0处)指向了"MoleBox Pro"的附加区段.adata内的一个调用00523F90,这就是所谓的“IAT加密”。ImpREC表明它是"Invalid"的。当然,我们可以找到:
0051C70C |. 8906 ||MOV DWORD PTR DS:[ESI],EAX
这个指令完成Thunk;下面这条指令进行“IAT加密”:
0051CE4D . 8906 MOV DWORD PTR DS:[ESI],EAX
将0051CE4D处2字节NOP就去掉了“IAT加密”。只剩一个004D00C0处的IMAGE_THUNK_DATA项指向00523AC0:
004D00C0 00523AC0 ETU-DASM.00523AC0
我们来看看它应该指向哪里。"BP VirtualProtect" Shift+F9八次后,原始输入表被解密,这时还未被Thunk。004D00C0指向000D0616,即函数GetProcAddress。之所以它比较特殊,是因为"MoleBox Pro"自身要用它来进行Thunk。通过BP GetProcAddress,可以得到GetProcAddress的实际入口地址为7C80ADA0,填回去,ImpREC就显示完全正常了。
基本上,"BP VirtualProtect" Shift+F9七次后,OEP所在的代码区段CODE和数据区段TLSCBA得到解密。
到OEP后,就可以Dump了。奇怪的是,"OllyDump 3.00.110"还Dump不了,不知是什么ANTI。显示"Unable to read memory of debugged process (00400000..0052FFFF)."和"Bad DOS Signature!!"。下面这句脚本也显示相同的提示:
dpe "C:\Temp\ETU-Dasm v2.35 ALPHA\test.exe", 00410000
LordPE的缺省engine - "LordPE's job"也不行,提示"Couldn't grab process memory : (";用"IntelliDump",提示"0x4B000 of 0x130000 bytes could not be dumped and were padded with zeros.",且Dump的大小约1.18MB。
Dump的结果修复起来很麻烦。这里就偷懒了,反正只需要“汉化”版的数据区段和资源区段,用下面两句脚本进行Dump:
dm 4B0000, 00015800, "C:\Temp\ETU-Dasm v2.35 ALPHA\4B0000.bin"
dm 500000, 00010000, "C:\Temp\ETU-Dasm v2.35 ALPHA\500000.bin"
其依据为原始英文版的区段表:
Number Name VirtSize RVA PhysSize Offset Flag
1 CODE 0007E000 00010000 0007D800 00000600 60000020
2 TLSCBA 00016000 00090000 00000000 0007DE00 C0000040
3 TLSCBA 00016000 000B0000 00015800 0007DE00 C0000040
4 .idata 00002000 000D0000 00001A00 00093600 C0000040
5 .edata 00001000 000E0000 00000200 00095000 40000040
6 .reloc 00007000 000F0000 00006200 00095200 50000040
7 .rsrc 00011000 00100000 00010800 0009B400 50000040
和"MoleBox Pro"加壳的"ETU-DASM 汉化版.exe"的区段表:
Number Name VirtSize RVA PhysSize Offset Flag
1 CODE 0007E000 00010000 00034A00 00000400 60000020
2 TLSCBA 00016000 00090000 00000000 00000000 C0000080
3 TLSCBA 00016000 000B0000 00006000 00034E00 C0000040
4 .idata 00002000 000D0000 00000A00 0003AE00 C0000040
5 .edata 00001000 000E0000 00000200 0003B800 40000040
6 .reloc 00007000 000F0000 00000000 00000000 40000080
7 .rsrc 0000FF10 00100000 00010000 0003BA00 40000040
8 .adata 0001FB20 00110000 00012A00 0004BA00 E0000020
以及"ETU-DASM 汉化版.exe"在OD中的内存窗口信息:
Address Size Owner Section Contains Type Access Initial access
004B0000 00016000 (90112.) ETU-DASM 00400000 TLSCBA Imag 01001040 RWE RWE
00500000 00010000 (65536.) ETU-DASM 00400000 .rsrc resources Imag 01001002 R RWE
在改好的英文版的基础上,将区段TLSCBA和区段.rsrc用Dump的结果4B0000.bin和500000.bin分别替换。最后调整一下对应区段和DataDirectory里对应项的信息就大功告成了!
从Dump的数据区段也可以发现,该汉化版并不“完善”:凡是代码生成的对话框的Caption、主窗口Status栏的文本及错误信息等全部没有“汉化”。当然,汉化者在"汉化说明.txt"中已经说了“汉化重点是右键功能。”;它还有一个“毛病”,汉化者为了美观,所有菜单命令的快捷键将不起作用。已经不错了,将就用吧。
附件:
英文版:ETU-Dasm_v2.35_ALPHA.7z
etu_v235.exe
Size: 704,512
MD5: fd59ecac46ea6ac8e580e06dcef7fde6
汉化版:ETU-Dasm v2.35 ALPHA CHS.7z
etu_v235.exe
Size: 701,440
MD5: fc85ce5fbc811eecbf8938fb89113960
ETU-Dasm_v2.35_ALPHA.7z
ETU-Dasm v2.35 ALPHA CHS.7z
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: