[2/2]解决Windows 8.1检查更新时发生的0x8024A008错误

昨天说到哪了,哦对,怎么把代码注入到别的进程里。

需要用的Windows API是WriteProcessMemory,然后我们还需要获取进程里kernel32.dll的基地址,因此还需要用EnumProcessModulesEx、EnumProcesses

最后为了防止修改进程内存的时候进程运行到OOBEComplete上,还需要两个未说明的API:NtSuspendProcess和NtResumeProcess

另外考虑到之后dll更新会引起函数地址变化,最好能从kernel32.dll的PE头里获取OOBEComplete的RVA再计算出实际地址进行修改

最后因为要修改的svchost.exe是系统进程,我们必须要提升到System级的权限才可以。这部分直接交给SysInternals的PsExec处理。

那么首先解决获取RVA的问题

PE文件的开头先是一段DOS文件头,然后是DOS下的可执行代码,然后才是PE文件头,其偏移量在DOS文件头中有记录。

DOS文件头的格式为:(偏移量是字节为单位的,所有数字都是小尾序)

    +-----+-----+-----+-----+-----+-----+-----+-----+
    |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |
    +-----+-----+-----+-----+-----+-----+-----+-----+
0+  | 'M' | 'Z' | lastSize  |  nBlocks  |  nReloc   |
    +-----+-----+-----------+-----------+-----------+
8+  |  hdrSize  | minAlloc  | maxAlloc  |    SS     |
    +-----------+-----------+-----------+-----------+
16+ |    SP     | checksum  |    IP     |    CS     |
    +-----------+-----------+-----------+-----------+
24+ | relocPos  | nOverlay  |       RESERVED        |
    +-----------+-----------+-----------+-----------+
32+ |       RESERVED        |  OEM ID   | OEM INFO  |
    +-----------+-----------+-----------+-----------+
40+ |                    RESERVED                   |
48+ |                    16 bytes                   |
    +-----------+-----------+-----------+-----------+
56+ |        RESERVED       | Pointer to PE Header  |
    +-----------+-----------+-----------+-----------+

我们需要在意的只有最后那个域里的值。读出来之后直接作为fseek的参数重新定位,我们就找到了PE文件头。

    +-----+-----+-----+-----+-----+-----+-----+-----+
    |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |
    +-----+-----+-----+-----+-----+-----+-----+-----+
0+  | 'P' | 'E' | 0x0 | 0x0 |  Machine  | nSections |
    +-----+-----+-----+-----+-----------+-----------+
8+  |     TimeDateStamp     |      pSymbolTable     |
    +-----------+-----------+-----------+-----------+
16+ |       nSymbols        | cbOptHdr  |   Attr    |
    +-----------+-----------+-----------+-----------+

紧随其后的是可选PE文件头,大小在cbOptHdr里说明,其中就有我们关心的函数导出表。

需要注意的是,32位的PE文件的可选头和64位的不同,因此需要分开处理。

对32位的PE文件,其可选头的结构为:

    +-----+-----+-----+-----+-----+-----+-----+-----+
    |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |
    +-----+-----+-----+-----+-----+-----+-----+-----+
0+  | 0x2 | 0xB |vLkMa|vLkMi|         cbCode        |
    +-----+-----+-----+-----+-----------+-----------+
8+  |     cbInitialized     |    cbUninitialized    |
    +-----------+-----------+-----------+-----------+
16+ |       pEntryPoint     |      Base of code     |
    +-----------+-----------+-----------+-----------+
24+ |      Base of data     |      addrImageBase    |
    +-----------+-----------+-----------+-----------+
32+ |     cbSectionAlign    |      cbFileAlign      |
    +-----------+-----------+-----------+-----------+
40+ | OSVerMajor| OSVerMinor|ImgVerMajor|ImgVerMinor|
    +-----------+-----------+-----------+-----------+
48+ |SubSysMajor|SubSysMinor|       RESERVED        |
    +-----------+-----------+-----------+-----------+
56+ |        cbImage        |       cbHeader        |
    +-----------+-----------+-----------+-----------+
64+ |       Checksum        | Subsystem |  DllAttr  |
    +-----------+-----------+-----------+-----------+
72+ |     cbStackReserve    |     cbStackCommit     |
    +-----------+-----------+-----------+-----------+
80+ |     cbHeapReserve     |     cbHeapCommit      |
    +-----------+-----------+-----------+-----------+
88+ |      LoaderFlags      |       nSizeRVA        |
    +-----------+-----------+-----------+-----------+
96+ |                 Data Directory                |
104+|            8 bytes for each entry             |
112+|        till the end of optional header        |
120+|                     .....                     |
    +-----------+-----------+-----------+-----------+

而对于64位的PE文件,可选头的格式是:

    +-----+-----+-----+-----+-----+-----+-----+-----+
    |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |
    +-----+-----+-----+-----+-----+-----+-----+-----+
