MessageBox细解目录(一)
0x00 简介
0.1 关于
开始学习WIN32API的时候,写过的第一个程序。就是使用MessageBox输出那个经典的HelloWorld!。但好像我对MessageBox的了解可能也只是皮毛的一点点。我决定重新回顾这个开始,重新认识一下它们。
0.2 工具
操作系统:Windows10。
调试器:Ollydbg以及IDA。
编译器:VS2019
0x01 程序对象
1.1 程序源码
此代码部分内容,仅适用于user32.dll(版本:10.0.19041.610)
#include <Windows.h>
#include "resource.h"
#define MSGBOX 0//设置预编译选项
VOID CALLBACK MsgBoxCallback(LPHELPINFO lpHelpInfo);//消息盒子帮助回调函数
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) {
#ifdef MSGBOXIND
MSGBOXPARAMS mbparams;
#endif
#ifdef MSGBOXEX//消息盒子更改语言为英文
MessageBoxEx(NULL, TEXT("Hello World!"), TEXT("Test"), MB_OKCANCEL, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));
#elif defined(MSGBOXIND)//消息盒子更改语言为英文,并添加帮助按钮和用户自定义图标。
memset(&mbparams, 0, sizeof(MSGBOXPARAMS));
mbparams.cbSize = sizeof(MSGBOXPARAMS);
mbparams.dwStyle = MB_OKCANCEL | MB_USERICON | MB_HELP;
mbparams.lpszText = TEXT("Hello World!");
mbparams.lpszCaption = TEXT("Test");
//设置自定义图标
mbparams.hInstance = hInstance;
mbparams.lpszIcon = TEXT("USERICON");
//设置帮助回文ID和回调函数
mbparams.dwContextHelpId = 0x100;
mbparams.lpfnMsgBoxCallback = MsgBoxCallback;
//设置显示语言
mbparams.dwLanguageId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
MessageBoxIndirect(&mbparams);
#else//普通消息盒子
MessageBox(NULL, TEXT("Hello World!"), TEXT("Test"), MB_YESNOCANCEL);
#endif
return 0;
}
//帮助回调函数
VOID CALLBACK MsgBoxCallback(LPHELPINFO lpHelpInfo) {
switch(lpHelpInfo->dwContextId) {
case 0x1:
MessageBox(NULL, TEXT("这是0x1的帮助"), TEXT("帮助"), MB_OK);
break;
case 0x100:
MessageBox(NULL, TEXT("这是0x100的帮助"), TEXT("帮助"), MB_OK);
break;
default:
MessageBox(NULL, TEXT("这是默认帮助"), TEXT("帮助"), MB_OK);
break;
}
}
使用的需要修改为对应的预编译值。
1.2 运行结果
- MessageBox:
- MessageBoxEx:
- MessageBoxIndirect:
1.3 函数解释
从来不做微软文档的搬运工,有需要可以看看下面的几个链接。
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxw
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxexw
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxindirectw
0x2 函数细解
2.1 准备
经查阅微软文档或者通过逆向工具可以获知这个函数在user32.dll中。
本例使用的user32.dll(版本:10.0.19041.546)
MessageBox有两个版本分别是MessageBoxA多字节版和MessageBoxW和宽字符版(后文简称为A版和W版)。本文均使用W版进行说明,若无特殊说明的A版大部分内部进行了一个字符串转换在交由W版进行处理。
2.2 查看导出函数
将user32.dll加载到IDA中,查看导出表。
很快就能发现MessageBox,MessageBoxEx和MessageBoxIndirect三个函数两个版本的导出。
值得一提的是圈起来的MessageBoxTimeout,查阅微软官网,但找不到相关文档。这是一个微软未公开的导出函数,先放置一会,很快我们就会再次遇到它了。
2.3 静态分析
2.3.1 程序源码
#include <Windows.h>
#include "resource.h"
#define MSGBOXSP_TIME 0//设置预编译选项
typedef struct _MSGBOXDATA {
MSGBOXPARAMSW mbparams;
BYTE unknown[0X68 - sizeof(MSGBOXPARAMSW)];
}MSGBOXDATA, *LPMSGBOXDATA;
typedef int (WINAPI *MSGTIMEPROC)(HWND, LPCWSTR, LPCWSTR, UINT, WORD, DWORD);
typedef int (_fastcall *MSGWORKERPROC)(LPMSGBOXDATA);
VOID CALLBACK MsgBoxCallback(LPHELPINFO lpHelpInfo);//消息盒子帮助回调函数
int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) {
HMODULE hDllUser32;
#ifdef MSGBOXSP_TIME
MSGTIMEPROC MessageBoxTimeout;
#elif defined(MSGBOXSP_WORKER)
MSGBOXDATA mbdata;
MSGBOXPARAMSW mbparams;
MSGWORKERPROC MessageBoxWorker;
#endif
#ifdef MSGBOXSP_TIME
if((hDllUser32 = LoadLibrary(TEXT("user32.dll"))) != NULL) {
MessageBoxTimeout = (MSGTIMEPROC)GetProcAddress(hDllUser32, "MessageBoxTimeoutW");
MessageBoxTimeout(NULL, TEXT("Hello World!"), TEXT("Test"), MB_OKCANCEL,
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), 3000);
}
#elif defined(MSGBOXSP_WORKER)
if((hDllUser32 = LoadLibrary(TEXT("user32.dll"))) != NULL) {
//这个是10.0.19041.610的user32偏移,昨天系统自动更新了,凑合着吧。
MessageBoxWorker = (MSGWORKERPROC)((DWORD)hDllUser32 + (DWORD)0x0007E5BD);
memset(&mbparams, 0, sizeof(MSGBOXPARAMSW));
mbparams.cbSize = sizeof(MSGBOXPARAMSW);
mbparams.dwStyle = MB_OKCANCEL | MB_USERICON | MB_HELP;
mbparams.lpszText = TEXT("Hello World!");
mbparams.lpszCaption = TEXT("Test");
//设置自定义图标
mbparams.hInstance = hInstance;
mbparams.lpszIcon = TEXT("USERICON");
//设置帮助回文ID和回调函数
mbparams.dwContextHelpId = 0x1;
mbparams.lpfnMsgBoxCallback = MsgBoxCallback;
//设置显示语言
mbparams.dwLanguageId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
memset(&mbdata, 0, sizeof(MSGBOXDATA));
memcpy(&mbdata, &mbparams, sizeof(MSGBOXPARAMSW));
MessageBoxWorker(&mbdata);
}
#endif
return 0;
}
//帮助回调函数
VOID CALLBACK MsgBoxCallback(LPHELPINFO lpHelpInfo) {
switch(lpHelpInfo->dwContextId) {
case 0x1:
MessageBox(NULL, TEXT("这是0x1的帮助"), TEXT("帮助"), MB_OK);
break;
case 0x100:
MessageBox(NULL, TEXT("这是0x100的帮助"), TEXT("帮助"), MB_OK);
break;
default:
MessageBox(NULL, TEXT("这是默认帮助"), TEXT("帮助"), MB_OK);
break;
}
}
程序包含两个部分,分别是MessageBoxTimeout和MessageBoxWorker的调用。
2.3.2 MessageBoxW()
.text:69E7EFA0 ; int __stdcall MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
.text:69E7EFA0 public _MessageBoxW@16
.text:69E7EFA0 _MessageBoxW@16 proc near ; DATA XREF: .text:69E03429↑o
.text:69E7EFA0 ; .text:off_69E99558↓o
.text:69E7EFA0
.text:69E7EFA0 hWnd = dword ptr 8
.text:69E7EFA0 lpText = dword ptr 0Ch
.text:69E7EFA0 lpCaption = dword ptr 10h
.text:69E7EFA0 uType = dword ptr 14h
.text:69E7EFA0
.text:69E7EFA0 mov edi, edi
.text:69E7EFA2 push ebp
.text:69E7EFA3 mov ebp, esp
.text:69E7EFA5 cmp ?gfEMIEnable@@3HA, 0 ; int gfEMIEnable
.text:69E7EFAC jz short loc_69E7EFD0
.text:69E7EFAE mov eax, large fs:18h
.text:69E7EFB4 mov edx, offset ?gdwEMIThreadID@@3PAXA ; void * gdwEMIThreadID
.text:69E7EFB9 mov ecx, [eax+24h]
.text:69E7EFBC xor eax, eax
.text:69E7EFBE lock cmpxchg [edx], ecx
.text:69E7EFC2 test eax, eax
.text:69E7EFC4 jnz short loc_69E7EFD0
.text:69E7EFC6 mov ?gpReturnAddr@@3PAXA, 1 ; void * gpReturnAddr
.text:69E7EFD0
.text:69E7EFD0 loc_69E7EFD0: ; CODE XREF: MessageBoxW(x,x,x,x)+C↑j
.text:69E7EFD0 ; MessageBoxW(x,x,x,x)+24↑j
.text:69E7EFD0 push 0FFFFFFFFh
.text:69E7EFD2 push 0
.text:69E7EFD4 push [ebp+uType]
.text:69E7EFD7 push [ebp+lpCaption]
.text:69E7EFDA push [ebp+lpText]
.text:69E7EFDD push [ebp+hWnd]
.text:69E7EFE0 call _MessageBoxTimeoutW@24 ; MessageBoxTimeoutW(x,x,x,x,x,x)
.text:69E7EFE5 pop ebp
.text:69E7EFE6 retn 10h
.text:69E7EFE6 _MessageBoxW@16 endp
地址69E7EFD0~69E7EFE0就是MessageBoxW的核心代码了。
值得一提的是过去版本的user32.dll,我甚至见过没有上面那一段的,着手分析,看到了那一连串熟手的交换比较线程ID后,便没有了继续的兴趣。
如果你有兴趣,那加油吧。建议想了解的可以先查看一下DLL入口点的代码。
回到核心部分,这一段调用了一个上面那个MessageBoxTimeout。
MessageBoxTimeout函数的资料
经百度百科查证:
https://baike.baidu.com/item/MessageBoxTimeout/7486390?fr=aladdin
编写测试代码:
if((hDllUser32 = LoadLibrary(TEXT("user32.dll"))) != NULL) {
MessageBoxTimeout = (MSGTIMEPROC)GetProcAddress(hDllUser32, "MessageBoxTimeoutW");
MessageBoxTimeout(NULL, TEXT("Hello World!"), TEXT("Test"), MB_OKCANCEL,
MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), 3000);
}
运行结果:
编写测试代码并执行,按钮语言发生变化3秒后窗口自动关闭。
2.3.3 MessageBoxExW()
这还不简单,不懂得往前看!
复制一下反汇编结果。跳过,跳过。
.text:69E7EB00 ; int __stdcall MessageBoxExW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType, WORD wLanguageId)
.text:69E7EB00 public _MessageBoxExW@20
.text:69E7EB00 _MessageBoxExW@20 proc near ; DATA XREF: .text:69E03410↑o
.text:69E7EB00 ; .text:off_69E99558↓o
.text:69E7EB00
.text:69E7EB00 hWnd = dword ptr 8
.text:69E7EB00 lpText = dword ptr 0Ch
.text:69E7EB00 lpCaption = dword ptr 10h
.text:69E7EB00 uType = dword ptr 14h
.text:69E7EB00 wLanguageId = word ptr 18h
.text:69E7EB00
.text:69E7EB00 mov edi, edi
.text:69E7EB02 push ebp
.text:69E7EB03 mov ebp, esp
.text:69E7EB05 push 0FFFFFFFFh
.text:69E7EB07 push dword ptr [ebp+wLanguageId]
.text:69E7EB0A push [ebp+uType]
.text:69E7EB0D push [ebp+lpCaption]
.text:69E7EB10 push [ebp+lpText]
.text:69E7EB13 push [ebp+hWnd]
.text:69E7EB16 call _MessageBoxTimeoutW@24 ; MessageBoxTimeoutW(x,x,x,x,x,x)
.text:69E7EB1B pop ebp
.text:69E7EB1C retn 14h
.text:69E7EB1C _MessageBoxExW@20 endp
2.3.4 MessageBoxIndirectW()
打开MessageBoxIndirectW(),发现了一个不通过MessageBoxTimeoutW()创建消息盒子的函数了,而是使用了更底层的MessageBoxWorker()。
废话不多说,上反汇编代码:
.text:69E7EC70 ; int __stdcall MessageBoxIndirectW(const MSGBOXPARAMSW *lpmbp)
.text:69E7EC70 public _MessageBoxIndirectW@4
.text:69E7EC70 _MessageBoxIndirectW@4 proc near ; DATA XREF: .text:69E0341A↑o
.text:69E7EC70 ; .text:off_69E99558↓o
.text:69E7EC70
.text:69E7EC70 var_70 = _MSGBOXDATA ptr -70h
.text:69E7EC70 var_4 = dword ptr -4
.text:69E7EC70 lpmbp = dword ptr 8
.text:69E7EC70
.text:69E7EC70 mov edi, edi
.text:69E7EC72 push ebp
.text:69E7EC73 mov ebp, esp
.text:69E7EC75 and esp, 0FFFFFFF8h
.text:69E7EC78 sub esp, 70h
.text:69E7EC7B mov eax, ___security_cookie
.text:69E7EC80 xor eax, esp
.text:69E7EC82 mov [esp+70h+var_4], eax
.text:69E7EC86 push esi
.text:69E7EC87 mov esi, [ebp+lpmbp]
.text:69E7EC8A lea eax, [esp+74h+var_70]
.text:69E7EC8E push edi ; struct _MSGBOXDATA *
.text:69E7EC8F push 68h ; 'h' ; Size
.text:69E7EC91 push 0 ; Val
.text:69E7EC93 push eax ; Dst
.text:69E7EC94 call _memset
.text:69E7EC99 add esp, 0Ch
.text:69E7EC9C lea edi, [esp+78h+var_70]
.text:69E7ECA0 cmp ?gfEMIEnable@@3HA, 0 ; int gfEMIEnable
.text:69E7ECA7 push 0Ah
.text:69E7ECA9 pop ecx
.text:69E7ECAA rep movsd
.text:69E7ECAC jz short loc_69E7ECD0
.text:69E7ECAE mov eax, large fs:18h
.text:69E7ECB4 mov edx, offset ?gdwEMIThreadID@@3PAXA ; void * gdwEMIThreadID
.text:69E7ECB9 mov ecx, [eax+24h]
.text:69E7ECBC xor eax, eax
.text:69E7ECBE lock cmpxchg [edx], ecx
.text:69E7ECC2 test eax, eax
.text:69E7ECC4 jnz short loc_69E7ECD0
.text:69E7ECC6 mov ?gpReturnAddr@@3PAXA, 1 ; void * gpReturnAddr
.text:69E7ECD0
.text:69E7ECD0 loc_69E7ECD0: ; CODE XREF: MessageBoxIndirectW(x)+3C↑j
.text:69E7ECD0 ; MessageBoxIndirectW(x)+54↑j
.text:69E7ECD0 lea ecx, [esp+78h+var_70] ; struct _MSGBOXDATA *
.text:69E7ECD4 call ?MessageBoxWorker@@YGHPAU_MSGBOXDATA@@@Z ; MessageBoxWorker(_MSGBOXDATA *)
.text:69E7ECD9 mov ecx, [esp+78h+var_4]
.text:69E7ECDD pop edi
.text:69E7ECDE pop esi
.text:69E7ECDF xor ecx, esp
.text:69E7ECE1 call @__security_check_cookie@4 ; __security_check_cookie(x)
.text:69E7ECE6 mov esp, ebp
.text:69E7ECE8 pop ebp
.text:69E7ECE9 retn 4
.text:69E7ECE9 _MessageBoxIndirectW@4 endp
地址:69E7EC86 ~ 69E7ECAA 和 69E7ECD0 ~ 69E7ECD4,是本函数的核心代码。
MSGBOXDATA是微软未公开的一个结构,大小为0x68字节。
MSGBOXPARAMS是微软已经公开的一个结构,大小为0x28字节。
程序把MSGBOXDATA置为0后把MSGBOXPARAMS的内容复制到MSGBOXDATA的头部,最后直接传给了MessageBoxWorker()。
虽然暂时不知道MSGBOXDATA的全部内容,但是可以肯定的是MSGBOXDATA开头部分一定是一个MSGBOXPARAMSW结构。而且不存在A版的MSGBOXDATA。
我们可以先把脑海里的MSGBOXDATA用C语言表示出来。如下:
typedef struct _MSGBOXDATA {
MSGBOXPARAMSW mbparams;
BYTE unknown[0X68 - sizeof(MSGBOXPARAMSW)];
}MSGBOXDATA, *LPMSGBOXDATA;
MessageBoxWorker()是微软的一个未导出函数,从反汇编的结果可以看出是一个使用了_fastcall调用约束的函数,参数只有一个指向MSGBOXDATA的指针,返回值同MessageBox()。综上可得:
typedef int (_fastcall *MSGWORKERPROC)(LPMSGBOXDATA);
如何获取MessageBoxWorker()的函数地址?
一个导出函数的地址我们可以通过GetProcAddress()获取,但是未导出的函数该怎么处理?
只需要使用 模块基址(等于模块句柄)+ 函数偏移 就可以得到函数地址。
一种能在一定范围内提高兼容性的方案:
- 获取user32的模块句柄。
- 从模块句柄开始搜索MessageBoxWorker()的某个特征。
- 得到MessageBoxWorker()的偏移并与模块句柄相加。
附:在MessageBoxTimeoutW()和MessageBoxIndirectW()内部调用了这个函数且上一条指令都是lea ecx,[ebp+x]指令。
这个文章里由于已经进行了静态且不需要什么兼容性,就直接获取函数偏移。
测试程序:
if((hDllUser32 = LoadLibrary(TEXT("user32.dll"))) != NULL) {
//这个是10.0.19041.610的user32偏移,昨天系统自动更新了,凑合着吧。
MessageBoxWorker = (MSGWORKERPROC)((DWORD)hDllUser32 + (DWORD)0x0007E5BD);
memset(&mbparams, 0, sizeof(MSGBOXPARAMSW));
mbparams.cbSize = sizeof(MSGBOXPARAMSW);
mbparams.dwStyle = MB_OKCANCEL | MB_USERICON | MB_HELP;
mbparams.lpszText = TEXT("Hello World!");
mbparams.lpszCaption = TEXT("Test");
//设置自定义图标
mbparams.hInstance = hInstance;
mbparams.lpszIcon = TEXT("USERICON");
//设置帮助回文ID和回调函数
mbparams.dwContextHelpId = 0x1;
mbparams.lpfnMsgBoxCallback = MsgBoxCallback;
//设置显示语言
mbparams.dwLanguageId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
memset(&mbdata, 0, sizeof(MSGBOXDATA));
memcpy(&mbdata, &mbparams, sizeof(MSGBOXPARAMSW));
MessageBoxWorker(&mbdata);
}
运行结果:
运行结果我用了嵌入汇编,请勿在意。只是懒得重新截图而已。
图标成功被更改,帮助按钮成功使用。
0x3 总结
以上就是第一部分的全部内容,本来这篇文章我是打算一次性全部发布的,我也写了一个WORD文档但是发现全部分析下来实在太长了,只好分成多部分进行了,其他部分就期待后面的发布吧。
注意: 细心的人一定发现了前面的程序有个挺明显的BUG,但为了防止有人没有发现,在这里说明一下,本文除了MessageBoxEx外没有一个程序成功更改了按钮文本语言。
原因是什么呢?后面的部分会说明的。欢迎各位与我交流哦。
!!第一次发布文章,有很多不清楚的地方,如果有什么不对或者违反规定的地方,请第一时间联系作者哦。