构造超小程序

文章目录

  • 构造超小程序
    • [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 贪吃蛇

相关推荐
LZQqqqqo8 分钟前
C# 中 ArrayList动态数组、List<T>列表与 Dictionary<T Key, T Value>字典的深度对比
windows·c#·list
季春二九10 分钟前
Windows 11 首次开机引导(OOBE 阶段)跳过登录微软账户,创建本地账户
windows·microsoft
芥子沫1 小时前
Jenkins常见问题及解决方法
windows·https·jenkins
cpsvps_net18 小时前
美国服务器环境下Windows容器工作负载智能弹性伸缩
windows
甄超锋18 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
cpsvps20 小时前
美国服务器环境下Windows容器工作负载基于指标的自动扩缩
windows
网硕互联的小客服1 天前
Apache 如何支持SHTML(SSI)的配置方法
运维·服务器·网络·windows·php
etcix1 天前
implement copy file content to clipboard on Windows
windows·stm32·单片机
许泽宇的技术分享1 天前
Windows MCP.Net:基于.NET的Windows桌面自动化MCP服务器深度解析
windows·自动化·.net
非凡ghost1 天前
AMS PhotoMaster:全方位提升你的照片编辑体验
windows·学习·信息可视化·软件需求