如何编译静态版的Python

0x01 Prelusion

在HackingTeam如何被黑( https://pastebin.com/raw/0SNSvyjJ )的这篇Guide中,渗透者提到一个细节:

I did a lot of work and testing before using the exploit against Hacking Team.
I wrote a backdoored firmware, and compiled various post-exploitation tools
for the embedded device.

作者在打下一个设备之后,选择在这台设备上运行各类后渗透阶段的工具。这其中不可避免的一个问题是,大部分嵌入式设备的系统环境都可能经过剪裁,如没有编译环境,没有包管理等。所以这里作者选择在其他地方进行静态编译后传上设备运行。尽管“Static link is evil”,但在特殊的环境下,尤其是渗透的过程中优势还是很明显的,如可控的体积,单一文件等。

这其中引用到了一个项目:
https://github.com/bendmorris/static-python

这个项目用来编译一个静态编译版本的Python(项目已无更新)。当前由Python驱动的安全工具种类丰富多样,于是笔者也萌生了编译一个静态版本Python用于渗透的想法。

0x02 How to accomplish the target

首先需要对我们的目标加以明确,这是一个怎么的Python版,有哪些要求来约束它,我想了一下,大概如下:

  1. 体积要求在5M以下(压缩情况)
  2. 尽可能的保留跨平台支持
  3. 能伴随Python版本更新而更新,也就是说尽可能小的对原工程进行改动
  4. 单一的可执行文件,并且不释放额外文件,全部在内存中执行
  5. 方便的扩展性,如选择性支持一些第三方库

笔者准备选择Python3的版本来进行调试及修改,在开始之前我们需要做一些准备工作。为了减小跨平台的复杂性(如编译环境的准备等工作),这里仅仅选择了如下环境作为测试:Windows 10 + Vs2017 latest。
Python版本为 https://www.python.org/ftp/python/3.6.4/Python-3.6.4.tgz

解压之后可以看到Python3的整体目录结构,下面加以简单的说明,以便在后续进行更好的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├─Doc
├─externals
│ └─openssl-1.0.2k
├─Grammar
├─Include
├─Lib
├─Mac
├─Misc
├─Modules
├─Objects
├─Parser
├─PC
├─PCbuild
│ └─win32
├─Programs
├─Python
└─Tools
  1. Doc目录毫无疑问的是保存Python文档相关的源码
  2. externals目录中保存了Python依赖的外部项目,这里主要是OpenSSL
  3. Grammar目录保存了Python的语法解释
  4. Include目录中保存了Python相关的头文件
  5. Lib目录中保存了Python的标准库实现,是用Python实现而非C语言
  6. Mac目录中保存了Mac系统下的编译脚本等
  7. Moduels目录中保存了一些依赖的C模块的源码
  8. Objects目录中保存了Python中的对象实现的C源码
  9. Parser目录中保存了Python语言的解析器实现的C源码
  10. PC目录中保存了与Windows平台强相关的Python源码(Dll、配置等)
  11. PCbuild目录中保存了Windows平台下编译的目标文件,如Pyd和EXE等
  12. Programs目录中保存了一个最小的Python驱动环境源码
  13. Python目录中保存了整个Python的核心C源码
  14. Tools目录保存了相关的Python工具,如编译C扩展需要使用的工具等

实际上这其中和Python核心比较息息相关的目录,主要集中在了Lib、Modules、Include、Objects、Parser、Python这几个目录中。
其中编译Python源码相关的目录为Include、Objects、Parser、Python。
一个嵌入式版本的又依赖Modules编译出来的Pyd文件(实际上就动态链接库文件),同时还依赖Lib目录下用Python实现的标准库,注入socket、ctypes这类标准库,最终的实际功能是调用C库去完成的。
而PC和PCBuild主要用于Windows下的Python编译选项及解释器源码等,在PCBuild中包含了VS的SLN解决方案文件,可以直接打开来进行编译。

Python的官方文档提供了一个在Windows下的编译指南( https://docs.python.org/3/using/windows.html ),在 PCbuild/readme.txt 中你可以查看具体详情,在无特殊配置下,运行 PCbuild/build.bat 会自动查找当前系统的VS环境并通过MSBuild执行编译的操作。

我们先尝试编译一个嵌入式版本的(动态链接库)Python来看他的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
libcrypto-1_1.dll
libssl-1_1.dll
pyexpat.pyd
python.exe
python3.dll
python37.dll
python37.zip
python37._pth
pythonw.exe
select.pyd
sqlite3.dll
unicodedata.pyd
vcruntime140.dll
winsound.pyd
_asyncio.pyd
_bz2.pyd
_ctypes.pyd
_decimal.pyd
_elementtree.pyd
_hashlib.pyd
_lzma.pyd
_msi.pyd
_multiprocessing.pyd
_overlapped.pyd
_socket.pyd
_sqlite3.pyd
_ssl.pyd

与正常安装版本不同的是,Python解释器会根据当前目录的._pth文件来找Sys.Path的路径,并且Python本身就支持ZlibImport的导入,所以在嵌入式版本中Lib目录下的Python标准库文件被打包到了PythonXY.zip(默认也可能是Python.zip)中。而其余的Pyd文件则是Python依赖的外部模块,这些Pyd模块也依赖其他以动态链接库形式链接过来的Dll文件,比如 _socket.pyd 模块也依赖了 select.pyd,_hashlib.pyd也依赖了libssl.dll等。而Python解释器(python.exe)最主要的依赖还是Pythoncore(python37.dll)。

所以需要将其变成静态编译的版本,要解决的问题可以总结如下:

  1. 将Pythoncore静态链接到Python解释器(Python.exe)
  2. 将其他外部模块也静态链接到Python解释器,解决模块之间的互相链接问题
  3. 将Lib(Python37.zip)的标准库以某种方式结合到Python解释器中

Pythoncore的静态链接

打开SLN,尝试修改Pythoncore的编译选项中的代码生成为/MT,同时输出类型改为Static Link Lib,同时修改Python工程编译选项中的代码生成为/MT,以Release方式进行编译。对于Python项目,会编译失败,输出

1
2
2>------ Build started: Project: python, Configuration: Release Win32 ------
2>python.obj : error LNK2001: unresolved external symbol __imp__Py_Main

这是因为( https://msdn.microsoft.com/zh-cn/library/f6xx1b1z.aspx ):

使用编译时/MD,对您的源中的”func”的引用将变为一个引用”impfunc”由于所有运行时现在都保留在 DLL 中的对象中。 如果您尝试与静态库 LIBC.lib 或 LIBCMT.lib 链接,您将收到有关 LNK2001 impfunc。 如果您尝试在没有 /MD 的情况下编译时与 MSVCxx.lib 链接则不会始终获得 LNK2001,但您可能有其他问题。

我们前面说过PC目录中负责了大部分和Windows系统下编译相关的源码配置等,而官方没有提供静态编译的文档,所以在这部分我们只能试图通过阅读源码来进行解析。找到从名称上看起来类似配置的头文件 PC/pyconfig.h 从中可以看到如下注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* pyconfig.h. NOT Generated automatically by configure.
This is a manually maintained version used for the Watcom,
Borland and Microsoft Visual C++ compilers. It is a
standard part of the Python distribution.
WINDOWS DEFINES:
The code specific to Windows should be wrapped around one of
the following #defines
MS_WIN64 - Code specific to the MS Win64 API
MS_WIN32 - Code specific to the MS Win32 (and Win64) API (obsolete, this covers all supported APIs)
MS_WINDOWS - Code specific to Windows, but all versions.
Py_ENABLE_SHARED - Code if the Python core is built as a DLL.
Also note that neither "_M_IX86" or "_MSC_VER" should be used for
any purpose other than "Windows Intel x86 specific" and "Microsoft
compiler specific". Therefore, these should be very rare.
NOTE: The following symbols are deprecated:
NT, USE_DL_EXPORT, USE_DL_IMPORT, DL_EXPORT, DL_IMPORT
MS_CORE_DLL.
WIN32 is still required for the locale module.
*/
/* Deprecated USE_DL_EXPORT macro - please use Py_BUILD_CORE */

这种包含了很多有趣的宏,依照 Py_BUILD_CORE 这个宏我们可以找到更多线索,而且注释中表明了非DLL形式的Pythoncore应该使用怎样的宏,我们在Python解释器项目的预定义中加上Py_BUILD_CORE,同时在Pythoncore项目中的预定义中加上Py_NO_ENABLE_SHARED,来声明Python解释器不以DLL的形式引用Pythoncore,然后进行编译,又输出了一个错误如下:

1
2
3
2>------ Build started: Project: python, Configuration: Release Win32 ------
2>python.c
2>LINK : fatal error LNK1104: cannot open file 'Microsoft.VisualStudio.Setup.Configuration.Native.lib'

看起来是缺少这个Lib,从Nuget仓库中能找到这个Lib的地址 https://www.nuget.org/packages/Microsoft.VisualStudio.Setup.Configuration.Native/ ,但是从介绍上来看并不能知道他的具体作用什么,我们姑且使用Nuget包管理安装上试试。再编译此时已经不出现缺少这个Lib的错误了,而是现实其他的如下错误:

1
2
3
4
5
1>python36.lib(signalmodule.obj) : error LNK2001: unresolved external symbol __imp__WSAGetLastError@0
1>python36.lib(signalmodule.obj) : error LNK2001: unresolved external symbol __imp__getsockopt@20
1>python36.lib(signalmodule.obj) : error LNK2001: unresolved external symbol __imp__send@16
1>python36.lib(getpathp.obj) : error LNK2001: unresolved external symbol __imp__PathCombineW@12
1>C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.exe : fatal error LNK1120: 4 unresolved externals

很明显这些静态依赖都是来自Windows本身的, WSA系列API来自 Ws2_32.lib;PathCombine这个API来自 shlwapi.lib ,我们在Python解释器项目的编译选项的库输入路径手工添加他们即可。

编译成功之后我们直接运行Python.exe会提示找不到模块Encodings,前面说过,Python解释器对于标准库在默认情况会去Sys.Path找,所以这里只需要将Lib和Python.exe置于同一目录即可顺利运行了。

解决外部模块的依赖问题

我们在渗透过程中,其实用得较为多还是很多网络相关的库,而这些库的基础就是Socket库了。前面说过Python引入的库分为两种,如上图的os属于内部模块(Build-In),而当尝试 import socket 时会发现找不到 _socket 这个Module。实际上Socket Module属于另外一类即为外部模块(External),默认情况下他们以动态链接的方式引入的。

因为我们的Python解释器使用了/MT选项进行编译,即使SLN中的_socket项目不修改其编译选项,也是无法引用的,故接下来我们需要解决的问题就是如何将外部模块也静态编译到Python.exe当中了。

同样我们修改 _socket 项目的编译选项为/MT,同时输出为Static Link Lib,同时还需要修改输出文件的扩展名为.lib(解决方案中项目的默认是为pyd)。编译 _socket 项目提示成功,但是这里我们的Python并没有显示的引入 _socket.lib,所以一定在哪部分的配置对内部模块的加载做了映射。查阅文档其实可以发现只提了部分关于内部模块的概念,而并没有指明哪个文件中如何进行配置。

这里我们依旧开启源码阅读模式,在 PC/config.c 中看到如下注释:

1
2
3
4
/* Module configuration */
/* This file contains the table of built-in modules.
See create_builtin() in import.c. */

这里无疑是配置内部模块的部分,也可以看到os这样的模块是如何映射的。我们依葫芦画瓢添加相应的初始化函数及映射的表,编译成功后发现 _socket 同时依赖了 select.pyd 照同样的方式添加到config中,我们再次编译,成功后在解释器中执行 import socket 发现并无错误了。

执行结果见上图,此时我们来依次处理其他几个库,单个pyd无依赖的基本都能正确编译通过。
较为麻烦的主要有 _ssl 、_asyncio 与 _ctypes等。下面我们来一一罗列具体的编译过程。先说 _ssl 的编译过程:PCBuild/get_externals.bat 提供了需要依赖的外部模块的下载,其实阅读 PCBuild/build.bat 的批处理源码也可以知道,是必须要经过这个过程的,不然 _ssl 编译时会提示找不到OpenSSL的相关头文件,如果中途遇到网络问题,请清空 Externals 目录然后在重新运行 PCBuild/get_externals.bat 来拉取需要依赖的外部模块源文件。
此时同时修改 _ssl、libeay、ssleay 三个项目的编译选项为Static Lib以及/MT,再次编译会提示找不到其他几个函数的导出错误,具体信息如下:

1
2
3
4
5
6
7
8
1>_ssl.lib(_ssl.obj) : error LNK2001: unresolved external symbol __imp__CertEnumCRLsInStore@8
1>_ssl.lib(_ssl.obj) : error LNK2001: unresolved external symbol __imp__CertGetEnhancedKeyUsage@16
1>_ssl.lib(_ssl.obj) : error LNK2001: unresolved external symbol __imp__CertFreeCRLContext@4
1>_ssl.lib(_ssl.obj) : error LNK2001: unresolved external symbol __imp__CertFreeCertificateContext@4
1>_ssl.lib(_ssl.obj) : error LNK2001: unresolved external symbol __imp__CertEnumCertificatesInStore@8
1>_ssl.lib(_ssl.obj) : error LNK2001: unresolved external symbol __imp__CertCloseStore@8
1>_ssl.lib(_ssl.obj) : error LNK2001: unresolved external symbol __imp__CertOpenStore@20
1>C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.exe : fatal error LNK1120: 7 unresolved externals

只要在Python项目的链接输入中添加 _ssl.lib、libeay.lib、ssleay.lib以及解决如上错误的Crypt32.lib(上面函数是Windows的Crypt系列API)即可编译成功。
给Python的Lib输入添加 libeay.lib ssleay.lib

接下来我们来处理 _asyncio 的静态编译, _asyncio 依赖 overlapped、multiprocessing 等几个库, 我们依照前面的处理方式来修改编译配置,进行编译后会出现一个冲突如下:

1
2
1> Creating library C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.lib and object C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.exp
1>C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.exe : fatal error LNK1169: one or more multiply defined symbols found

在SLN中搜索 OverlappedType 确实发现有两处定义,所以产生了冲突。尝试替换 Winapi.c 中所有的OverlappedType 定义为 WinApiOverlappedType ,处理后编译通过,但就目前情况来说不不确定这样Dirty的修改方法会不会产生什么其他影响。

最后我们来解决 _ctypes 的静态,依照前面的方式,编译无其他错误,但是运行后会出现如下类似错误:

1
2
3
4
5
6
1>Traceback (most recent call last):
1> File "C:\Users\DarkRay\Desktop\Python-3.6.4\PC\validate_ucrtbase.py", line 8, in <module>
1> from ctypes import (c_buffer, POINTER, byref, create_unicode_buffer,
1> File "C:\Users\DarkRay\Desktop\Python-3.6.4\Lib\ctypes\__init__.py", line 432, in <module>
1> pythonapi = PyDLL("python dll", None, _sys.dllhandle)
1>AttributeError: module 'sys' has no attribute 'dllhandle'

我们先来尝试找到 dllhandle 这个定义在哪儿,经过搜索可以找到其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef MS_COREDLL
/* concatenating string here */
PyDoc_STR(
"dllhandle -- [Windows only] integer handle of the Python DLL\n\
winver -- [Windows only] version number of the Python DLL\n\
"
)
#endif /* MS_COREDLL */
#ifdef MS_WINDOWS
/* concatenating string here */
PyDoc_STR(
"_enablelegacywindowsfsencoding -- [Windows only] \n\
"
)
#endif

看起来这是和一个宏有关,尝试将pythoncore项目预定义中的 _USRDLL 改为 MS_COREDLL 之后进行编译,结果变为了:

1
2
3
1>python36.lib(sysmodule.obj) : error LNK2001: unresolved external symbol _PyWin_DLLVersionString
1>python36.lib(sysmodule.obj) : error LNK2001: unresolved external symbol _PyWin_DLLhModule
1>C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.exe : fatal error LNK1120: 2 unresolved externals

从上面的错误中似乎我们设定的宏并没有生效,因为前面设定了 Py_NO_ENABLE_SHARED 会导致即便使用了 MS_COREDLL 的预定义之后,还是报上面的错误,因为产生了冲突。现在保持Pythoncore中的预定义不变(原值为:_USRDLL;Py_BUILD_CORE;Py_ENABLE_SHARED;MS_DLL_ID="$(SysWinVer)";%(PreprocessorDefinitions) ),编译Python.exe 则会出现另外的一个错误:

1
2
3
4
2>python.c
2>python36.lib(dl_nt.obj) : error LNK2005: _DllMain@12 already defined in python36.lib(dl_nt.obj)
2> Creating library C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.lib and object C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.exp
2>C:\Users\DarkRay\Desktop\Python-3.6.4\PCBuild\win32\python.exe : fatal error LNK1169: one or more multiply defined symbols found

同样是一个重复定义的问题,我们在 dl_nt.dll 找到 DllMain 的定义,同时查找一下其所有的引用,发现只有这一处,那么就放心大胆的尝试修改吧。将 dl_nt.c中的 DllMain 函数重命名为 PythonDllMain,然后进行编译,全部通过。

其他库的部分,只需要注意一下依赖关系即可,如 _sqlite3 依赖 sqlite.lib,_lzma 依赖 liblzma.lib,这些库都需要static link,最后在Python项目的链接输入中添加。

还有一些库,可以对比嵌入式版本的Python,发现没有引用 freeze_importlib 这个Pyd,所以对于这列库我们不需要去额外做处理,但是后续有时间了,可以弄清楚这个库具体是做什么用的。以及从命名上就能很容易看出库的作用的,也可以斟酌之后选择不链接他们,比如winsound这个库可以不需要,谁会在渗透过程中需要在Windows系统播放声音呢?(笑)

一切就绪之后,我们来测试下,导入这些原本是外部模块的库是否能够成功静态链接到Python.exe,如图所示是OK的。

打包Python标准库

我们还剩最后一项工作,那就是Python解释器在启动的时候会去先找Lib中用Python本身实现的标准库。较为方便的是本身Python的代码中就支持使用ZlibImport的方式来从ZIP中加载包了。那么我们可以选择两种方式,减少修改代码量的方式就是直接通过资源释放这个Zip文件,然后直接指定Sys.Path就可以了;另外一种方式修改代码量较大,需要用C代码来完成ZlibImport这个过程,然后通过Python的marshal来内存加载这些Lib,优点当然是显而易见的就是不需要释放文件。

我们这里仅仅为了演示,所以采取比较简单方法,就是释放文件这种方式,主要流程如下:

  1. 从资源中读取,释放到临时目录
  2. 在C代码中硬编码指定在临时目录中的Lib.zip
  3. 待脚本处理完退出后清理现场,删除释放到临时目录的Lib.zip

除此之外,我们回顾前面在目标中提出的要求,有一条是尽可能的减小体积。观察Lib中的Py文件可以发现,有很多是非必要的,我们可以在PythonDocs中的Lib Reference查看所有的标准库有哪些( https://docs.python.org/3/library/index.html ),在标准库中,有一些是和系统强相关的。所以可以通过两个途径来尽可能的减小体积:

  1. 删除Lib中的无用库:比如 test、unittest 和 Tkinker、IdleLib 等,前者因为我们在实际渗透中并不需要跑单元测试和其他测试脚本所以干掉;后者因为我们没有用于GUI开发的所以干掉;ensurepip 因为用不到Pip相关的所以干掉;distutils主要是用来编写Py扩展的,这里可以干掉;其他包括如 pydoc_data 这类说明的库也可以干掉
  2. 完成这些步骤之后,最终编译成Pyc也能显著的减小体积

0x03 Standing on the shoulders of giants

事实上除了前面提到过的 Static-Python 已经有很多项目尝试过静态编译的版本了,他们包括但不限于如下这些项目:

  1. https://github.com/bendmorris/static-python
  2. https://github.com/python-cmake-buildsystem/python-cmake-buildsystem
  3. https://github.com/zeha/python-superstatic
  4. https://github.com/vmurashev/BundledPython27

这里需要重点说一下的,其实是Meterpreter的Python扩展功能( 源码见 https://github.com/rapid7/metasploit-payloads/tree/master/c/meterpreter/source/extensions/python ),简单的阅读了一下源码,其通过 ReflectDllInjection 把PythonCore打包到了Patch过的DLL中,从而实现在没有Python环境的Windows下执行Python代码和模块等。实际上,@OJ 修改了Python的入口,而不是像本文这样,直接通过原本的代码进行加工,通过这个Commit,从其与原Python代码的Diff可以看出完整的修改流程 ,这里就不再进一步分析了。

0x04 Summary and todo

到目前为止一个能用的静态编译版本Python基本就完工了,但是离实际应用还有很长一段路要走:

  1. 将工程转变为Cmake项目,增加跨平台支持,以及编译选项
  2. 抽离出修改的代码为Patch,保证能随着Python版本增长而无缝迁移进去
  3. 进一步优化体积
  4. 打包常用Python安全软件到Lib.zip,在Python命令行增加单独入口

要深入的进行修改,还是需要多阅读文档,以及如 Misc/HISTORY 记录一些源码层面的改变等,这些将能帮你全面的了解Python的源码实现。在整个项目完成后会,会在 https://github.com/darkr4y/Penth0n 上开放源码,届时欢迎大家提交issue以及PR。

最后感谢 @beee 和 @zonadu 对本文的建议 ;) 以及推荐下 TizzyT&JonyJ的《想把你留在这里

附上本文中的最终产物 Python_Static_Win