0+  | 0x2 | 0xB |vLkMa|vLkMi|         cbCode        |
    +-----+-----+-----+-----+-----------+-----------+
8+  |     cbInitialized     |    cbUninitialized    |
    +-----------+-----------+-----------+-----------+
16+ |       pEntryPoint     |      Base of code     |
    +-----------+-----------+-----------+-----------+
24+ |                 addrImageBase                 |
    +-----------+-----------+-----------+-----------+
32+ |     cbSectionAlign    |      cbFileAlign      |
    +-----------+-----------+-----------+-----------+
40+ | OSVerMajor| OSVerMinor|ImgVerMajor|ImgVerMinor|
    +-----------+-----------+-----------+-----------+
48+ |SubSysMajor|SubSysMinor|       RESERVED        |
    +-----------+-----------+-----------+-----------+
56+ |        cbImage        |       cbHeader        |
    +-----------+-----------+-----------+-----------+
64+ |       Checksum        | Subsystem |  DllAttr  |
    +-----------+-----------+-----------+-----------+
72+ |                 cbStackReserve                |
    +-----------+-----------+-----------+-----------+
80+ |                 cbStackCommit                 |
    +-----------+-----------+-----------+-----------+
88+ |                 cbHeapReserve                 |
    +-----------+-----------+-----------+-----------+
96+ |                  cbHeapCommit                 |
    +-----------+-----------+-----------+-----------+
104+|      LoaderFlags      |       nSizeRVA        |
    +-----------+-----------+-----------+-----------+
112+|                 Data Directory                |
120+|            8 bytes for each entry             |
128+|        till the end of optional header        |
136+|                     .....                     |
    +-----------+-----------+-----------+-----------+

我们关心的函数导出表是Data Directory中的第0项,Data Directory每项为两个long int,第一个是RVA,第二个是大小。

RVA指向函数导出表的描述结构,其结构为:

    +-----+-----+-----+-----+-----+-----+-----+-----+
    |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |
    +-----+-----+-----+-----+-----+-----+-----+-----+
0+  |    Characteristics    |     TimeDateStamp     |
    +-----------+-----------+-----------+-----------+
8+  | VerMajor  | VerMinor  |        lpName         |
    +-----------+-----------+-----------+-----------+
16+ |         Base          |       nFunctions      |
    +-----------+-----------+-----------+-----------+
24+ |        nNames         |     lplpFunctions     |
    +-----------+-----------+-----------+-----------+
32+ |       lplpNames       |       lpOrdinal       |
    +-----------+-----------+-----------+-----------+

到这里熟悉Windows API的应该已经知道怎么去搜索OOBEComplete的RVA地址了,不过我还是解释一下。

前缀lp的意思是一个指针,lplp就是指向指针的指针,lplpFunctions是指向[指向函数入口的指针]的数组的指针,lplpNames是指向[指向函数名字的指针]的数组的指针,二者一一对应。(指针在这里指的是RVA)

于是只需要遍历lplpNames里的每个字符串就能找到OOBEComplete的序数n,而lplpFunctions的第n项就是它的RVA,加上kernel32.dll的基地址就是OOBEComplete在内存里的地址。

但是要注意一点,RVA和文件中的偏移量并不相同,也就是说需要有一个把RVA翻译成文件偏移量的方法。于是我们需要区段表。

在可选文件头之后的就是区段表,其格式为:

    +-----+-----+-----+-----+-----+-----+-----+-----+
    |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |
    +-----+-----+-----+-----+-----+-----+-----+-----+
0+  |        Section name (8 characters max)        |
    +-----------+-----------+-----------+-----------+
8+  |       cbInMemory      |      pMemoryAddr      |
    +-----------+-----------+-----------+-----------+
16+ |        cbOnDisk       |      cbFileOffset     |
    +-----------+-----------+-----------+-----------+
24+ |                   RESERVED                    |
    +-----------+-----------+-----------+-----------+
32+ |       RESERVED        |         flag          |
    +-----------+-----------+-----------+-----------+

对一个给定的RVA,其在文件中的偏移量可以通过如下方法计算出来:

找到一个区段,满足pMemoryAddr <= RVA < pMemoryAddr + cbOnDisk,如果找不到,表示给定的RVA地址不存在在文件中(比如整个.bss区段),偏移量为RVA – pMemoryAddr + cbFileOffset

这样一来就解决了RVA到文件偏移量的翻译问题,再加上之前搜索导出表的方法,就可以获取OOBEComplete的RVA,加上从EnumProcessModulesEx返回的模块基地址,就可以求出OOBEComplete在特定进程的虚拟内存中的地址,再用同样的方法获取ntdll中RtlSetLastWin32Error的地址,作差可以求出汇编代码中空缺的偏移量,最后使用WriteProcessMemory就可以进行修改了。

成品:

kernel32patch.c
kernel32patch.exe

发表评论