orz...原来vb的native code是这样调试的……
Wednesday, 20. September 2006, 12:10:05
标 题:解读 (6千字)
发信人:blowfish
时 间:2001-9-20 15:34:33
详细信息:
VB DLL(主要指msvbvm50.dll、msvbvm60.dll)中导出的函数没有正式文档描述,既不知道传递给函数的参数是什么,也不知道函数返回什么值,这对从汇编层次上理解VB程序的算法和流程造成了一定的困难。
VB程序使用的关键数据结构是VARIANT(MSDN中有描述),这是用作OLE Automation的一个通用结构,实际上是一个联合,其中可以存放各种标准的数据类型如LONG、BYTE、FLOAT、BSTR等。该结构中有一个标志用来指明在某一时刻该结构中具体存放的是哪种数据类型。不同的数据类型之间可以根据需要进行转换。
该结构的定义如下(#include <oaidl.h>)。其中vt就是用来表明实际存放的数据类型的标志。从该结构的偏移0x08处开始就是实际的数据。一般传给VB函数的参数都是某个VARIANT结构的指针,假定该指针的值放在寄存器eax中,则用"dd eax+8"(对于值类型)或"dd *(eax+8)"(对于指针类型)就可以看见传给该函数的实际参数值。知道这一点之后再去分析VB DLL中的函数相对来说就比较简单一些。/* VARIANT STRUCTURE * * VARTYPE vt; //WORD * WORD wReserved1; * WORD wReserved2; * WORD wReserved3; * union { * LONG VT_I4 * BYTE VT_UI1 * SHORT VT_I2 * FLOAT VT_R4 * DOUBLE VT_R8 * VARIANT_BOOL VT_BOOL * SCODE VT_ERROR * CY VT_CY * DATE VT_DATE * BSTR VT_BSTR * IUnknown * VT_UNKNOWN * IDispatch * VT_DISPATCH * SAFEARRAY * VT_ARRAY * BYTE * VT_BYREF|VT_UI1 * SHORT * VT_BYREF|VT_I2 * LONG * VT_BYREF|VT_I4 * FLOAT * VT_BYREF|VT_R4 * DOUBLE * VT_BYREF|VT_R8 * VARIANT_BOOL * VT_BYREF|VT_BOOL * SCODE * VT_BYREF|VT_ERROR * CY * VT_BYREF|VT_CY * DATE * VT_BYREF|VT_DATE * BSTR * VT_BYREF|VT_BSTR * IUnknown ** VT_BYREF|VT_UNKNOWN * IDispatch ** VT_BYREF|VT_DISPATCH * SAFEARRAY ** VT_BYREF|VT_ARRAY * VARIANT * VT_BYREF|VT_VARIANT * PVOID VT_BYREF (Generic ByRef) * CHAR VT_I1 * USHORT VT_UI2 * ULONG VT_UI4 * INT VT_INT * UINT VT_UINT * DECIMAL * VT_BYREF|VT_DECIMAL * CHAR * VT_BYREF|VT_I1 * USHORT * VT_BYREF|VT_UI2 * ULONG * VT_BYREF|VT_UI4 * INT * VT_BYREF|VT_INT * UINT * VT_BYREF|VT_UINT * } */
标准数据类型VARTYPE的定义(#include <wtypes.h>):typedef unsigned short VARTYPE; /* * VARENUM usage key, * * * [V] - may appear in a VARIANT * * [T] - may appear in a TYPEDESC * * [P] - may appear in an OLE property set * * [S] - may appear in a Safe Array * * * VT_EMPTY [V] [P] nothing * VT_NULL [V] [P] SQL style Null * VT_I2 [V][T][P][S] 2 byte signed int * VT_I4 [V][T][P][S] 4 byte signed int * VT_R4 [V][T][P][S] 4 byte real * VT_R8 [V][T][P][S] 8 byte real * VT_CY [V][T][P][S] currency * VT_DATE [V][T][P][S] date * VT_BSTR [V][T][P][S] OLE Automation string * VT_DISPATCH [V][T][P][S] IDispatch * * VT_ERROR [V][T][P][S] SCODE * VT_BOOL [V][T][P][S] True=-1, False=0 * VT_VARIANT [V][T][P][S] VARIANT * * VT_UNKNOWN [V][T] [S] IUnknown * * VT_DECIMAL [V][T] [S] 16 byte fixed point * VT_RECORD [V] [P][S] user defined type * VT_I1 [V][T][P][s] signed char * VT_UI1 [V][T][P][S] unsigned char * VT_UI2 [V][T][P][S] unsigned short * VT_UI4 [V][T][P][S] unsigned short * VT_I8 [T][P] signed 64-bit int * VT_UI8 [T][P] unsigned 64-bit int * VT_INT [V][T][P][S] signed machine int * VT_UINT [V][T] [S] unsigned machine int * VT_VOID [T] C style void * VT_HRESULT [T] Standard return type * VT_PTR [T] pointer type * VT_SAFEARRAY [T] (use VT_ARRAY in VARIANT) * VT_CARRAY [T] C style array * VT_USERDEFINED [T] user defined type * VT_LPSTR [T][P] null terminated string * VT_LPWSTR [T][P] wide null terminated string * VT_FILETIME [P] FILETIME * VT_BLOB [P] Length prefixed bytes * VT_STREAM [P] Name of the stream follows * VT_STORAGE [P] Name of the storage follows * VT_STREAMED_OBJECT [P] Stream contains an object * VT_STORED_OBJECT [P] Storage contains an object * VT_BLOB_OBJECT [P] Blob contains an object * VT_CF [P] Clipboard format * VT_CLSID [P] A Class ID * VT_VECTOR [P] simple counted array * VT_ARRAY [V] SAFEARRAY* * VT_BYREF [V] void* for local use * VT_BSTR_BLOB Reserved for system use */ enum VARENUM { VT_EMPTY = 0, VT_NULL = 1, VT_I2 = 2, VT_I4 = 3, VT_R4 = 4, VT_R8 = 5, VT_CY = 6, VT_DATE = 7, VT_BSTR = 8, VT_DISPATCH = 9, VT_ERROR = 10, VT_BOOL = 11, VT_VARIANT = 12, VT_UNKNOWN = 13, VT_DECIMAL = 14, VT_I1 = 16, VT_UI1 = 17, VT_UI2 = 18, VT_UI4 = 19, VT_I8 = 20, VT_UI8 = 21, VT_INT = 22, VT_UINT = 23, VT_VOID = 24, VT_HRESULT = 25, VT_PTR = 26, VT_SAFEARRAY = 27, VT_CARRAY = 28, VT_USERDEFINED = 29, VT_LPSTR = 30, VT_LPWSTR = 31, VT_RECORD = 36, VT_FILETIME = 64, VT_BLOB = 65, VT_STREAM = 66, VT_STORAGE = 67, VT_STREAMED_OBJECT = 68, VT_STORED_OBJECT = 69, VT_BLOB_OBJECT = 70, VT_CF = 71, VT_CLSID = 72, VT_BSTR_BLOB = 0xfff, VT_VECTOR = 0x1000, VT_ARRAY = 0x2000, VT_BYREF = 0x4000, VT_RESERVED = 0x8000, VT_ILLEGAL = 0xffff, VT_ILLEGALMASKED = 0xfff, VT_TYPEMASK = 0xfff };
VB DLL还调用了oleauto32.dll中的部分函数。oleauto32.dll是个通用的proxy/stub DLL,其每个函数的原型在<oleauto.h>中定义,并在MSDN中有详细描述。这也有助于理解VB DLL中的函数的作用。
举例:LEA EAX, [EBP-58] PUSH EAX CALL [MSVBVM60!__vbaI4Var]
执行call之前敲dd eax+8,得到的值为3;
执行完call之后,eax = 3
从而可知__vbaI4Var的作用是将一个VARIANT转换为I4(即一个长整数)。
简单来说,根据VARIANT的结构,在调试中看到的地址如果直接d xxxx是看不到变量真实的数值的(看到的是VARTYPE),要加上8的偏移量才行(也就是要跳过那4个长度为WORD的变量)。认识到这一点,用OD调试Native程序就简单多了(SmartCheck实际上得到的是一个VB函数调用的log,虽然方便,但似乎log得不太完整,所以如果要对代码进行分析,还是要用OD)。
简单记录一下某个程序的调试:
这个程序的注册对话框非常简单,一项是用户名,一项是根据用户名生成的序列号,下面就是key,输入错误的key会弹出提示对话框。
首先 bp rtcMsgBox 对MsgBox调用设置断点,拦截下来后用ctrl+f9运行到返回(alt+f9似乎用不了,可能是因为msvbvm60.dll跟执行文件放在同一目录下,被OD认为也是用户代码)
返回到这里:
0045BC6A . 51 push ecx 0045BC6B . FF15 90104000 call near [<&MSVBVM60.#595>] ; MSVBVM60.rtcMsgBox 0045BC71 . 8D95 5CFFFFFF lea edx, [ebp-A4] 0045BC77 . 8D85 6CFFFFFF lea eax, [ebp-94]
向上看可以看到一个跳转的终点
0045BBE1 . /E9 2E010000 jmp app.0045BD14 0045BBE6 > |B9 04000280 mov ecx, 80020004 0045BBEB . |B8 0A000000 mov eax, 0A
弹出对话框的那段代码就是从0045BBE6开始执行的了。再上一行可以看到一个jmp,这个jmp会跳过MsgBox这段代码,也就是说,前面应该有一个判断调转,如果key错误,则跳到0045BBE6,否则继续执行,直到0045BBE1的jmp指令。
看看是从哪里跳到0045BBE6的。鼠标点中0045BBE6那行,可以看到:
跳转来自 0045AF41
从右键菜单就可以跳到那个判断语句了:
0045AF3A . 66:399D BCFEF>cmp [ebp-144], bx 0045AF41 . 0F84 9F0C0000 je app.0045BBE6
是比较[ebp-144]和bx的值来决定跳转的。一时难以看出bx怎么得来的,但再向上,可以看到[ebp-144]的来源:
0045AEC6 . 50 push eax 0045AEC7 . 51 push ecx 0045AEC8 . 895D A8 mov [ebp-58], ebx 0045AECB . C785 2CFFFFFF>mov dword ptr [ebp-D4], 8008 0045AED5 . FF15 E4104000 call near [<&MSVBVM60.__vbaVarTstEq>] ; MSVBVM60.__vbaVarTstEq 0045AEDB . 8985 BCFEFFFF mov [ebp-144], eax
嗯,这个MSVBVM60.__vbaVarTstEq从字面上看是比较字符串。按[F2]对call下断点,重新点一次注册按键。
拦截下来的时候可以在堆栈窗口看到它的参数:
0013F344 0013F434 |Arg1 = 0013F434 0013F348 0013F484 \Arg2 = 0013F484
两个参数是通过那两条push eax和push ecx指令压入stack的。可是 d eax看到的数值是
0013F484 08 00 13 00
而d ecx看到的是
0013F434 08 80 00 00
这个既不是字符串,也不像一个指针(根据上面那篇文章,其实0008是VT_BSTR的意思...嗯,可是为什么下面那个是8008呢?@)。但是加上8字节的偏移量后呢?
d eax+8:
0013F48C 74 E3 15 00
d ecx+8:
0013F43C 64 C4 15 00
现在看起来是指针了。用d [eax+8]和d [ecx+8],果然看到了Unicode形式的真实key和自己输入的伪key。
继续逆着向上,结合SmartCheck得到的信息,就可以很容易地分析出Key的计算方法,写keymaker了。








