增加你的记事本--给记事本加上文本预览功能
现在记事本是越来越鸡肋了,随便找个文本编辑器都能把它给替代了,为了增加记事本在我的硬盘上的存活时间,我决定给它增加点特色功能。现在PHOTOSHOP等图形处理软件都能在图像预览,不妨我也给记事本加上个文本预览...

一般的打开/保存文件对话框是这样的(图1):

再看记事本的打开/保存文件对话框(图2):

多了一个编码的选项。这说明记事本定制了模板。在OPENFILENAME结构中:
Flags  字段如果指明OFN_ENABLEHOOK|OFN_ENABLETEMPLATE这两个选项则可以自行定制文件对话框的行为,包括消息处理等操作。
lpfnHook  字段指向的是消息处理的过程。
思路:
明白了上面这两点,思路就明确了,我们在lpfnHook指向的过程中找到处理WM_NOTIFY消息的地方,在记事本处理这个消息之前先跳转到我们事先准备好的流程中,在我们自己的流程里面进行文本预览的操作,完成之后再跳回去让记事本继续处理,这样就完成了整个过程。

操作流程:

1.先找到lpfnHook指向的过程地址。OD载入记事本(建议先从系统目录复制一份出来再进行操作,以防不测),在命令行插件中下断点:bp GetOpenFileNameW (记事本是用UNICODE方式编译的,断这个API就行了),F9运行记事本,用记事本打开一个文件。会在浏览文件的时候被OD断在系统领空。这时查看堆栈窗口,数据如下:

