使用工具:PEid, Stud_PE, ResourceHacker, OllyDbg


0. 背景

  SHA-1是“安全散列算法”的简称,该算法预先给定5个32-bit的整数(下称为初始化
常数),用待加密的信息串对其进行一定变换,最后联成160-bit的散列值输出。正统
SHA-1算法的初始化常数是(C语言形式表示):

      0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0

但有的时候可能有特殊需求,希望使用别的5个数值作为初始化常数来进行计算,这时网
上普遍流行的Hash计算器就无能为力了。在本文中,将尝试给一个Hash计算器增加自定义
这些常数的功能。

  原软件下载地址:http://www.pediy.com/tools/Cryptography/Hash/HashCalc.rar



1. PE-DIY

  这个软件用PEid检查是VC6写的程序,在上述下载地址的同一页面还有另外两款Hash
计算器,但都加了Aspack的壳,这也是选择本程序下手的主要原因。

  我们打算在主界面上新建一个按钮,按下此按钮弹出一个对话框,可以在其中进行修
改SHA-1常数的功能,具体情形如下图所示:


图中上部是程序主界面,标题为“Custom”的按钮就是我们新增加的,按下此按钮将弹出
一个如下部所示的对话框,在5个文本框中可以分别输入5个初始化常数,按下“Change”
按钮将文本框中的新数值写入到原程序并关闭对话框,按下“Use Default”按钮将把文
本框中的数值设置为默认值(0x67452301等)——这是为了在不经意间把数据改乱时可以
快速恢复到默认的状态,而按下“Cancel”按钮则直接关闭对话框,放弃写入新数值。此
外,对话框初始化时会将程序中这些常数的现行值读入并显示到文本框中,便于核对。

  下面来具体实现这些功能。

   (1) 增加所需资源

  用ResHacker打开原程序。在主界面上增加一个按钮不成问题,找到代表主界面的对
话框,直接在上面插入一个按钮并对应调整相关控件的尺寸就行了。关键是添加对话框。
ResHacker有个缺点,只能从一个现有的.res文件中添加对话框,而不能白手起家新建一
个对话框。如果手边没有创建资源的工具,这还真是件恼人的事。幸好还可以用变通的办
法:在原程序中随便找一个对话框资源,去掉原来的控件、调整大小、新增自己的控件等
等,把它变成我们的新对话框的样子,完了之后选择“另存为.res文件”,下面就从这个
文件中导入新的对话框。这里要注意的是,对话框的资源ID直接在资源脚本的界面是改不
了的,改了这里只会导致脚本编译无法通过。要改这个ID,只有通过“操作”菜单中选择
“重命名资源”才可以输入新的ID。等导入新对话框工作完成以后,还必须点击“编译脚
本”按钮,这样在保存文件的时候才能把新加的资源信息写到原文件中去。

  改动过的原程序主界面资源脚本如下:

============================================================
102 DIALOGEX 0, 0, 256, 304
STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_CLIENTEDGE | WS_EX_APPWINDOW
CAPTION "HashCalc"
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
FONT 8, "MS Sans Serif"
{
   /* 此部分略 */
   CONTROL "", 1011, EDIT, ES_LEFT | ES_AUTOHSCROLL | ES_READONLY | WS_CHILD | WS_VISIBLE | WS_BORDER, 63, 97, 145, 12 
   /* 改动这一句,原来的186改成现在的145,即缩小SHA-1文本框的宽度 */
   /* 此部分略 */
   CONTROL "Custom...", 5031, BUTTON, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 211, 97, 37, 12
   /* 新增加这一句,即添加新按钮 */ 
}
============================================================

  新增加的对话框资源脚本如下:

