Nuitka 编译时注入自定义 C 代码
Nuitka 是一款用 Python 实现的 Python 编译器,可以生成独立的可执行文件。其原理是生成 C 代码,然后使用 Scons 调用 C 编译器进行编译构建。据说使用 Nuitka 编译后的程序性能比 CPython 更好,和传统的打包工具 py2exe 与 PyInstaller 相比, Nuitka 的优势相当明显。
Nuitka 的使用也十分简单。比如要将下面的 main.py 文件进行打包:
python# main.py
print("Nuitka")
首先安装 Nuitka 和建议安装的三方库:
shellpip install nuitka ordered-set zstandard
另外,编译 C 代码还需要一个支持 C11 标准的 C 编译器。 Windows 系统上可以安装 Visual Studio 2022 或者 MinGW64 。然后就能用 nuitka
命令进行编译了:
shellnuitka --standalone --output-dir=build main.py
在 build\main.dist 目录下就能找到编译后的可执行文件 main.exe 以及程序所依赖的各种运行时文件。如果需要打包成单一的可执行文件,可以使用 --onefile
命令行参数:
shellnuitka --standalone --output-dir=build --onefile main.py
编译成功后,会在 build 目录下生成一个可独立运行的 main.exe 文件。这个文件其实只是个外壳,内部保存了 main.dist 目录下的所有文件,并在运行时将这些文件释放到临时目录下,然后再调用临时目录中真正的 main.exe 本体。因此在运行 onefile 打包的程序时,系统中会存在两个 main.exe 进程。
Nuitka 的使用可以说是相当简单,但是问题也来了:既然 Nuitka 生成了 C 代码,那么是否可以让用户向其中加入自定义的 C 代码呢?很遗憾, Nuitka 官方没有提供这个功能,似乎在将来也不会加入此功能。既然如此,只能通过魔改 Nuitka 的方式来实现了。
经过一番探索,一个朴素的方案在脑海中形成:如果可以在 Nuitka 生成 C 代码与执行编译之间中断执行,然后修改生成的 C 代码,最后再恢复执行就可以了。当使用 --onefile
参数编译时, Nuitka 会生成 build/main.onefile-build/static_src/OnefileBootstrap.cpp
源代码文件,此文件便是打包后可执行文件的代码入口。剩下只要修改本地的 Nuitka 代码,使其在恰当的位置中断执行即可。
在 Nuitka 代码中搜索 OnefileBootstrap
关键字,于 nuitka/build/Onefile.scons 文件中发现了如下代码:
pythondef discoverSourceFiles():
result = []
# Scan for Nuitka created source files, and add them too.
result.extend(scanSourceDir(env=env, dirname=source_dir, plugins=False))
result.extend(
scanSourceDir(
env=env,
dirname=os.path.join(source_dir, "plugins"),
plugins=True,
)
)
# Main onefile bootstrap program
result.append(
provideStaticSourceFile(
sub_path="OnefileBootstrap.c",
nuitka_src=nuitka_src,
source_dir=source_dir,
c11_mode=env.c11_mode,
)
)
return result
这里似乎就是我们要找的断点位置。在函数返回前添加如下代码:
pythondef discoverSourceFiles():
result = []
# Scan for Nuitka created source files, and add them too.
result.extend(scanSourceDir(env=env, dirname=source_dir, plugins=False))
result.extend(
scanSourceDir(
env=env,
dirname=os.path.join(source_dir, "plugins"),
plugins=True,
)
)
# Main onefile bootstrap program
result.append(
provideStaticSourceFile(
sub_path="OnefileBootstrap.c",
nuitka_src=nuitka_src,
source_dir=source_dir,
c11_mode=env.c11_mode,
)
)
import time
pause_nuitka = '../../pause_nuitka'
if os.path.exists(pause_nuitka):
print('paused')
while os.path.exists(pause_nuitka):
time.sleep(1)
return result
这几行代码的意思是:如果当前目录下存在 pause_nuitka
文件,则一直保持休眠,直到该文件被删除。
在当前目录下创建 pause_nuitka
文件后再次执行 Nuitka 打包命令。果然如期待的一样中断了:
Nuitka-Options:INFO: Used command line options: --standalone --output-dir=build --onefile .\main.py
Nuitka:INFO: Starting Python compilation with Nuitka '1.4.3' on Python '3.10' commercial grade 'not installed'.
Nuitka:INFO: Completed Python level compilation and optimization.
Nuitka:INFO: Generating source code for C backend compiler.
Nuitka:INFO: Running data composer tool for optimal constant value handling.
Nuitka:INFO: Running C compilation via Scons.
Nuitka-Scons:INFO: Backend C compiler: cl (cl 14.3).
Nuitka-Scons:INFO: Backend linking program with 6 files (no progress information available).
Nuitka-Scons:INFO: Compiled 6 C files using clcache with 6 cache hits and 0 cache misses.
Nuitka-Postprocessing:INFO: Creating single file from dist folder, this may take a while.
Nuitka-Onefile:INFO: Running bootstrap binary compilation via Scons.
Nuitka-Scons:INFO: Onefile C compiler: cl (cl 14.3).
paused
打开 build/main.onefile-build/static_src/OnefileBootstrap.cpp
文件,找到主函数入口,加入一行代码试试效果:
c#ifdef _NUITKA_WINMAIN_ENTRY_POINT
int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, wchar_t *lpCmdLine, int nCmdShow) {
int argc = __argc;
wchar_t **argv = __wargv;
#else
#if defined(_WIN32)
int wmain(int argc, wchar_t **argv) {
// 添加下面的代码
ShellAbout(NULL, TEXT("Code Injection"), TEXT("Test"), NULL);
// MessageBox(NULL, TEXT("Inject Code"), TEXT("Test"), MB_OK);
#else
int main(int argc, char **argv) {
#endif
#endif
删除 pause_nuitka
文件, Nuitka 继续执行:
Nuitka-Scons:INFO: Onefile linking program with 1 files (no progress information available).
Nuitka-Scons:INFO: Compiled 1 C files using clcache with 0 cache hits and 1 cache misses.
Nuitka-Onefile:INFO: Keeping onefile build directory 'build\main.onefile-build'.
Nuitka-Onefile:INFO: Using compression for onefile payload.
Nuitka-Onefile:INFO: Onefile payload compression ratio (31.67%) size 18276430 to 5788244.
Nuitka:INFO: Keeping dist folder 'build\main.dist' for inspection, no need to use it.
Nuitka:INFO: Keeping build directory 'build\main.build'.
Nuitka:INFO: Successfully created 'main.exe'.
运行生成的 main.exe 程序,果然成功弹出了系统关于对话框。至此,试验成功!
如果将此处的注入的 ShellAbout
替换成 MessageBox
则编译会报错:
Unexpected output from this command:
link /nologo /LTCG /CGTHREADS:16 /INCREMENTAL:NO /OUT:C:\nuitka-demo\build\main.exe Shell32.lib imagehlp.lib "static_src\OnefileBootstrap.obj"
OnefileBootstrap.obj : error LNK2001: 无法解析的外部符号 __imp_MessageBoxA
C:\nuitka-demo\build\main.exe : fatal error LNK1120: 1 个无法解析的外部命令
这是因为 Nuitka 默认的 scons 脚本仅 link 了 Shell32.lib 和 imagehlp.lib 两个库,而调用 MessageBox 需要 link User32.lib 库。在 nuitka/build/Onefile.scons 文件中添加 User32.lib 库:
pythonif win_target:
link_libraries.append("imagehlp")
link_libraries.append("User32")
如果后端编译器是 MSVC ,则可以在代码中使用 #pragma
指令:
c#pragma comment(lib, "advapi32.lib")
另一种方法是直接修改 Nuitka 源代码中 OnefileBootstrap.c 的创建模板。不过这个方案侵入性太强,如果注入代码经常变动会比较麻烦。