2010年5月4日星期二

转贴:为记事本增加功能-大小写转换,加解密文件,字数统计。


1)对理解WIN用户界面的messageloop,消息处理过程和PEDIY技巧很有帮助,保存一下
http://bbs.pediy.com/showthread.php?t=111575


目标:给记事本添加接口,并通过菜单来执行自定义的功能。
思路:通过反汇编确定记事本的窗口过程地址,把它替换为我们自己的窗口过程地址,在自己窗口过程中先判断是否是自定义的消息,是则执行相应的操作,不是则把流程转到记事本原来的窗口过程中进行处理。
添加的功能:转换大小写,加解密文本,统计字数。

1.通过OD来找到记事本的窗口过程地址
先拷贝系统的记事本出来一份备用,由于记事本是纯WIN32 SDK编写的,所以创建窗口的方式是调用CreateWindowEx的方式,在创建窗口之前先要调用RegisterClassEx注册窗口类,我们要找的窗口过程地址就在RegisterClassEx的参数中。
OD打开记事本,命令行下断: bp RegisterClassExW 1F9运行程序。程序断在了我们设的断点(图2:
1
2
此时查看堆栈窗口,(3)说明在记事本程序调用RegisterClassExW的代码地址在1004550处。先Alt+B打开断点窗口,删除所有断点。然后在图3中的堆栈窗口中鼠标点击第一行,回车来到调用RegisterClassExW的代码处。
3
此时CPU窗口如图4
4
这里要简单的介绍一下相关的WIN32编程知识:
RegisterClassExW的原型 (MSDN)
ATOM RegisterClassEx(CONST WNDCLASSEX *lpwcx);
它的参数WNDCLASSE是一个结构体:
typedef struct {
UINT cbSize; //结构体的大小
UINT style;
WNDPROC lpfnWndProc; //窗口过程地址,这里存放的就是我们需要的窗口过程地址
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX, *PWNDCLASSEX;
这个函数的作用是注册一个窗口类(注意:这里的类和C++中的类并不是同一个概念)。它的参数是一个指向WNDCLASSEX结构的指针,使用RegisterClassEx之前必须先初始化WNDCLASSEX这个结构体类型的各个参数,函数才能根据结构体中的字段来正确注册窗口类。
好了,现在有了这些前置知识,再来看一下图4中的反汇编代码,call之上的mov语句都是用来给结构本中的成员赋值的语句,最上面的push eax是把指向整个结构体的地址压入堆栈中,供RegisterClassExW使用,而1004524处的lea eax,dword ptr [ebp-0x30]正是把结构体的地址赋值给eax(顺便说一下,在OD中看到的数值都是16进制的)。这样可以知道ebp-0x30是结构体中第一个成员的地址,那么我们要找的窗口过程的地址就是在偏移8字节的地方。简单计算一下:WndProc = ebp-0x30+8=ebp-0x28。再看一下图4中给这个地址赋值的语句是
Mov dword ptr [ebp-0x28], 1003429可以确定,1003429就是主窗口过程的地址。好了,现在通过OD右键->复制->到剪贴板 把这行代码复制下来备用:
01004539 |. C745 D8 29340>mov dword ptr [ebp-28], 01003429 ; |
现在拿出我们的另一件利器VC,新建一个WIN32 DLL工程,在DLL中导出一个函数做为我们自定义的窗口过程,这个窗口过程要在转到记事本原来的窗口过程之前执行。自定义的窗口过程定义如下:
DWORD ExWndProc(CONST DWORD Reserved, HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
写过WIN32程序的朋友都知道,标准的窗口过程定义是:
DWORD CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
我们自定义的窗口过程比标准的窗口过程多了一个CONST DWORD Reserved 参数,这是为什么呢?
由于WndProc函数是一个回调函数(即程序不主动调动,而是由系统在需要的时候进行调用的函数),在WndProc被调用的时候,所需要的数据都已经压栈,其中包括了WndProc的返回地址,我们可以验证一下:在ODCtrl+G跳转到记事本的窗口过程地址01003429,F2下断,F9运行。这时候的堆栈窗口(5):
5
调用窗口过程的时候:
Push lParam
Push wParam
Push uMsg
Push hWnd
Call Wndproc
执行完Call Wndproc之后,在跳转到WndProc地址之前,系统还把 Call WndProc指令的下一句地址也压入了栈中,为的是执行完WndProc之后能跳到这个地址,使程序按流程运行。所以在刚转到WndProc入口的时候,栈中的情况就是
NextAddr ->函数返回地址
hWnd ->窗口句柄
uMsg ->窗口消息
wParam ->附加信息
lParam ->附加信息
由于系统执行我们自定义的窗口过程的时候,上面这5个参数已经自动进栈,所以我们只要把这5个参数当做自定义窗口过程的参数就可以了。第一个参数就是返回地址,我们不需要使用。
我们先给记事本添加菜单,通过菜单来响应我们新增加的功能。我使用的资源修改工具是Restorator 2009 (6),后面跟着的数字是菜单的ID,不能重复。
6
接下来的过程就和正常写程序没什么区别了。我写好的框架如下:
DWORD ExWndProc(CONST DWORD Reserved, HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg) //判断消息
{
case WM_COMMAND: //如果是WM_COMMAND消息则再一步判断是不是我们的菜单发出的
{
switch (LOWORD(wParam)) //判断控件ID是不是我们的
{
case IDR_UPPER: //转换为大写
{
TO_UPPER(hWnd);
}
break;
case IDR_LOWER: //转换为小写
{
TO_LOWER(hWnd);
}
break;
case IDR_ENCRYPT: //加解密文本
{
EnCrypt(hWnd);
}
break;
case IDR_COUNT: //文字计数
{
Count(hWnd);
}
break;
}
}
break;
}

return 0;
}
各个函数的实现如下:
//转换为大写
VOID WINAPI TO_UPPER(HWND hWnd)
{
//读取编辑框句柄
if (g_hEdit = GetDlgItem(hWnd, 0xF))
{
INT len = GetWindowTextLength(g_hEdit); //获取文本长度
if (len > 0)
{
LPTSTR pstrBuffer = new TCHAR[len+2]; //根据文本长度申请合适的内存空间
memset(pstrBuffer, 0, sizeof(TCHAR)*(len+2));
GetWindowText(g_hEdit, pstrBuffer, len+2); //读出全部文本
SendMessage(g_hEdit, EM_SETMODIFY, (WPARAM)TRUE, 0);
CharUpper(pstrBuffer); //把所有文本当作一个字符串直接进行转换
SetWindowText(g_hEdit, pstrBuffer); //转换结果输出到编辑框中
delete []pstrBuffer; //释放申请的内存
}
}
}

//转换为小写,为TO_UPPER函数一样
VOID WINAPI TO_LOWER(HWND hWnd)
{
//读取编辑框句柄
if (g_hEdit = GetDlgItem(hWnd, 0xF))
{
INT len = GetWindowTextLength(g_hEdit);
if (len > 0)
{
LPTSTR pstrBuffer = new TCHAR[len+2];
memset(pstrBuffer, 0, sizeof(TCHAR)*(len+2));
GetWindowText(g_hEdit, pstrBuffer, len+2);
SendMessage(g_hEdit, EM_SETMODIFY, (WPARAM)TRUE, 0);
CharLower(pstrBuffer); //把所有文本当作一个字符串转换为小写
SetWindowText(g_hEdit, pstrBuffer);
delete []pstrBuffer;
}
}
}

//统计字数,为了简单,直接返回字符串的长度,当然也可以复杂一点像Word一样更实用
VOID WINAPI Count(HWND hWnd)
{
if (g_hEdit = GetDlgItem(hWnd, 0xF))
{
INT len = GetWindowTextLength(g_hEdit);
if (len > 0)
{
TCHAR strText[MAX_PATH];
memset(strText, 0, sizeof(TCHAR)*MAX_PATH);
wsprintf(strText, _T("当前总字数为: %d"), len);
MessageBox(hWnd, strText, _T("字数统计"), MB_OK);
}
}
}
文本加解密,这个内容代码比较长,只贴出加密部分的代码,解密是加密的逆运算
switch (uID)
{
case IDC_BTN_ENCRYPT://加密
{
HWND hEncryptEdit = GetDlgItem(hDlg, IDC_EDT_KEY);
INT nKeyLen = GetWindowTextLength(hEncryptEdit);
if (nKeyLen > 0)
{
LPTSTR pstrKey = new TCHAR[nKeyLen+2];
memset(pstrKey, 0, nKeyLen+2);
//获取用户输入的密匙
GetWindowText(hEncryptEdit, pstrKey, nKeyLen+2);
INT len = GetWindowTextLength(g_hEdit);

LPTSTR pstrText = new TCHAR[len+2];
memset(pstrText, 0, len+2);
//获取文本框中的文本
GetWindowText(g_hEdit, pstrText, len+2);
SendMessage(g_hEdit, EM_SETMODIFY, (WPARAM)TRUE, 0);
DWORD dwKey = 0;

for (INT j = 0; j < nKeyLen; j++)
{
//累加用户密匙
dwKey += pstrKey[j];
}
//确保结果在0-32之间
dwKey %= 32;
//循环对编辑框中的每个字符都进行加密,使用减法。解密则使用加法。
for (INT i = 0; i < len; i++)
{
pstrText[i] -= (TCHAR)dwKey;
}
//把加密的结果输出到编辑框中,完成加密
SetWindowText(g_hEdit, pstrText);
delete []pstrText;
delete []pstrKey;
}
}
break;
代码写好之后编译为.dll文件。把这个 dll和拷贝出来的记事本放在同一目录下,用LordPE打开记事本。为记事本添加一个导入函数(7)
7
记得勾上 “总是查看FirstThunk”,并记下ExWndProcFirstThunk值为1301A。再次用OD打开记事本,
先找块地方插入我们自己的窗口处理过程代码。我们在代码段中找,点工具栏上的 M 按钮,打开进程内存。打开记事本的.text(7)
双击进入,找到代码和零区域交汇的地方(8):
8
现在转到反汇编窗口Ctrl+G来到100874C (一般来说找地址要和代码保持一定距离,但是不要太远,否则在保存文件时OD会无法定位数据),写入代码执行我们的窗口过程,再跳回原流程中(9)
9

Jmp 1003429是为了执行完自定义的过程之后再把流程转到正常的窗口处理过程中。
再继续,胜利就在前方了。Ctrl+G直接跳到1004539,把原来的
01004539 |. C745 D8 29340>mov dword ptr [ebp-28], 01003429 ; |
更改为
mov dword ptr [ebp-28], 100874C

现在保存文件,右键->复制到可执行文件->所有修改->全部复制,在弹出的窗口中再右键->保存文件,输入一个不一样文件名保存到ExNotepad.dll同一目录下。
好了,现在打开我们改造过的记事本。测试一下各个功能,是不是很酷了?
小结
通过为记事本添加接口的方式来进行功能扩展很灵活,能实现的功能取决于自己的编程功底和想象力。

沒有留言:

發佈留言