代码:
0007FB48   01002D89  /CALL 到 GetOpenFileNameW 来自 NOTEPAD.01002D83
0007FB4C   0100A680  \pOpenFileName = NOTEPAD.0100A680
Ctrl+G 来到01002D89,也就是调用这个函数的地方。往上看,找到调用GetOpenFileName的代码:
代码:
01002D3D  |.  68 80A60001   push    0100A680                         ; /pOpenFileName = NOTEPAD.0100A680
01002D42  |.  A3 B0A60001   mov     dword ptr [100A6B0], eax         ; |
01002D47  |.  C705 8CA60001>mov     dword ptr [100A68C], 0100A5E0    ; |
01002D51  |.  C705 BCA60001>mov     dword ptr [100A6BC], 010013C4    ; |UNICODE "txt"
01002D5B  |.  C705 B4A60001>mov     dword ptr [100A6B4], 881064      ; |
01002D65  |.  C705 98A60001>mov     dword ptr [100A698], 1           ; |
01002D6F  |.  C705 C8A60001>mov     dword ptr [100A6C8], 010013A0    ; |UNICODE "NpEncodingDialog"
01002D79  |.  C705 C4A60001>mov     dword ptr [100A6C4], 01002452    ; |
01002D83  |.  FF15 D8120001 call    dword ptr [<&comdlg32.GetOpenFil>; \GetOpenFileNameW
01002D89  |.  85C0          test    eax, eax
由于GetOpenFileName的参数是一个结构体的指针(不要问我怎么知道的,查下MSDN),所以离Call GetOpenFileName最近的
代码:
01002D3D  |.  68 80A60001   push    0100A680 
0100A680就是结构体OPENFILENAME的首地址,通过查MSDN可以知道lpfnHook字段在偏移0x44的地方,也就是
100A6C4 = 0100A680 + 0x44
从上面的代码找到给它赋值的代码:
代码:
01002D79  |.  C705 C4A60001>mov     dword ptr [100A6C4], 01002452    ; |
不用怀疑,01002452就是我们要找的过程地址了,Ctrl+G跳到01002452。

2.在lpfnHook指向的处理过程中找到处理WM_NOTIFY消息的地方,慢慢往下找。找到下面的代码处:
代码:
010025EB  |> \817F 08 A6FDF>cmp     dword ptr [edi+8], -25A          ;  Case 4E (WM_NOTIFY) of switch 0100246C
010025F2  |.  0F85 01010000 jnz     010026F9
010025F8  |.  8D85 F4FDFFFF lea     eax, dword ptr [ebp-20C]
在OD的提示下,很容易就知道,如果消息是WM_NOTIFY,那么就会跳到010025EB来进行处理。

3.编写补丁代码(path code)
有两种方案,一是添加区段,在新增的区段里写代码。二是用第三方开发工具来写函数,利用DLL导出函数,再在程序里调用。
显然第二种方式比较灵活,对PE文件的path操作比较少。缺点就是运行时要多带一个dll不太好看。这里我们选择第二种方案。函数代码如下:
代码:
BOOL NEAR CALLBACK HandleNotify(HWND hDlg,LPOFNOTIFY pofn)
{
  WCHAR wsFilePath[MAX_PATH] = {0};
  WCHAR  wsFileTitle[MAX_FNAME_LEN] = {0};
  
  switch(pofn->hdr.code)
  {
  case CDN_SELCHANGE: //文件名选择更改消息
    {
      CommDlg_OpenSave_GetFilePath(GetParent(hDlg), wsFilePath, MAX_PATH);  //获取选择的文件名(完整路径名称)
      
      GetFileTitle(wsFilePath, wsFileTitle, MAX_FNAME_LEN);
      SetDlgItemText(hDlg, IDC_ST_FILENAME, wsFileTitle);
      
      DWORD  dwFileAttr  =  GetFileAttributes(wsFilePath);
      
      if (!(dwFileAttr & FILE_ATTRIBUTE_DIRECTORY))  //如果选中的是文件
      {
        
        
        BY_HANDLE_FILE_INFORMATION  stFileInfo;
        HANDLE  hFile  =  CreateFile(wsFilePath, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        GetFileInformationByHandle(hFile, &stFileInfo);
        DWORD  dwMapSize  =  stFileInfo.nFileSizeLow > 1024?1024:stFileInfo.nFileSizeLow;

        if (!dwMapSize)  //如果文件大小为零
        {
          SetDlgItemText(hDlg, IDC_EDTPREV, TEXT(""));
          break;
        }

        HANDLE  hFileMap  =  CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, dwMapSize, NULL);
        CloseHandle(hFile);
        LPWSTR  lpVoid  =  (LPWSTR)MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, dwMapSize);
        CloseHandle(hFileMap);
        LPSTR  lpMem;
        INT  nCount;
        
        if (0xFEFF  ==  lpVoid[0])  //如果是UNICODE文本文件
        {
          nCount  =  WideCharToMultiByte(CP_ACP, 0, lpVoid + 1, dwMapSize/sizeof(WCHAR) - 1, NULL, 0, NULL, NULL);
          lpMem  =  (LPSTR)VirtualAlloc(NULL, nCount + 1, MEM_COMMIT, PAGE_READWRITE);
          WideCharToMultiByte(CP_ACP, 0, lpVoid + 1, dwMapSize/sizeof(WCHAR), lpMem, nCount, NULL, NULL);
        }
        else
        {
          nCount  =  dwMapSize + sizeof(CHAR);
          lpMem  =  (LPSTR)VirtualAlloc(NULL, nCount, MEM_COMMIT, PAGE_READWRITE);
          lpMem[nCount]  =  '\0';
          memcpy(lpMem, lpVoid, nCount);
        }
        
        UnmapViewOfFile(lpVoid);
        
        for (INT i = 0; i < (nCount - 1); i++)
        {
          if (lpMem[i] == 0)
            lpMem[i]  =  0x20;
        }
        
        SetDlgItemTextA(hDlg, IDC_EDTPREV, (LPSTR)lpMem);
        
        VirtualFree(lpMem, nCount, MEM_RELEASE);
        
      }
      else if (dwFileAttr & FILE_ATTRIBUTE_DIRECTORY)  //如果是目录
      {
        SetDlgItemText(hDlg, IDC_EDTPREV, TEXT(""));
      }
    }
    break;
  }
  return TRUE;
}
4.修改文件
建立一个DLL工程,把上面的函数导出,再来对PE文件进行操作:
代码:
010025EB  |> \817F 08 A6FDF>cmp     dword ptr [edi+8], -25A          ;  Case 4E (WM_NOTIFY) of switch 0100246C
010025F2  |.  0F85 01010000 jnz     010026F9
010025F8  |.  8D85 F4FDFFFF lea     eax, dword ptr [ebp-20C]
修改为:
代码:
010025EB     /E9 5A610000   jmp     0100874A
010025F0     |90            nop
010025F1     |90            nop
010025F2     |90            nop
010025F3     |90            nop
010025F4     |90            nop
010025F5     |90            nop
010025F6     |90            nop
010025F7     |90            nop
跳过来先执行我们的补丁代码:
代码:
0100874A      50            push    eax
0100874B      8B45 14       mov     eax, dword ptr [ebp+14]
0100874E      50            push    eax
0100874F      8B45 08       mov     eax, dword ptr [ebp+8]
01008752      50            push    eax
01008753      FF15 40300101 call    dword ptr [<&PrevText.HandleNotify>]   ;  PrevText.HandleNotify
01008759      58            pop     eax
0100875A      58            pop     eax
0100875B      58            pop     eax
0100875C      817F 08 A6FDF>cmp     dword ptr [edi+8], -25A
01008763    ^ 0F85 909FFFFF jnz     010026F9
01008769    ^ E9 8A9EFFFF   jmp     010025F8
保存所有修改,打开文件试试看!

