【文章标题】: 逆向软件原理及功能重现之初步
【文章作者】: 书呆彭
【作者邮箱】: pengzhicheng1986@gmail.com
【软件名称】: 魔兽争霸显血工具
【下载地址】: http://hi.baidu.com/pylzj/blog/item/ca25a6af4f3736c97dd92a67.html
【加壳方式】: UPX 0.89.6 - 1.02 / 1.05 - 1.24 -> Markus & Laszlo
【编写语言】: VB 5/6
【使用工具】: PEiD OD VC60 MSDN60
【软件介绍】: 一个魔兽争霸的小辅助工具,非作弊性质。
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
论坛上有人发了这个工具,请求帮助,正好无事,拿来看了一下。
该工具目前已经有新版本,作者吕志杰,他的空间是http://hi.baidu.com/pylzj
对人家的工具分析了一次,就帮着宣传下,否则有点过意不去。
该工具其实很简单但实用。在玩魔兽争霸时,我们经常为了查看单位的血量而按ALT键,但ALT和WIN键是挨着的,结果很容易碰到WIN键而弹出游戏,非常影响。
该工具就是可以让单位的血条一直显示而不必按住ALT,同时也提供了屏蔽WIN键的功能。
按HOME可以显示或隐藏自己单位的血量。按END是对方敌人单位的。
新版本还加入了强行退出游戏的功能,用来解决浩方对战平台玩时游戏无响应的问题(引用作者原话)。
我分析的这个版本作者留下的发布日期为2008-03-06,请参见http://bbs1.pediy.com/showthread.php?t=74866
好,分析开始。
首先,将它发送到PEiD,告诉我UPX 0.89.6 - 1.02 / 1.05 - 1.24 -> Markus & Laszlo
呵呵,因为本来就是个无私发布的工具,也不需要什么保护。
脱壳机伺候之。
脱壳之后,程序由13.0K“长大”到了 40.3K。
再次用PEiD查看,Microsoft Visual Basic 5.0 / 6.0
用P Code or Nativo 查看,PEiD无响应,可能我的插件版本问题。
用VB Decompiler Pro处理,显示为 Native Code.
从左边的树形资源中可以看到,有两个类,frmMain和cHotKey.
cHotKey可能是作者自己编写的或使用的第三方的类。
对VB不太熟悉,先试着用OD跟一下。
载入程序时有一个异常,忽略之(调试VB时会有大量的异常,习惯就好),来到入口点。
由于以前写程序,知道注册热键的API,所以直接 bp RegisterHotKey, F9运行,断下,堆栈窗口为:
0012F9D8 00405080 /CALL 到 RegisterHotKey 来自 显血工具.0040507B
0012F9DC 000203F2 |hWnd = 000203F2 ('关闭(&C)',class='ThunderRT6CommandButton',parent=000203FE)
0012F9E0 FFFFC038 |HotKeyID = FFFFC038
0012F9E4 00000000 |Modifiers = 0
0012F9E8 00000024 \Key = VK_HOME
可以看到下如所料,参数为 VK_HOME
再一次运行,中断,堆栈为
0012F9D8 00405080 /CALL 到 RegisterHotKey 来自 显血工具.0040507B
0012F9DC 000203F2 |hWnd = 000203F2 ('关闭(&C)',class='ThunderRT6CommandButton',parent=000203FE)
0012F9E0 FFFFC037 |HotKeyID = FFFFC037
0012F9E4 00000000 |Modifiers = 0
0012F9E8 00000023 \Key = VK_END
好。这次是VK_END
再一次F9程序便运行起来,说明只注册了这两个热键。
我原本的思路是直接到与热键关联的窗口的窗口过程去分析,结果发现注册热键时用的窗口参数是 “关闭” 按钮。
并且转到该窗口过程下消息断点,按HOME键后并不中断。
换个思路。
回到VB Decompiler,查看代码,发现RegisterHotKey是在 cHotKey 类的方法 AddHotKeys 中调用的。
也就是说HotKey的回调处理被cHotKey类封装起来了。由于对VB的类不懂,决定从其它地方下手。
我们知道VB中调用VB运行时的函数不需要声明,而调用其它函数需要进行声明。
我们就从他调用的API下手。
从VB Decompiler给出的结果看,它只引入了少量的外部API,分别是
GlobalDeleteAtom
GlobalAddAtom
WaitMessage
PeekMessage
UnregisterHotKey
RegisterHotKey
CopyMemory
CallNextHookEx
UnhookWindowsHookEx
SetWindowsHookEx
keybd_event
ShellExecute
跟HotKey有关的函数有 GlobalAddAtom,GlobalDeleteAtom,RegisterHotKey,UnregisterHotKey,没什么异样的
但从PeekMessage我猜到我之所以在VB的运行时中的窗口过程中断不下来,可能是因为cHotKey类处理WM_HOTKEY消息的方法是PeekMeesage后直接处理,而没有DispatchMessage.
从RegisterHotKey这条路不好走,不过我看到了感兴趣的东西,SetWindowsHookEx和keybd_event
好吧,看看软件用这些API做了些什么。bp SetWindowsHookExA,运行程序,不中断
那我们做点什么。点选 “禁用WIN键”, 啪,OD断到了。好,看一下参数:
0012F20C 00406A5A /CALL 到 SetWindowsHookExA 来自 显血工具.00406A55
0012F210 0000000D |HookType = 13.
0012F214 00406930 |Hookproc = 显血工具.00406930
0012F218 00400000 |hModule = 00400000 (显血工具)
0012F21C 00000000 \ThreadID = 0
参照MSDN,查看WIN32 SDK的头文件, HookType = 13 == WH_KEYBOARD_LL
MSDN对这个参数的解释为:Installs a hook procedure that monitors low-level keyboard input events. For more information, see the LowLevelKeyboardProc hook procedure.
这是个低级键盘消息的系统钩子,我们来到参数指出的地址:00406930,看下这个钩子干了什么。
根据MSDN,这个函数的原型为:
LRESULT CALLBACK LowLevelKeyboardProc(
int nCode, // hook code
WPARAM wParam, // message identifier
LPARAM lParam // pointer to structure with message data
);
我们需要由此分析堆栈。
注意:这里下断的话,每次按F9运行程序,结果每次都会断到这里。所以要么用鼠标点“运行”,要么干脆不要下断点。
我们分析代码(我去掉了对我们不太重要的机器码那一列,以使代码更清楚):
00406930 >PUSH EBX
00406931 >MOV EBX, DWORD PTR SS:[ESP+10] ; EBX存放一个指针指向一个 KBDLLHOOKSTRUCT 的结构体
00406935 >PUSH EBP
00406936 >MOV EBP, DWORD PTR DS:[<&MSVBVM60.__>; MSVBVM60.__vbaSetSystemError
0040693C >PUSH ESI
0040693D >MOV ESI, DWORD PTR SS:[ESP+14]
00406941 >PUSH EDI
00406942 >MOV EDI, DWORD PTR SS:[ESP+14]
00406946 >TEST EDI, EDI
00406948 >JNZ SHORT 显血工具.00406992 ; 这几条指令是简单地参数检查而已。
0040694A >CMP ESI, 100
00406950 >JE SHORT 显血工具.0040696A
00406952 >CMP ESI, 104
00406958 >JE SHORT 显血工具.0040696A
0040695A >CMP ESI, 101
00406960 >JE SHORT 显血工具.0040696A
00406962 >CMP ESI, 105
00406968 >JNZ SHORT 显血工具.00406992
0040696A >PUSH 10 ; nBytesToCopy = 0x10 ; == sizeof(KBDLLHOOKSTRUCT)
0040696C >PUSH EBX ; pSrc = EBX
0040696D >PUSH 显血工具.00408024 ; pDst = 408024
00406972 >CALL 显血工具.00402DD4 ; 这个函数是CopyMemory函数的一外包装
00406977 >CALL EBP ; __vbaSetSystemError,这种VB运行时在VB程序中非常多,无视之
00406979 >MOV EAX, DWORD PTR DS:[408024] ; EAX = KBDLLHOOKSTRUCT.vkCode,刚复制过来的KBDLLHOOKSTRUCT结构体
0040697E >CMP EAX, 5B ; 0x5b == VK_LWIN
00406981 >JL SHORT 显血工具.00406992
00406983 >CMP EAX, 5C ; 0x5c == VK_RWIN
00406986 >JG SHORT 显血工具.00406992
00406988 >POP EDI
00406989 >POP ESI
0040698A >POP EBP
0040698B >OR EAX, FFFFFFFF
0040698E >POP EBX
0040698F >RETN 0C
00406992 >PUSH EBX
00406993 >PUSH ESI
00406994 >PUSH EDI
00406995 >PUSH 0
00406997 >CALL 显血工具.00402D7C ; CallNextHookEx的包装
0040699C >MOV ESI, EAX
0040699E >CALL EBP
004069A0 >MOV EAX, ESI
004069A2 >POP EDI
004069A3 >POP ESI
004069A4 >POP EBP
004069A5 >POP EBX
004069A6 >RETN 0C
那么这个函数的功能已经非常明显了,就是屏蔽WIN键。它的伪代码是
if ( (LPKBDLLHOOKSTRUCT) (arg3) -> vkCode >= VK_LWIN &&
(LPKBDLLHOOKSTRUCT) (arg3) -> vkCode <= VK_RWIN)
{
return;
}
else
{
return CallNextHookEx(arg1,arg2,arg3);
}
这段代码非常常见,也不复杂。
好,那么现在只剩下keybd_event函数了。
那么我们显示或隐藏血条的功能百分之九十以上是通过它实现的。
再看看MSDN,The keybd_event function synthesizes a keystroke
就是说这个函数是模拟一次键盘敲击。
究竟是什么,看一下就知道了。 bp keybd_event,F9运行起来。
我们试着按一下HOME键, 啪,果然被OD断下。
堆栈是这样的
0012EF10 00403F82 /CALL 到 keybd_event 来自 显血工具.00403F7D
0012EF14 000000DB |Key = DB
0012EF18 00000000 |ScanCode = 0
0012EF1C 00000000 |Flags = 0
0012EF20 00000000 \ExtraInfo = 0
按F9再运行,再按HOME,又被断下,栈区参数有一个发生变化,如下
0012EF10 00403F82 /CALL 到 keybd_event 来自 显血工具.00403F7D
0012EF14 000000DB |Key = DB
0012EF18 00000000 |ScanCode = 0
0012EF1C 00000002 |Flags = KEYEVENTF_KEYUP
0012EF20 00000000 \ExtraInfo = 0
其中参数 Flags 在两次调用时不相同。反复按HOME,这个参数就在这两个值之间变化。
另外,按END键时参数Key = 0xDD,其它都相同。
MSDN:KEYEVENTF_KEYUP If specified, the key is being released. If not specified, the key is being depressed.
那么就非常清晰了,当你按下这个键,就显示血量,当你松开这个键,就不显示。
那么这个键是键盘上的哪个键呢???
查找MSDN,0xDB,0xDD,没有对应的VK_XXXX的常量定义,而是
DB–E4 OEM specific
哦,这两个键不对应于键盘,是真正的“虚拟”键码了,呵呵。
那么就是说,如果键盘上有某个键,它的键码是0xDB,那么这个键在玩魔兽时就是用来显示自己单位血条的,而0xDD是敌人的。
OEM Specific,那么暴雪就利用了这两个键码。
好吧,原来是这么简单的东西,那就自己写个程序验证一下。屏蔽Win键的因为有很多人都说过,也比较公开,就不写了,我只是简单地写了个显示和隐藏血条的功能。
在VC60下编译,运行,进入魔兽争霸,按一下HOME,自己单位的血条显示出来了。按下END,敌人也成功。
以下是写的代码,比较丑陋,大家不要见笑。
// 血条显示.cpp : Defines the entry point for the application.
//
#include <windows.h>
HINSTANCE hInst; // current instance
TCHAR szTitle[] = "Demo"; // The title bar text
TCHAR szWindowClass[] = "DemoClass"; // The title bar text
TCHAR szHello[] = "按HOME键显示或隐藏自己的血条\n按END键显示或隐藏敌人的血条\n对魔兽争霸有效";
// Foward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE hInstance);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// TODO: Place code here.
hInst = hInstance; // Store instance handle in our global variable
MSG msg;
ATOM homeAtom = ::GlobalAddAtom( "HOME" );
ATOM delAtom = ::GlobalAddAtom( "DEL" );
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = 0;
wcex.hCursor = 0;
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = 0;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = 0;
::RegisterClassEx(&wcex);
HWND hWnd = ::CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
::RegisterHotKey( hWnd, homeAtom, 0, VK_HOME );
::RegisterHotKey( hWnd, delAtom, 0, VK_END );
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if ( msg.message == WM_DESTROY )
{
::UnregisterHotKey( hWnd, homeAtom );
::UnregisterHotKey( hWnd, delAtom );
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
::GlobalDeleteAtom( homeAtom );
::GlobalDeleteAtom( delAtom );
return msg.wParam;
}
//
// FUNCTION: WndProc(HWND, unsigned, WORD, LONG)
//
// PURPOSE: Processes messages for the main window.
//
// WM_HOTKEY - process the hotkey
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
RECT rt;
GetClientRect(hWnd, &rt);
DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_HOTKEY:
{
UINT vk = (UINT) HIWORD(lParam);
if ( vk == VK_HOME ) // home键按下,显示或隐藏自己部队的血条
{
static bool keydown = false; //初始不显示
if ( keydown )
{
keydown = false;
::keybd_event( 0xdb, 0, KEYEVENTF_KEYUP, 0 ); // 隐藏
}
else
{
keydown = true;
::keybd_event( 0xdb, 0, 0, 0 ); // 显示
}
break;
}
if ( vk == VK_END ) // END键,敌人血条
{
static bool keydown = false;
if ( keydown )
{
keydown = false;
::keybd_event( 0xdd, 0, KEYEVENTF_KEYUP, 0 );
}
else
{
keydown = true;
::keybd_event( 0xdd, 0, 0, 0 );
}
break;
}
}
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
--------------------------------------------------------------------------------
【经验总结】
以上过程是我整理后写的,与我实际的分析过程有些许差别。
我实际分析时,脱壳后直接用OD跟踪,我凭以往的经验,分别在以下API下过断点,
OpenProcess
PostThreadMessage
SetKeyboardState
RegisterHotKey
因为我之前以为该工具是直接操作魔兽争霸的进程,甚至试了WriteProcessMemory
结果可想而知,只有RegisterHotKey函数被断下来了,然而却没有太大用。
后来我才想到VB的专用分析工具,才最终把它弄明白了。
本文水平不算太高,权当抛砖引玉之用。如有错误或不恰当的地方,欢迎大家提出,不甚感谢。
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2008年10月18日 20:47:19
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!