============================================================
500 DIALOGEX 0, 0, 256, 86
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_CLIENTEDGE
CAPTION "Custom SHA-1 Constants"
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
FONT 8, "MS SANS SERIF"
{
   CONTROL "", 501, EDIT, ES_LEFT | ES_NUMBER | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 51, 4, 107, 14 
   CONTROL "", 502, EDIT, ES_LEFT | ES_NUMBER | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 51, 20, 107, 14 
   CONTROL "", 503, EDIT, ES_LEFT | ES_NUMBER | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 51, 36, 107, 14 
   CONTROL "", 504, EDIT, ES_LEFT | ES_NUMBER | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 51, 52, 107, 14 
   CONTROL "", 505, EDIT, ES_LEFT | ES_NUMBER | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 51, 68, 107, 14 
   CONTROL "Const.1", -1, STATIC, SS_LEFT | WS_CHILD | WS_VISIBLE | WS_GROUP, 13, 4, 32, 14 
   CONTROL "Const.2", -1, STATIC, SS_LEFT | WS_CHILD | WS_VISIBLE | WS_GROUP, 13, 20, 32, 14 
   CONTROL "Const.3", -1, STATIC, SS_LEFT | WS_CHILD | WS_VISIBLE | WS_GROUP, 13, 36, 32, 14 
   CONTROL "Const.4", -1, STATIC, SS_LEFT | WS_CHILD | WS_VISIBLE | WS_GROUP, 13, 52, 32, 14 
   CONTROL "Const.5", -1, STATIC, SS_LEFT | WS_CHILD | WS_VISIBLE | WS_GROUP, 13, 68, 32, 14 
   CONTROL "Change", 520, BUTTON, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 168, 4, 82, 14 
   CONTROL "Use Default", 521, BUTTON, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 168, 20, 82, 14 
   CONTROL "Cancel", 522, BUTTON, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 168, 36, 82, 14 
}
============================================================

这里把输入新常数的文本框设置了ES_NUMBER风格,因而只能输入十进制数值。这其实是
有些“偷懒”的成分的,本来最自然的是设置成允许输入16进制数值,但那样的话还要涉
及控件的子类化,增加额外的代码。

  (2) 增加新资源的响应代码

  首先自然是在原文件中找一块空间来存放新增的代码,由于预先无法估计需要增加的
代码有多少,为了保险起见,在文件尾新建一个区段作为添加代码的空间。用Stud_PE可
以完成这个功能,也可以用LordPE。这里,把新区段的VirtualSize和RawSize都设置为
0x1000(4KB)。

  让我们考虑如何使代码实现我们预期的效果,按下主界面上的“Custom...”按钮等
于向主窗口发送wParam为5031的WM_COMMAND消息,主窗口收到这个消息后应该调用
DialogBoxParam建立ID等于500的对话框,取得文本框中的数值和向文本框中设置数值可
以使用G(S)etDlgItemInt,最后用EndDialog关闭对话框。

  对于这里用到的API函数,我们原则上尽量考虑从原文件的导入表中寻找已有的,而
不是新增导入函数。因为导入表是块难啃的骨头,修改它不象修改区段信息那么简单,不
能直接在原来的导入表中插入新的导入函数信息,象Stud_PE等工具对于新增导入函数的
请求都是采用新建一个区段来存放导入信息的方法,不仅显得拖泥带水,而且一不小心就
会误操作。

  用Stud_PE查看原文件导入表可以找到EndDialog,但却没有DialogBoxParam,只有一
个十分“鸡肋”的CreateDialogIndirectParam,如果要用它建立对话框,如何把资源调
到内存还是个问题,何况这样建立的对话框还不能用EndDialog关闭。用CreateWindowEx
也不行,这样无法把定义好的对话框资源当作一个参数提供。幸运的是,原导入表中还有
GetModuleHandle和GetProcAddress,这可是两个至关重要的函数!这意味着可以先获取
user32.dll等导入dll的句柄,然后再获得其中DialogBoxParam等函数的地址,以间接的
方式调用API!

  接下来就是找到原程序中对WM_COMMAND消息的处理代码,以便在此处用我们的代码进
行挂钩。用OllyDbg载入程序,F9运行后在“窗口”栏目找到主窗口,对它设置
WM_COMMAND消息断点。按下“Custom...”按钮后中断在如下地方:(Alt+F9)


====================以下是代码==================

0044E835  |.  8945 FC       mov     [ebp-4], eax
0044E838  |>  8B45 FC       mov     eax, [ebp-4]
0044E83B  |.  5E            pop     esi
0044E83C  |.  C9            leave
0044E83D  \.  C2 0C00       retn    0C
0044E840  /$  B8 14814500   mov     eax, 00458114
0044E845  |.  E8 2636FCFF   call    00411E70
0044E84A  |.  83EC 54       sub     esp, 54
0044E84D  |.  8365 F0 00    and     dword ptr [ebp-10], 0
0044E851  |.  53            push    ebx
0044E852  |.  8B5D 08       mov     ebx, [ebp+8]
0044E855  |.  56            push    esi
0044E856  |.  57            push    edi
0044E857  |.  81FB 11010000 cmp     ebx, 111                         ;  Switch (cases 6..111)
0044E85D  |.  8BF9          mov     edi, ecx
0044E85F  |.  75 18         jnz     short 0044E879
0044E861  |.  FF75 10       push    dword ptr [ebp+10]               ;  Case 111 (WM_COMMAND) of switch 0044E857
0044E864  |.  8B07          mov     eax, [edi]
0044E866  |.  FF75 0C       push    dword ptr [ebp+C]

