目录

构造超小程序

文章目录

  • 构造超小程序
    • [1 编译器-大小优化](#1 编译器-大小优化)
    • [2 编译器-移除 C++ 异常](#2 编译器-移除 C++ 异常)
    • [3 链接器-移除所有依赖库](#3 链接器-移除所有依赖库)
    • [4 移除所有函数依赖](#4 移除所有函数依赖)
      • [_RTC_InitBase() _RTC_Shutdown()](#_RTC_InitBase() _RTC_Shutdown())
      • [__security_cookie __security_check_cookie()](#__security_cookie __security_check_cookie())
      • __chkstk()
    • [5 链接器-移除清单文件](#5 链接器-移除清单文件)
    • [6 链接器-移除调试信息](#6 链接器-移除调试信息)
    • [7 链接器-关闭随机基址](#7 链接器-关闭随机基址)
    • [8 移除异常目录](#8 移除异常目录)
    • [9 小结](#9 小结)
  • 附录
    • [附录1 超小 Hello world 程序](#附录1 超小 Hello world 程序)

构造超小程序

为了更方便查看编译结果的大小, 可以在 项目属性页>配置属性>生成事件>生成后事件>命令行 添加

复制代码
powershell -Command "Write-Output 目标大小:%24((Get-Item '$(TargetPath)').Length)"

链接器警告 LINK : 已指定 /LTCG,但不需要生成代码;从链接命令行中移除 /LTCG 以提高链接器性能

可以到 项目属性页>配置属性>链接器>优化>链接时间代码生成 切换 默认配置 来关闭警告

从一个打印 "Hello world!" 的程序开始

cpp 复制代码
#include <print>

int main() {
	std::println("Hello world!");
}

Release 下构建结果大小: 18432B

1 编译器-大小优化

  • 项目属性页>配置属性>C/C++>优化>优化 选 最大优化(优选大小) (/O1)
  • 项目属性页>配置属性>C/C++>优化>优选大小或速度 选 代码大小优先 (/Os)

2 编译器-移除 C++ 异常

通知编译器禁用 C++ 异常

  • 项目属性页>配置属性>C/C++>代码生成>启用C++异常 选 否 (移除 /EH)

通知 C/C++ 库不使用 C++ 异常

  • 项目属性页>配置属性>C/C++>预处理器>预处理器定义 添加 _HAS_EXCEPTIONS=0

3 链接器-移除所有依赖库

  • 项目属性页>配置属性>链接器>输入>附加依赖项 清空, 手动输入下面要依赖的 kernel32.dll
  • 项目属性页>配置属性>链接器>输入>忽略所有默认库 选 是 (/NODEFAULTLIB)

只在代码里用 pragma 添加 /NODEFAULTLIB 并不够, 默认情况下新项目会通过附加依赖项直接指名链接库, 这些库不是通过选项 /DEFAULTLIB 附加的, 用 /NODEFAULTLIB 不能消除依赖

此时链接会报错, 下面来解决链接错误

4 移除所有函数依赖

std::println() 函数依赖 ucrtbase.dll 中的函数, CRT 库相关代码和依赖比较庞大

所用到的函数可以依赖 kernel32.dll

cpp 复制代码
#include <Windows.h>

int __stdcall mainCRTStartup(void* teb) {
	HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
	WriteConsoleA(output, "Hello world!\n", 13, NULL, NULL);
	return 0;
}

现在 Release 大小: 3584B

用 Denpendencies 可以看到导入符号列表现在变得非常干净

_RTC_InitBase() _RTC_Shutdown()

Debug 下默认会开启基本运行时检查, 引入 _RTC_InitBase() 和 _RTC_Shutdown() 两个函数依赖

  • 项目属性页>配置属性>C/C++>代码生成>基本运行时检查 选 默认值 (移除 /RTC)

通常这两个函数随 msvcrt.lib 链接进入程序

部分函数尾部会被插入 cookie 检查函数, 引入 __security_check_cookie() 函数和 __security_cookie 变量依赖

  • 项目属性页>配置属性>C/C++>代码生成>安全检查 选 禁用安全检查 (/GS-)

通常这两个函数和变量随 msvcrt.lib 链接进入程序, 其中 __security_cookie 定义于 gs_cookie.c 中

__chkstk()

当栈空间占用可能超过 8KB 时(包括局部变量和 _alloca() 调用), 会引入 __chkstk() 函数依赖, 用于提交栈空间

属性页中没有相关配置开关, 需要手动填写选项来控制这个栈空间大小阈值

  • 项目属性页>配置属性>C/C++代码生成>命令行 填 /GsN 其中N是足够大的值

通常该函数在 kernelbase.dll 中导出

5 链接器-移除清单文件

清单文件用于声明系统本程序在启动时请求的资源, 包括请求管理员权限, WIndows版本兼容性, 高 DPI 声明, 视觉主题等, 但现在不需要

  • 项目属性页>配置属性>链接器>清单文件>生成清单 选 否 (/MANIFEST:NO)

.rsrc 节 将被移除

6 链接器-移除调试信息

调试信息用于帮助编译器定位每段机器码在源码文件中的位置, 移除将导致程序无法在源码中设置断点

  • 项目属性页>配置属性>链接器>调试>生成调试信息 选 否 (移除 /DEBUG)

用 /NOCOFFGRPINFO 移除调试目录

  • 项目属性页>配置属性>链接器>命令行 添加 /NOCOFFGRPINFO

7 链接器-关闭随机基址

一些防御技术依赖于随机基址, 关闭后可能导致程序更容易被攻击, 不要在生产环境关闭随机基址

关闭随机基址使得程序默认加载到 0x140000000 处, 可用 /BASE 改变默认基址

  • 项目属性页>配置属性>链接器>高级>固定基址 选 是 (/FIXED)
  • 项目属性页>配置属性>链接器>高级>随机基址 选 否 (/DYNAMICBASE:NO)

Debug 下的 .reloc 节 将被移除, 而 Release 下 .reloc 节本身就被优化合并了

8 移除异常目录

异常目录即 .pdata 节, 可指定当程序跑在某个函数崩溃后,有相对应的异常处理函数可供调用

移除并不会影响程序的正常运行

这篇 Stackoverflow 的回答指出异常目录是强制生成的, 但可以用 CFF Explorer 手动移除异常目录

9 小结

至此我们得到了一个彻底剥离所有基础设施的开发环境

程序大小从 18KB 缩小到 2KB 左右

想问更小的程序? 有的,兄弟😆

本文附录 1 给出一个超小程序, 在只使用 MSVC 工具并且不使用十六进制编辑器的前提下做到了 480B 的大小

附录

附录1 超小 Hello world 程序

C/C++ 生成的代码太长了, 用汇编吧

asm 复制代码
code
mainCRTStartup proc             ; rcx = PEB
                                ; rax = mainCRTStartup
                                ; r10, rdx, r8, r9  填函数 1~4 参数
                                ; rsp+28h ~ rsp+50h 填函数 5~9 参数
    mov byte ptr [rsp+38h],14   ; rsp+38h = Length        = 14
    mov ax,0008h
    mov qword ptr [rsp+30h],rax ; rsp+30h = Buffer        = "Hello world\n" 覆盖返回地址
    mov dword ptr [rsp+28h],eax ; rsp+28h = IoStatusBlock = Buffer
    mov r10,qword ptr [rcx+20h]
    mov r10,qword ptr [r10+28h] ;     r10 = FileHandle    = Peb->ProcessParameter->StandardOutput
    xor edx,edx                 ;     rdx = Event         = NULL
    syscall                     ;      ax = NtWriteFile   = 8
    ret
mainCRTStartup endp
end

/SECTION 先申请一个具有读, 写, 执行的全能节

然后用 /MERGE 将所有节包括代码节和数据节合并

复制代码
/SECTION:.all,ERW /MERGE:.text=.all /MERGE:.data=.all /MERGE:.rdata=.all

/ALIGN 调整节的对齐大小, 默认值 512B 会导致节的尾部留下大量空白, 最小可设为 16 (只有 1 个节时才能非 512B 对齐加载)

复制代码
/ALIGN:16

/BASE 选项设置基址到 0x80000000 用 32 位地址, 方便使用 32 指令节约代码大小

复制代码
/BASE:0x80000000

新建一个 stub.txt 做 DOS 存根程序, 用 /STUB 使 stub.txt 替换默认的 DOS 头

复制代码
/STUB:stub.txt

stub.txt 输入以下内容, 顺便在这里存放要输出的字符串

复制代码
MZ234567Hello world!
012345678901234567890123456789012345678901

NtWriteFile服务号 为 8, 将字符串设置从偏移 8 开始, 方便复用 rax 寄存器

文件以 MZ 起头, 用多余字符填充到刚好 64B 大小来满足链接器对存根程序的要求

程序大小 480B

用十六进制编辑器修改 PE 头还可以让程序更小, 懒得折腾了🐳

程序的PE头其实大部分字段都没有实际功能, 将程序的几个头部进行重叠可以得到更小的程序
Tiny PE 详细讨论了最小程序的构造方法, 得到了 133B 的程序, 文章还提到 97B 的程序, 但链接失效

只是现在 Windows 10 上加载器允许的最小程序大小是 268B
TinyPE on Win10: 268B 消息弹窗
runcalc.asm: 268B 启动附件计算器
smallEXE 收集了一些超小的程序
微软收录的文章 里也有关于最小程序的讨论
snake-qr: 2953B 贪吃蛇

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
Tee xm17 分钟前
清晰易懂的 Jenkins 安装与核心使用教程
linux·windows·macos·ci/cd·jenkins
Tee xm2 小时前
清晰易懂的 Flutter 开发环境搭建教程
linux·windows·flutter·macos·安装
martian6653 小时前
NVM 多版本Node.js 管理全指南(Windows系统)
java·开发语言·windows·node.js
404_not_found5 小时前
Windows操作系统安全配置(一)
windows·安全
kfepiza10 小时前
MBR的 扩展分区 和 逻辑分区 笔记250406
linux·windows
心灵宝贝10 小时前
Redis-x64-3.2.100.msi : Windows 安装包(MSI 格式)安装步骤
windows
W_chuanqi11 小时前
Windows环境下开发pyspark程序
windows·python·spark·conda
染指11101 天前
6.第二阶段x64游戏实战-分析人物状态
开发语言·汇编·windows·游戏·游戏逆向·x64dbg·x64游戏
tjsoft1 天前
实操日志之Windows Server2008R2 IIS7 配置Php7.4.3
windows·iis·php·2008·7.4.3
信必诺1 天前
CMake —— 2、cmake在windows与linux下动态链接库编译与链接实例(附:过程代码与CMakeLists.txt)
windows·ubuntu·cmake·动态链接库·linux\