5.最后的完善,进行文件保存操作的时候,也可以看到文件预览框了。但是没有看到文件内容。不好,有预览框竟然不能看内容,不和谐!
下面进行最后的完善:
下断bp GetSaveOpenFileNameW
找到给lpfnHook赋值的地方:
代码:
01002C58  |.  C705 B4A60001>mov     dword ptr [100A6B4], 888866
01002C62  |.  C705 C8A60001>mov     dword ptr [100A6C8], 010013A0    ;  UNICODE "NpEncodingDialog"
01002C6C  |.  C705 C4A60001>mov     dword ptr [100A6C4], 01001A28  ;就是这句
01002C76  |.  C705 8CA60001>mov     dword ptr [100A68C], 0100A540
01002C80  |.  C705 BCA60001>mov     dword ptr [100A6BC], 010013C4    ;  UNICODE "txt"
Ctrl + G来到01001A28,处理消息是一系列case语句,由于保存对话框没有处理WM_NOTIFY消息,所以我们只能在default语句的地方做修改了
代码:
01001A4C  |.  48            dec     eax
01001A4D  |.  0F85 1F010000 jnz     01001B72  ;这句是处理default的
让它跳转到,我们的代码处进行第二次判断
代码:
01001A4C  |.  48               dec     eax
01001A4D      0F85 1D6D0000    jnz     01008770  ;跳到我们的处理流程
代码:
01008770      8B45 0C          mov     eax, dword ptr [ebp+C]  ;让 eax = uMsg
01008773      83E8 4E          sub     eax, 4E      ;WM_NOTIFY消息的值是0x004E
01008776      75 0E            jnz     short 01008786    ;如果不是WM_NOTIFY消息就跳走,否则先进行预览。
01008778      8B45 14          mov     eax, dword ptr [ebp+14]
0100877B      50               push    eax
0100877C      8B45 08          mov     eax, dword ptr [ebp+8]
0100877F      50               push    eax
01008780      FF15 40300101    call    dword ptr [<&PrevText.HandleNoti>;  PrevText.HandleNotify
01008786      33C0             xor     eax, eax
01008788    ^ E9 0294FFFF      jmp     01001B8F
最后保存文件,再看看保存文件对话框,也可以进行内容预览了。


6.补充说明
要得到上面图中的效果,我们要先修改记事本的资源,要手工添加3个控件用于信息的显示,两个静态控件和一个文本框。
我是使用Restorator导出资源后手动修改的,调整位置的时候要有点小技巧。
在附件中包含了修改好的.rc资源文件,直接用Restorator导入替换原来的NPENCODINGDIALOG对话框再进行上面的操作就可以看到效果了。
祝大家玩的愉快!
上传的附件 工程源码和对话框资源.rar
增加了文本预览功能的记事本.rar