====================以上是代码==================


程序中断在44E835处,再往下看就出现了眼熟的常数111,看OD给它加的注释,这十有八
九应该就是处理WM_COMMAND消息的分支,这次尝试在44E861处下断,再按“Custom...”
按钮,果然中断了!那么我们就选择这里作为挂钩的地方吧:用一个jmp语句跳到我们的
代码处。由于长jmp有5个字节,将覆盖掉原有的push [ebp+10]和mov eax, [edi]两句,
这两句应该被转移到我们的代码中。好了,废话不多说了,将这里改成打算去的地方,我
是把它改成jmp 474800——新区段是474000到474FFF——而在474800处写入:


====================以下是代码==================

00474800    817D 0C A713000>cmp     dword ptr [ebp+C], 13A7          ; 判断按下的是不是“Custom”按钮,[ebp+0C]中是wParam
00474807    75 5A           jnz     short 00474863                   ; 不是,则没事发生
00474809    60              pushad                                   ; 保存现场
0047480A    68 00404700     push    00474000                         ; ASCII "kernel32.dll"
0047480F    FF15 CC914500   call    [<&KERNEL32.GetModuleHandleA>]   ; kernel32.GetModuleHandleA
00474815    A3 004F4700     mov     [474F00], eax
0047481A    68 0D404700     push    0047400D                         ; ASCII "user32.dll"
0047481F    FF15 CC914500   call    [<&KERNEL32.GetModuleHandleA>]   ; kernel32.GetModuleHandleA
00474825    A3 044F4700     mov     [474F04], eax                    ; 获取user32.dll句柄
0047482A    6A 00           push    0
0047482C    FF15 CC914500   call    [<&KERNEL32.GetModuleHandleA>]   ; kernel32.GetModuleHandleA
00474832    A3 084F4700     mov     [474F08], eax                    ; 获取hInstance作为下面的DialogBoxParam的参数
00474837    68 18404700     push    00474018                         ; ASCII "DialogBoxParamA"
0047483C    FF35 044F4700   push    dword ptr [474F04]
00474842    FF15 80914500   call    [<&KERNEL32.GetProcAddress>]     ; kernel32.GetProcAddress
00474848    6A 00           push    0
0047484A    68 6D484700     push    0047486D
0047484F    FFB5 D4000000   push    dword ptr [ebp+D4]               ; 父窗口句柄
00474855    68 F4010000     push    1F4
0047485A    FF35 084F4700   push    dword ptr [474F08]
00474860    FFD0            call    eax                              ; 调用DialogBoxParamA
00474862    61              popad                                    ; 调用完毕恢复现场
00474863    FF75 10         push    dword ptr [ebp+10]               ; 执行被覆盖的代码
00474866    8B07            mov     eax, [edi]
00474868  - E9 F99FFDFF     jmp     0044E866                         ; 回到挂钩的下一句
0047486D    55              push    ebp                              ; 窗口过程
0047486E    8BEC            mov     ebp, esp
00474870    8B45 0C         mov     eax, [ebp+C]
00474873    3D 10010000     cmp     eax, 110                         ; WM_INITDIALOG分支,在这里获取一些重要API的地址
00474878    0F85 9F000000   jnz     0047491D
0047487E    68 28404700     push    00474028                         ; ASCII "GetDlgItemInt"
00474883    FF35 044F4700   push    dword ptr [474F04]
00474889    FF15 80914500   call    [<&KERNEL32.GetProcAddress>]     ; kernel32.GetProcAddress
0047488F    A3 0C4F4700     mov     [474F0C], eax
00474894    68 36404700     push    00474036                         ; ASCII "SetDlgItemInt"
00474899    FF35 044F4700   push    dword ptr [474F04]
0047489F    FF15 80914500   call    [<&KERNEL32.GetProcAddress>]     ; kernel32.GetProcAddress
004748A5    A3 104F4700     mov     [474F10], eax
004748AA    6A 00           push    0
004748AC    FF35 09C34100   push    dword ptr [41C309]               ; 将SHA-1常数读出并依次显示到5个对话框中
004748B2    68 F5010000     push    1F5
004748B7    FF75 08         push    dword ptr [ebp+8]
004748BA    FF15 104F4700   call    [474F10]                         ; SetDlgItemInt
004748C0    6A 00           push    0
004748C2    FF35 10C34100   push    dword ptr [41C310]
004748C8    68 F6010000     push    1F6
004748CD    FF75 08         push    dword ptr [ebp+8]
004748D0    FF15 104F4700   call    [474F10]
004748D6    6A 00           push    0
004748D8    FF35 17C34100   push    dword ptr [41C317]
004748DE    68 F7010000     push    1F7
004748E3    FF75 08         push    dword ptr [ebp+8]
004748E6    FF15 104F4700   call    [474F10]
004748EC    6A 00           push    0
004748EE    FF35 1EC34100   push    dword ptr [41C31E]
004748F4    68 F8010000     push    1F8
004748F9    FF75 08         push    dword ptr [ebp+8]
004748FC    FF15 104F4700   call    [474F10]
00474902    6A 00           push    0
00474904    FF35 25C34100   push    dword ptr [41C325]
0047490A    68 F9010000     push    1F9
0047490F    FF75 08         push    dword ptr [ebp+8]
00474912    FF15 104F4700   call    [474F10]
00474918    E9 35010000     jmp     00474A52
0047491D    3D 11010000     cmp     eax, 111                         ; WM_COMMAND分支
00474922    0F85 18010000   jnz     00474A40
00474928    0FB745 10       movzx   eax, word ptr [ebp+10]
0047492C    3D 08020000     cmp     eax, 208                         ; 按下Change按钮
00474931    0F85 83000000   jnz     004749BA
00474937    6A 00           push    0
00474939    6A 00           push    0
0047493B    68 F5010000     push    1F5
00474940    FF75 08         push    dword ptr [ebp+8]
00474943    FF15 0C4F4700   call    [474F0C]                         ; GetDlgItemInt
00474949    A3 09C34100     mov     [41C309], eax                    ; 依次取得文本框中的常数并写入到程序中
0047494E    6A 00           push    0
00474950    6A 00           push    0
00474952    68 F6010000     push    1F6
00474957    FF75 08         push    dword ptr [ebp+8]
0047495A    FF15 0C4F4700   call    [474F0C]
00474960    A3 10C34100     mov     [41C310], eax
00474965    6A 00           push    0
00474967    6A 00           push    0
00474969    68 F7010000     push    1F7
0047496E    FF75 08         push    dword ptr [ebp+8]
00474971    FF15 0C4F4700   call    [474F0C]
00474977    A3 17C34100     mov     [41C317], eax
0047497C    6A 00           push    0
0047497E    6A 00           push    0
00474980    68 F8010000     push    1F8
00474985    FF75 08         push    dword ptr [ebp+8]
00474988    FF15 0C4F4700   call    [474F0C]
0047498E    A3 1EC34100     mov     [41C31E], eax
00474993    6A 00           push    0
00474995    6A 00           push    0
00474997    68 F9010000     push    1F9
0047499C    FF75 08         push    dword ptr [ebp+8]
0047499F    FF15 0C4F4700   call    [474F0C]
004749A5    A3 25C34100     mov     [41C325], eax
004749AA    6A 00           push    0
004749AC    FF75 08         push    dword ptr [ebp+8]
004749AF    FF15 20944500   call    [<&USER32.EndDialog>]            ; USER32.EndDialog
004749B5    E9 9A000000     jmp     00474A54
004749BA    3D 09020000     cmp     eax, 209                         ; 按下Use Default按钮
004749BF    75 6B           jnz     short 00474A2C
004749C1    6A 00           push    0
004749C3    68 01234567     push    67452301
004749C8    68 F5010000     push    1F5
004749CD    FF75 08         push    dword ptr [ebp+8]
004749D0    FF15 104F4700   call    [474F10]                         ; 将文本框中的数值设置为SHA-1的默认值
004749D6    6A 00           push    0
004749D8    68 89ABCDEF     push    EFCDAB89
004749DD    68 F6010000     push    1F6
004749E2    FF75 08         push    dword ptr [ebp+8]
004749E5    FF15 104F4700   call    [474F10]
004749EB    6A 00           push    0
004749ED    68 FEDCBA98     push    98BADCFE
004749F2    68 F7010000     push    1F7
004749F7    FF75 08         push    dword ptr [ebp+8]
004749FA    FF15 104F4700   call    [474F10]
00474A00    6A 00           push    0
00474A02    68 76543210     push    10325476
00474A07    68 F8010000     push    1F8
00474A0C    FF75 08         push    dword ptr [ebp+8]
00474A0F    FF15 104F4700   call    [474F10]
00474A15    6A 00           push    0
00474A17    68 F0E1D2C3     push    C3D2E1F0
00474A1C    68 F9010000     push    1F9
00474A21    FF75 08         push    dword ptr [ebp+8]
00474A24    FF15 104F4700   call    [474F10]
00474A2A    EB 26           jmp     short 00474A52
00474A2C    3D 0A020000     cmp     eax, 20A                         ; 按下Cancel按钮
00474A31    75 1F           jnz     short 00474A52
00474A33    6A 00           push    0
00474A35    FF75 08         push    dword ptr [ebp+8]
00474A38    FF15 20944500   call    [<&USER32.EndDialog>]            ; USER32.EndDialog
00474A3E    EB 14           jmp     short 00474A54
00474A40    83F8 10         cmp     eax, 10                          ; WM_CLOSE分支
00474A43    75 0D           jnz     short 00474A52
00474A45    6A 00           push    0
00474A47    FF75 08         push    dword ptr [ebp+8]
00474A4A    FF15 20944500   call    [<&USER32.EndDialog>]            ; USER32.EndDialog
00474A50    EB 02           jmp     short 00474A54
00474A52    33C0            xor     eax, eax
00474A54    C9              leave
00474A55    C2 1000         retn    10

