时间旅行:在 XP 上运行 Python 3.7(2018)

2021-07-27 04:31:37

为了重新开始我作为技术作家的职业生涯,我选择了一个轻松的话题。即,在 Windows XP 上运行使用新版本 Visual Studio 编译的应用程序。我没有找到有关该主题的任何先前研究,但我也没有进行太多搜索。本文背后没有真正的目的,除了我想知道什么会阻止新应用程序在 XP 上运行这一事实之外。我们的目标应用程序将是适用于 x86 的 Python 3.7 的嵌入式版本。如果我们尝试在 XP 上启动任何新应用程序,我们将收到一条错误消息,通知我们它不是有效的 Win32 应用程序。发生这种情况是因为便携式可执行文件的可选标头中的某些字段。大多数人可能已经知道需要按如下方式调整这些字段: 幸运的是,调整我们要启动的可执行文件(python.exe)中的字段就足够了,不需要调整 DLL。如果我们现在尝试运行应用程序,我们将收到一条错误消息,因为 kernel32 中缺少 API。所以让我们把注意力转向进口。我们缺少 vcruntime140.dll,然后是一堆“api-ms-win-*”DLL,然后只有 python37.dll 和 kernel32.dll。首先想到的是,在新的应用程序中,我们经常会发现这些“api-ms-win-*”DLL。如果我们在 Windows 目录中搜索前缀,我们会在 System32 和 SysWOW64 中找到一个名为“downlevel”的目录,其中包含大量这些 DLL 的列表。

正如我们稍后将看到的,这些 DLL 并没有实际使用,但是如果我们用 PE 查看器打开一个,我们会看到它包含专门的转发器,这些 API 包含在通常的可疑对象中,例如 kernel32、kernelbase、user32 等。有趣的是,在downlevel目录中我们找不到python.exe导入的任何文件。这些 DLL 实际上公开了 C 运行时 API,如 strlen、fopen、exit 等。如果我们对该主题没有任何先验知识,只是在 Windows 目录中搜索此类 DLL 名称,我们将在 C:\Windows\System32\apisetschema.dll 中找到匹配项。这个 DLL 很特别,因为它包含一个 .apiset 部分,可以很容易地将其数据识别为某种格式,用于将“api-ms-win-*”名称映射到其他名称。偏移量 0 1 2 3 4 5 6 7 8 9 ABCDEF Ascii 00013AC0 C8 3A 01 00 20 00 00 00 73 00 74 00 6F 00 72 00 .:...... stor00017 007 03AD 00017 0607 06 07 00 67 00 65 00 ageusage00013AE0 2E 00 64 00 6C 00 6C 00 65 00 78 00 74 00 2D 00 .dllext-.00013AF0 6D 00 27D 073 0D 00 00 07 0 0 000ms 0 0 0 00 00 00 s.00013B00 78 00 73 00 2D 00 00 6F 6C 00 65 00 61 00 75 00 XS-.oleau00013B10 74 00 6F 00 6D 00 61 00 74 00 69 00 6F 00 6E 00 tomation00013B20 2D 00 6C 00 31 00 2D 00 31 00 2D 00 30 00 00 00 -.l.1.-.1.-.0...00013B30 00 00 00 00 00 00 00 00 00 00 00 00 44 3B 01 00 ..................... .D;..00013B40 0E 00 00 00 73 00 78 00 73 00 2E 00 64 00 6C 00 ....sxs..dl00013B50 6C 00 00 00 73 0 0 D 7 0 7 D 0 0 7 ext-.ms 在网上搜索,我找到的第一个关于这个主题的资源是 Quarkslab 博客上的两篇文章(第 1 部分和第 2 部分)。然而,我很快发现,虽然它们很有用,但它们过于陈旧,无法为我提供最新的结构来解析数据。实际上,第二篇文章显示的版本号为 2,在我撰写本文时,版本号为 6。 Offset 0 1 2 3 4 5 6 7 8 9 ABCDEF Ascii 00000000 06 00 00 00 .... 只是为了完整性,在当前文章发表后,我知道了 deroko 的一篇关于早于 Quarkslab 的主题的文章。

