书接上文,我们已经打开了 chtitle.hhp,那么是时候看看程序是如何读取这个文件的了。
先说个题外话开场吧,第一次找到打开 chtitle 时,可能是喜悦冲昏了头脑,
我觉得读取文件的地方一定在不远的下面,于是开始 F7 一步一步跟了下去,
一下午的时间很快过去,在几进几出关键区后,我觉得这不是个办法,
可能 windows 程序的复杂程度超过了我的想象。不过还好,
我想到了 api 应该会有读取文件的地方,从那边定位应该会比较快。这就有了 ReadFile
。
BOOL ReadFile(
[in] HANDLE hFile,
[out] LPVOID lpBuffer,
[in] DWORD nNumberOfBytesToRead,
[out, optional] LPDWORD lpNumberOfBytesRead,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
ReadFile
把文件 handle 和缓冲区地址(lpBuffer
)告诉内核,
告诉它我们要读取哪个文件,并把文件内容放到哪个缓冲区。
我们一样可以在 cutter 的导入表中,找到 ReadFile
,从而去定位它的调用位置:
有两个位置,但是距离很近,我们就先看第一个好了:
cutter 很贴心地向我们指明了缓存区的位置保存在这行的 edx 中:
记下这行的位置 40bd43h
,后面我们需要验证缓冲区中保存的内容。
我们接着 第二部分 来讲,这时我们中断在打开 ctitle.hhp 时:
这时我们使用 Ctrl-g,前往 40bd43h
下断点:
F9 继续程序,很快就会来到刚刚我们下的断点:
edx 的值是 6aec84h
,这个就是缓冲区的地址,注意 bd47
行 edx 的值会有变化,
所以我们要及时观察 edx 的值,不能停在 bd4b
行 call
之前。
我们把内存窗口调整到显示缓冲区的内容,然后让程序执行到 40bd5b
行,
也就是 ReadFile
执行完成。这时我们就可以去检查缓冲区的内容:
我们来对比一下实际文件的内容:
缓冲区确实加载了这段内容,接下来我们要做的就是不断地 F7 来寻找缓冲区的内容被搬到哪里去, 顺带一提,也许 TDW 有读取内存时中断的功能,但是我没试出来,所以只好自己看了, 不过还好这个还算好找:
我们注意到 ecx 的值就是缓冲区的地址,把值放入 al,然后地址自增 1
,
这一定就是读取的位置了。看来这里值得我们仔细研究一下,我们回到 cutter,
cutter 可以很贴心地帮我们找到整块函数:
;-- section..text:
fcn.00401000 (int32_t arg_14h);
; arg int32_t arg_14h @ esp+0x24
0x00401000 push ebx ; [00] -r-x section size 73728 named .text
0x00401001 push esi
0x00401002 push edi
0x00401003 mov esi, ecx
0x00401005 push ebp
0x00401006 xor edi, edi
0x00401008 mov ebx, dword [arg_14h]
0x0040100c cmp ebx, edi
0x0040100e jle 0x401089
0x00401010 cmp dword [esi + 0x200c], 0
0x00401017 jne 0x401069
0x00401019 cmp dword [esi + 0x2008], 0
0x00401020 jne 0x401044
0x00401022 lea ebp, [esi + 4]
0x00401025 mov eax, dword [esi]
0x00401027 push eax
0x00401028 push 0x2000 ; int32_t arg_14h
0x0040102d push 1 ; 1 ; int32_t arg_18h_2
0x0040102f push ebp ; int32_t arg_18h
0x00401030 call fcn.00408c40
0x00401035 add esp, 0x10
0x00401038 mov dword [esi + 0x2008], eax
0x0040103e mov dword [esi + 0x2004], ebp
0x00401044 mov ecx, dword [esi + 0x2004]
0x0040104a dec dword [esi + 0x2008]
0x00401050 mov al, byte [ecx]
0x00401052 inc ecx
0x00401053 mov byte [esi + 0x2010], al
0x00401059 mov dword [esi + 0x200c], 8
0x00401063 mov dword [esi + 0x2004], ecx
0x00401069 add edi, edi
0x0040106b mov al, byte [esi + 0x2010]
0x00401071 test al, 0x80 ; 128
0x00401073 je 0x401078
0x00401075 or edi, 1
0x00401078 add al, al
0x0040107a dec dword [esi + 0x200c]
0x00401080 dec ebx
0x00401081 mov byte [esi + 0x2010], al
0x00401087 jne 0x401010
0x00401089 cmp dword [esi + 0x200c], 0
0x00401090 jne 0x4010e2
0x00401092 cmp dword [esi + 0x2008], 0
0x00401099 jne 0x4010bd
0x0040109b lea ebx, [esi + 4]
0x0040109e mov eax, dword [esi]
0x004010a0 push eax
0x004010a1 push 0x2000 ; int32_t arg_14h
0x004010a6 push 1 ; 1 ; int32_t arg_18h_2
0x004010a8 push ebx ; int32_t arg_18h
0x004010a9 call fcn.00408c40
0x004010ae add esp, 0x10
0x004010b1 mov dword [esi + 0x2008], eax
0x004010b7 mov dword [esi + 0x2004], ebx
0x004010bd mov eax, dword [esi + 0x2004]
0x004010c3 dec dword [esi + 0x2008]
0x004010c9 mov cl, byte [eax]
0x004010cb inc eax
0x004010cc mov byte [esi + 0x2010], cl
0x004010d2 mov dword [esi + 0x200c], 8
0x004010dc mov dword [esi + 0x2004], eax
0x004010e2 mov eax, edi
0x004010e4 pop ebp
0x004010e5 pop edi
0x004010e6 pop esi
0x004010e7 pop ebx
0x004010e8 ret 4
但是还是很难理解,经过不停的 F7 之后,我对这个 al 最终如何处理的还是一头雾水,
而且一直看到 40107a
和 401080
像倒计时一样一直在 876543210 这样变化,
像倒计时一样。不过我觉得我们可以参照以前的经验,
看看哪里引用了这个 fcn.00401000 (int32_t arg_14h)
:
很多地方用到,我们看第一个好了:
0x00401136 push 8 ; 8 ; int32_t arg_14h
0x00401138 mov byte [ebx - 4], al
0x0040113b mov ecx, dword [esi]
0x0040113d call fcn.00401000
0x00401142 mov dword [var_10h_2], eax
0x00401146 mov dword [var_10h], 0
0x0040114e fild qword [esp + 0x10]
0x00401152 fmul qword [esp + 0x18]
0x00401156 call flirt.ftol
0x0040115b push 8 ; 8 ; int32_t arg_14h
0x0040115d mov byte [ebx - 3], al
0x00401160 mov ecx, dword [esi]
0x00401162 call fcn.00401000
每次调用前都 push 了一个 8
进去,如果你是一个对汇编很熟悉的人,
可能已经意识到什么,不过对于我这种汇编新手,这还意味不了什么,
不过 cutter 还有一个贴心功能,显示反编译后的代码,方便我这种人理解逻辑,
切换反编译器的 tab 在窗口右下侧:
我们点击反编译器,cutter 会自动显示当前函数反编译后的代码, 我们可以看到这样一个循环:
edi = 0x100;
...
do {
ecx = *(esi);
ebx++;
eax = fcn_00401000 (8);
ebx++;
...
ebx++;
ebx++;
...
*((ebx - 4)) = al;
ecx = *(esi);
eax = fcn_00401000 (8);
...
*((ebx - 3)) = al;
ecx = *(esi);
eax = fcn_00401000 (8);
...
edi--;
*((ebx - 2)) = al;
*((ebx - 1)) = 0;
} while (edi != 0);
我省略了一些代码中的内容,这个代码虽然乱七八糟,
不过我们可以很快根据结构推测出 fcn_00401000
的功能:按位读取,所以三次调用,
参数都是 8
,表示读取一个 byte。那么每次循环读取 3
个 byte,我们也很好推测:
这是在读取调色板的 rgb 数值,而且由于循环次数就是 256
到 1
,
所以这个是调色板基本实锤。事实上如果我们仔细观察 chtitle.hhp 的前 768
个字节,
确实也很像是调色板:
其实我个人觉得,这种按 byte 读取数据的功能,确实可以套用读取 8
位数据的逻辑,
但其实没什么必要,直接实现就好。不过这不是我们的重点,
我们也没有细看按位读取的逻辑,只是我的个人看法。
既然我们知道了 fcn_00401000
是按位从缓冲区读取内容的函数,
并且确认文件前 768
个字节就是调色板,那么接下来,应该就是我们的重头戏了。