====================以上是代码==================


写完这些代码后,将改动复制到可执行文件中就行了。

  需要说明的几处地方:

  I) DialogBoxParam需要的两个参数hInstance和hWndOwner,程序中都无法即时提
供,前者还比较好办,只要用参数NULL再调用GetModuleHandle就可以了,但后者就有些
麻烦了,实在想不到有哪个API可以即时获取,在实践中基于一种思想:主窗口句柄需要
用做参数来建立各种控件子窗口,因此它必定会在堆栈的某个地方出现,在OD的
“窗口”查看栏中查看到句柄的值后,再在堆栈中搜索这个值,结果才发现在[ebp+0D4]
的位置保存着一份主窗口句柄。

  II) 为了更改SHA-1常数,必须知道这些常数在代码段的什么位置,换言之就是需要
知道往什么地方写入数据,用OD中“查找常量”功能查找特征常量如0xC3D2E1F0查到的位
置有3处,怎么办呢,这时PEid又发挥作用了,用分析密码学算法插件查到SHA-1算法在地
址41CE56处,于是判断正确的位置是在这个地址往上的一处。当然这需要验证。验证的方
法也很简单,新程序完工后运行一份原程序对同一个字符串算Hash,其他方法算出来的值
都一样,只有SHA-1不同,就证明我们的操作是正确的。

  III) 获取user32.dll等库的句柄需要提供文件名,获取函数地址需要提供函数名,
总之就是需要提供一些字符串,这些字符串也写在新建的区里,实践中是写到474000开始
的地方(新区段的起点),到了写入文件的时候,这些改动也要写进去。

  IV) 调用导入表里的函数方式是间接调用,机器码是FF15,不要错写成直接调用的
E8,譬如在Stud_PE里查到导入表中EndDialog的RVA是459420那么指令应该是
call [459420],实际上很好理解,导入表中怎么可能包含可执行代码呢。(VC生成的程
序好象没有跳板,直接call向导入表中)

  V) 原始汇编方式有个难点就是确定跳转的距离,如果跳转的位置无法预计,最好
都先汇编成长跳转留下5个字节的空间,即使以后改短跳转,空出来的字节填些nop就行,
总比先汇编短跳转后发现必须改长跳转好。

  VI) 最后也是最容易被忽略的一点:由于我们所做的操作是往原来的代码段中读写
数据,改完之后必须再修改PE文件头,把代码段的属性改成可读写,程序才能正常运行。