无论如何,我搜索了更多内容,并在 Windows Internals 的存储库中找到了 Alex Ionescu 和 Pavel Yosifovich 的代码片段。我从那里采用了以下结构。 typedef struct _API_SET_NAMESPACE { ULONG 版本;超长尺寸;乌龙旗;乌龙计数; ULONG 入口偏移; ULONG HashOffset; ULONG HashFactor;} API_SET_NAMESPACE, *PAPI_SET_NAMESPACE;typedef struct _API_SET_HASH_ENTRY { ULONG Hash; ULONG Index;} API_SET_HASH_ENTRY, *PAPI_SET_HASH_ENTRY;typedef struct _API_SET_NAMESPACE_ENTRY { ULONG Flags; ULONG NameOffset; ULONG 名称长度; ULONG HashedLength; ULONG 值偏移量; ULONG ValueCount;} API_SET_NAMESPACE_ENTRY, *PAPI_SET_NAMESPACE_ENTRY;typedef struct _API_SET_VALUE_ENTRY { ULONG Flags; ULONG NameOffset; ULONG 名称长度; ULONG 值偏移量; ULONG ValueLength;} API_SET_VALUE_ENTRY, *PAPI_SET_VALUE_ENTRY; Count 指定 API_SET_NAMESPACE_ENTRY 和 API_SET_HASH_ENTRY 结构的数量。 EntryOffset 指向 API_SET_NAMESPACE_ENTRY 结构数组的开始,在我们的例子中,它正好在 API_SET_NAMESPACE 之后。每个 API_SET_NAMESPACE_ENTRY 通过 NameOffset 字段指向“api-ms-win-*”DLL 的名称,而 ValueOffset 和 ValueCount 指定 API_SET_VALUE_ENTRY 结构的位置和计数。 API_SET_VALUE_ENTRY 结构为给定的“api-ms-win-*”DLL 生成分辨率值(例如 kernel32.dll、kernelbase.dll)。有了这些信息,我们已经可以编写一个小脚本来将新名称映射到实际的 DLL。 import osfrom Pro.Core import *from Pro.PE import *def main(): c = createContainerFromFile("C:\\Windows\\System32\\apisetschema.dll") pe = PEObject() 如果不是 pe.Load(c ): 打印("无法加载 apisetschema.dll") 返回 sect = pe.SectionHeaders() nsects = sect.Count() d = None for i in range(nsects): if sect.Bytes(0) == b ".apiset\x00": cs = pe.SectionData(i)[0] d = CFFObject() d.Load(cs) break sect = sect.Add(1) if not d: print("could find .apiset section ") return n, ret = d.ReadUInt32(12) offs, ret = d.ReadUInt32(16) for i in range(n): name_offs, ret = d.ReadUInt32(offs + 4) name_size, ret = d.ReadUInt32 (offs + 8) name = d.Read(name_offs, name_size).decode("utf-16") line = str(i) + ") " + name + " ->" values_offs, ret = d.ReadUInt32(offs + 16) value_count, ret = d.ReadUInt32(offs + 20) for j in range(value_count): vname_offs, ret = d.ReadUInt32(values_offs + 12) vname_size, ret = d.ReadUInt32(values_offs + 16) vname = .Read(vname_offs, vname_size).decode("utf-16") line += " " + vname val ues_offs += 20 offs += 24 print(line) main() 可以使用 Cerbero Profiler 从命令行作为“cerpro.exe -r apisetschema.py”执行此代码。这些是生成的输出的第一行:

0) api-ms-onecoreuap-print-render-l1-1-0 -> printrenderapihost.dll1) api-ms-onecoreuap-settingsync-status-l1-1-0 -> settingsynccore.dll2) api-ms-win- appmodel-identity-l1-2-0 -> kernel.appcore.dll3) api-ms-win-appmodel-runtime-internal-l1-1-3 -> kernel.appcore.dll4) api-ms-win-appmodel-运行时-l1-1-2 -> kernel.appcore.dll5) api-ms-win-appmodel-state-l1-1-2 -> kernel.appcore.dll6) api-ms-win-appmodel-state-l1- 2-0 -> kernel.appcore.dll7) api-ms-win-appmodel-unlock-l1-1-0 -> kernel.appcore.dll8) api-ms-win-base-bootconfig-l1-1-0 - > advapi32.dll9) api-ms-win-base-util-l1-1-0 -> advapi32.dll10) api-ms-win-composition-redirection-l1-1-0 -> dwmredir.dll11) api-ms -win-composition-windowmanager-l1-1-0 -> udwm.dll12) api-ms-win-core-apiquery-l1-1-0 -> ntdll.dll13) api-ms-win-core-appcompat-l1 -1-1 -> kernelbase.dll14) api-ms-win-core-appinit-l1-1-0 -> kernel32.dll kernelbase.dll... 回到API_SET_NAMESPACE,它的字段HashOffset指向一个API_SET_HASH_ENTRY数组结构。正如我们稍后将看到的,Windows 加载程序使用这些结构来快速索引“api-ms-win-*”DLL 名称。 Hash 字段实际上是名称的散列,通过同时考虑 HashFactor 和 HashedLength 来计算,而 Index 指向关联的 API_SET_NAMESPACE_ENTRY 条目。 77EA1DAC mov ebx, dword ptr [ebx + 0x18]; ebx 77EA1DAF mov esi, eax 中的HashFactor; esi = dll 名称长度 77EA1DB1 movzx eax, word ptr [edx] ;一个 unicode 字符到 eax77EA1DB4 lea ecx, dword ptr [eax - 0x41] ; ecx = 字符 - 0x4177EA1DB7 cmp cx, 0x19 ;与 0x1977EA1DBB jbe 0x77ea2392 相比;如果低于或等于,则退出 77EA1DC1 mov ecx, ebx ; ecx = HashFactor77EA1DC3 movzx eax, ax77EA1DC6 imul ecx, edi ; ecx *= edi77EA1DC9 添加 edx, 2 ; edx += 277EA1DCC 添加 ecx, eax ; ecx += eax77EA1DCE mov edi, ecx ; edi = ecx77EA1DD0 子 esi, 1 ; len -= 177EA1DD3 jne 0x77ea1db1 ;如果不是从 77EA1DB1 的零重复 const char *p = dllname;int HashedLength = 0x23;int HashFactor = 0x1F;int Hash = 0;for (int i = 0; i < HashedLength; i++, p++) Hash = (Hash * HashFactor) + *p;作为一个实际示例,让我们以 DLL 名称“api-ms-win-core-processthreads-l1-1-2.dll”为例。它的哈希值为 0x445B4DF3。如果我们找到其匹配的 API_SET_HASH_ENTRY 条目,我们将拥有关联 API_SET_NAMESPACE_ENTRY 结构的索引。偏移量 0 1 2 3 4 5 6 7 8 9 ABCDEF Ascii 00014DA0 F3 4D 5B 44 .M[D00014DB0 5B 00 00 00 [...因此,0x5b(或 91)是索引。通过返回映射的输出,我们可以看到它匹配。

通过检查相同的输出,我们还可以注意到所有 C 运行时 DLL 都解析为 ucrtbase.dll。 167) api-ms-win-crt-conio-l1-1-0 -> ucrtbase.dll168) api-ms-win-crt-convert-l1-1-0 -> ucrtbase.dll169) api-ms-win- crt-environment-l1-1-0 -> ucrtbase.dll170) api-ms-win-crt-filesystem-l1-1-0 -> ucrtbase.dll171) api-ms-win-crt-heap-l1-1- 0 -> ucrtbase.dll172) api-ms-win-crt-locale-l1-1-0 -> ucrtbase.dll173) api-ms-win-crt-math-l1-1-0 -> ucrtbase.dll174) api -ms-win-crt-multibyte-l1-1-0 -> ucrtbase.dll175) api-ms-win-crt-private-l1-1-0 -> ucrtbase.dll176) api-ms-win-crt-process -l1-1-0 -> ucrtbase.dll177) api-ms-win-crt-runtime-l1-1-0 -> ucrtbase.dll178) api-ms-win-crt-stdio-l1-1-0 -> ucrtbase.dll179) api-ms-win-crt-string-l1-1-0 -> ucrtbase.dll180) api-ms-win-crt-time-l1-1-0 -> ucrtbase.dll181) api-ms- win-crt-utility-l1-1-0 -> ucrtbase.dll 当我注意到微软实际上支持在其上部署运行时时,我已经不得不弄清楚如何在 XP 上支持 C 运行时。以下来自 MSDN 的摘录同样说明了这一点:如果您当前使用 VCRedist(我们的可再发行包文件),那么事情将像以前一样对您有用。 Visual Studio 2015 VCRedist 包包含上述 Windows 更新包,因此只需安装 VCRedist 即可安装 Visual C++ 库和通用 CRT。这是我们推荐的部署机制。在没有通用 CRT Windows 更新 MSU 的 Windows XP 上,VCRedist 将自行部署通用 CRT。这意味着在 XP 之后的 Windows 版本中,支持是通过 Windows 更新提供的,但在 XP 上我们必须自己部署文件。我们可以在 C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs 中找到要部署的文件。该路径包含三个子目录:x86、x64 和 arm。我们显然对 x86 感兴趣。其中包含的文件很多(42 个),显然是最常见的“api-ms-win-*”DLL 和 ucrtbase.dll。我们可以将这些文件部署到 XP 上以使我们的应用程序正常工作。我们仍然缺少 vcruntime140.dll,但我们可以从 Visual C++ 安装中获取该 DLL。事实上,该 DLL 旨在部署,而通用 CRT (ucrtbase.dll) 旨在成为 Windows 系统的一部分。这满足了我们对 DLL 的依赖。但是,Windows 多年来引入了许多 XP 上没有的新 API。因此,我编写了一个脚本,通过检查导入的 API 与 XP 上 DLL 导出的 API 来测试应用程序的兼容性。它的命令行是“cerpro.exe -r xpcompat.py application_path”。它将检查指定目录中的所有 PE 文件。 import os, sysfrom Pro.Core import *from Pro.PE import *xp_system32 = "C:\\Users\\Admin\\Desktop\\system32"apisetschema = { "OMITTED FOR BREVITY" }cached_apis = {}missing_result = {} def getAPIs(dllpath): apis = {} c = createContainerFromFile(dllpath) dll = PEObject() 如果不是 dll.Load(c): print("error: 无法加载 dll") return apis ordbase = dll.ExportDirectory( ).Num("Base") functions = dll.ExportDirectoryFunctions() names = dll.ExportDirectoryNames() nameords = dll.ExportDirectoryNameOrdinals() n = functions.Count() it = functions.iterator() for x in range(n) : func = it.next() ep = func.Num(0) if ep == 0: continue apiord = str(ordbase + x) n2 = nameords.Count() it2 = nameords.iterator() name_found = False for y in range(n2): no = it2.next() if no.Num(0) == x: name = names.At(y) offs = dll.RvaToOffset(name.Num(0)) name, ret = dll .ReadUInt8String(offs, 500) apiname = name.decode("ascii") apis[apiname] = apiord apis[apiord] = apiname name_found = True break if not name_found: apis[apiord ] = apiord return apis def checkMissingAPIs(pe, ndescr, dllname, xpdll_apis): ordfl = pe.ImportOrdinalFlag() ofts = pe.ImportThunks(ndescr) it = ofts.iterator() while it.hasNext(): ft = it. next().Num(0) if (ft & ordfl) != 0: name = str(ft ^ ordfl) else: offs = pe.RvaToOffset(ft) name, ret = pe.ReadUInt8String(offs + 2, 400)如果不是 ret: continue name = name.decode("ascii") 如果不是 xpdll_apis 中的 name: print(" ", "missing:", name) temp = missing_result.get(dllname, set()) temp.add(name ) missing_result[dllname] = tempdef verifyXPCompatibility(fname): print("file:", fname) c = createContainerFromFile(fname) pe = PEObject() 如果不是 pe.Load(c): return it = pe.ImportDescriptors()。 iterator() ndescr = -1 while it.hasNext(): descr = it.next() ndescr += 1 offs = pe.RvaToOffset(descr.Num("Name")) name, ret = pe.ReadUInt8String(offs, 400) 如果不是 ret: continue name = name.decode("ascii").lower() 如果不是 name.endswith(".dll"): continue fwdlls = apisetschema.get(name[:-4], [])如果 len(fwdlls) == 0: 打印(" ", name) else: fwdll = fwdlls[0] print(" ", name, "->", fwdll) name = fwdll if name == "ucrtbase.dll": continue xpdll_path = os.path.join(xp_system32, name) 如果不是 os.path.isfile(xpdll_path):如果不是 cached_apis 中的 name,则继续:cached_apis[name] = getAPIs(xpdll_path) checkMissingAPIs(pe, ndescr, name, cached_apis[name]) print() def main(): if os.path.isfile(sys.argv[1]): verifyXPCompatibility(sys.argv[1]) else: files = [os.path.join(dp, f) for dp, dn, fn in os.walk( sys.argv[1]) for f in fn] for fname in files: with open(fname, "rb") as f: if f.read(2) == b"MZ": verifyXPCompatibility(fname) # summary n = 0 print("\nsummary:") for rdll, rapis in missing_result.items(): print(" ", rdll) for rapi in rapis: print(" ", "missing:", rapi) n += 1 print ("缺失的 API 总数:", str(n))main()

为简洁起见,我不得不省略 apisetschema 全局变量的内容。你可以从这里下载完整的脚本。代码中引用的system32目录是我复制到桌面的Windows XP目录。文件:python-3.7.0-embed-win32\python37.dll version.dll shlwapi.dll ws2_32.dll kernel32.dll 丢失:GetFinalPathNameByHandleW 丢失:InitializeProcThreadAttributeList 丢失:UpdateProcThreadAttribute 丢失:DeleteProcThreadAttributeList 丢失:GetTickCount64 advapi32.dll vcrun ms-win-crt-runtime-l1-1-0.dll -> ucrtbase.dll api-ms-win-crt-math-l1-1-0.dll -> ucrtbase.dll api-ms-win-crt-语言环境-l1-1-0.dll -> ucrtbase.dll api-ms-win-crt-string-l1-1-0.dll -> ucrtbase.dll api-ms-win-crt-stdio-l1-1- 0.dll -> ucrtbase.dll api-ms-win-crt-convert-l1-1-0.dll -> ucrtbase.dll api-ms-win-crt-time-l1-1-0.dll -> ucrtbase .dll api-ms-win-crt-environment-l1-1-0.dll -> ucrtbase.dll api-ms-win-crt-process-l1-1-0.dll -> ucrtbase.dll api-ms- win-crt-heap-l1-1-0.dll -> ucrtbase.dll api-ms-win-crt-conio-l1-1-0.dll -> ucrtbase.dll api-ms-win-crt-filesystem- l1-1-0.dll -> ucrtbase.dll[...] 文件:python-3.7.0-embed-win32\_socket.pyd ws2_32.dll 丢失:inet_ntop 丢失:inet_pton kernel32.dll p ython37.dll vcruntime140.dll api-ms-win-crt-runtime-l1-1-0.dll -> ucrtbase.dll[...] 摘要:缺少 kernel32.dll:缺少 InitializeProcThreadAttributeList:缺少 GetTickCount64:缺少 GetFinalPathNameByHandleW:UpdateProcThreadAttribute丢失:DeleteProcThreadAttributeList ws2_32.dll 丢失:inet_pton 丢失:inet_ntoptotal 丢失的 API:7 我们从 kernel32.dll 中丢失了 5 个 API,从 ws2_32.dll 中丢失了 2 个,但是 Winsock API 只是由 _socket.pyd 导入的仅在 Python 执行网络操作时加载。因此,理论上,我们现在可以将精力集中在缺失的 kernel32 API 上。我的计划是创建一个假的 kernel32.dll,称为 xernel32.dll,其中包含大多数 API 的转发器和仅用于缺失 API 的真实实现。这是一个创建 C++ 文件的脚本,其中包含 Windows 10 上常见 DLL 的所有 API 的转发器:import os, sysfrom Pro.Core import *from Pro.PE import *xpsys32path = "C:\\Users\\Admin\\Desktop\\ system32"sys32path = "C:\\Windows\\SysWOW64"def getAPIs(dllpath): pass # 与上面相同的代码 def isOrdinal(i): try: int(i) return True except: return False def createShadowDll(name): xpdllpath = os.path.join(xpsys32path, name + ".dll") xpapis = getAPIs(xpdllpath) dllpath = os.path.join(sys32path, name + ".dll") apis = sorted(getAPIs(dllpath).keys ()) if len(apis) != 0: with open(name + ".cpp", "w") as f: f.write("#include \n\n") for a in apis: comment = " // XP" if a in xpapis else "" if not isOrdinal(a): f.write("#pragma comment(linker, \"/export:" + a + "=" + name + "." + a + "\")" + comment + "\n") # print("created", name + ".cpp") def main(): dlls = ("advapi32", "comdlg32", "gdi32", "iphlpapi" , "kernel32", "ole32", "oleaut32", "shell32", "shlwapi", "user32", "uxtheme", "ws2_32") 用于 dll i n dlls: createShadowDll(dll)main() #include #pragma comment(linker, "/export:AcquireSRWLockExclusive=kernel32.AcquireSRWLockExclusive")#pragma comment(linker, "/export:AcquireSRWLockShared=kernel32.AcquireSRWLockShared comment") linker, "/export:ActivateActCtx=kernel32.ActivateActCtx") // XP#pragma comment(linker, "/export:ActivateActCtxWorker=kernel32.ActivateActCtxWorker")#pragma comment(linker, "/export:AddAtomA=kernel32")AddAtom // XP#pragma comment(linker, "/export:AddAtomW=kernel32.AddAtomW") // XP#pragma comment(linker, "/export:AddConsoleAliasA=kernel32.AddConsoleAliasA") // XP#pragma comment(linker, " /export:AddConsoleAliasW=kernel32.AddConsoleAliasW") // XP#pragma comment(linker, "/export:AddDllDirectory=kernel32.AddDllDirectory")[...] 右边的注释("// XP")表示XP 上是否存在转发的 API。我们可以专门为我们想要的 API 提供真正的实现。 Windows 加载程序不关心我们是否转发不存在的函数,只要它们没有被导入。

GetTickCount64:我只是调用了 GetTickCount,并不是很重要 GetFinalPathNameByHandleW:从 Wine 获取实现,但必须稍微调整它 InitializeProcThreadAttributeList:从 Wine 获取实现 UpdateProcThreadAttribute:相同 DeleteProcThreadAttributeList:相同 我必须感谢这里的 Wine 项目,因为它提供了有用的实现,这节省了我的精力。我将针对旧 Windows 版本的支持运行时的尝试称为“XP Time Machine Runtime”,您可以在此处找到存储库。我用 Visual Studio 2013 和 cmake 编译了它。这样我们现在就有了 xernel32.dll,我们唯一要做的就是在 python37.dll 中重命名导入的 DLL。当然,我们还没有完全完成,因为我们没有实现缺少的 Winsock API,但也许 t ......