第1章:系统调用与API安全
系统调用和API,说白了就是用户态程序跟操作系统内核打交道的「门」。这门开得好,程序跑得顺;这门没锁好,攻击者就能溜进来。我做了这么多年安全审计,见过太多因为API调用不当导致的漏洞——有些甚至就是一行代码的事。
今天咱们就聊聊这个话题。我会结合自己踩过的坑,把系统调用和API安全的核心要点讲清楚。
1.1 系统调用的安全使用
系统调用是用户态进入内核态的唯一通道。每个系统调用都有明确的参数和返回值,但问题往往就出在「你以为你传对了」。
- 参数未校验直接传入内核
- 缓冲区大小与预期不符
- 返回值未检查就继续使用
- 竞态条件(TOCTOU)问题
我个人习惯,每次写系统调用前都会问自己三个问题:参数从哪里来?返回值可能是什么?如果失败了我该怎么办?
举个例子,read() 这个调用看起来简单吧?
// 不安全的写法
char buf[1024];
read(fd, buf, 1024); // 假设一定读到1024字节
// 安全的写法
char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n < 0) {
// 处理错误
perror("read failed");
return -1;
}
buf[n] = '\0'; // 确保字符串终止
我曾经在一个嵌入式项目里见过类似的代码,开发人员没检查返回值,结果读取失败后继续处理未初始化的缓冲区——那个bug查了我整整两天。
1.2 Windows API安全
Windows的API设计得挺丰富,但丰富也意味着容易出错。我重点讲两个最常用的:CreateProcess 和 CreateFile。
1.2.1 CreateProcess 安全使用
这个API用来创建进程。你想想看,如果攻击者能控制你创建的是什么进程,那后果不堪设想。
- 始终指定完整路径,不要依赖搜索路径
- 使用
lpApplicationName而非lpCommandLine - 设置合适的
dwCreationFlags - 正确管理句柄继承
// 不安全的写法
CreateProcess(NULL, "notepad.exe", ...); // 路径可被劫持
// 安全的写法
WCHAR appPath[] = L"C:\\Windows\\System32\\notepad.exe";
CreateProcess(appPath, NULL, NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
我记得有一次审计一个第三方组件,他们用 CreateProcess 启动一个子进程,但没禁用句柄继承。结果子进程意外持有了父进程的敏感文件句柄——这就是典型的权限提升漏洞。
1.2.2 CreateFile 安全使用
文件操作是API安全的重灾区。CreateFile 的参数多,容易出错。
| 参数 | 常见错误 | 安全建议 |
|---|---|---|
| dwDesiredAccess | 授予过多权限 | 按需申请,最小权限原则 |
| dwShareMode | 未设置共享模式 | 根据场景设置 FILE_SHARE_READ 等 |
| lpSecurityAttributes | 使用 NULL | 显式指定安全描述符 |
| dwCreationDisposition | 误用 OPEN_ALWAYS | 明确意图,避免意外创建 |
1.3 POSIX API安全
Linux/Unix 下的API安全,核心在于理解「一切皆文件」和「权限模型」。
我挑几个容易出问题的点说说:
- exec 系列函数:使用
execve而非system,避免shell注入 - 文件操作:使用
open()时指定 O_CLOEXEC 防止句柄泄露 - 临时文件:使用
mkstemp()而非mktemp(),避免竞态条件 - 信号处理:信号处理函数中只调用异步信号安全函数
// 不安全的临时文件创建
char template[] = "/tmp/myapp_XXXXXX";
mktemp(template); // 存在TOCTOU漏洞
int fd = open(template, O_RDWR | O_CREAT, 0600);
// 安全的做法
char template[] = "/tmp/myapp_XXXXXX";
int fd = mkstemp(template); // 原子操作
if (fd == -1) {
perror("mkstemp failed");
exit(1);
}
嗯,这里要注意:mktemp 只是生成一个文件名,但生成和打开之间有时间窗口。攻击者可以在这个窗口内创建符号链接指向你的敏感文件。这就是典型的TOCTOU(Time of Check, Time of Use)漏洞。
1.4 API钩子检测
API钩子(Hooking)是安全监控和攻击者都常用的技术。作为防御方,我们需要能检测到恶意的API钩子。
常见的钩子技术包括:
- IAT钩子:修改导入地址表
- Inline钩子:修改函数前几个字节
- SSDT钩子:修改系统服务描述表(内核级)
检测方法我总结了几种:
| 检测方法 | 原理 | 适用场景 |
|---|---|---|
| 代码完整性校验 | 计算函数代码的哈希值 | Inline钩子检测 |
| IAT比对 | 对比内存中的IAT与磁盘上的原始IAT | IAT钩子检测 |
| 系统调用号校验 | 验证系统调用号是否被篡改 | SSDT钩子检测 |
| 执行时间分析 | 测量API调用耗时,异常延迟可能表示被钩子 | 通用检测 |
// 简单的IAT钩子检测示例(伪代码)
bool CheckIATHook(const char* moduleName, const char* funcName) {
// 从磁盘加载原始模块
HMODULE diskModule = LoadLibraryEx(moduleName, NULL, DONT_RESOLVE_DLL_REFERENCES);
// 获取原始函数地址
FARPROC originalFunc = GetProcAddress(diskModule, funcName);
// 获取内存中的函数地址
FARPROC currentFunc = GetProcAddress(GetModuleHandle(moduleName), funcName);
// 比对地址
if (originalFunc != currentFunc) {
return true; // 检测到钩子
}
return false;
}
我曾经在一个安全产品中遇到过这种情况:攻击者先钩住了 GetProcAddress,然后我们的检测代码无论怎么查,返回的都是「正常」——实际上函数早就被篡改了。后来我们改用直接读取PE结构的方式,才绕过了这层伪装。
知识体系总览
下面这张图概括了本章的核心内容,我建议你保存下来,写代码时对照着看:
这张图把四个核心模块串起来了。你可以看到,无论是哪种API,安全的核心思路都是相通的:信任边界要清晰,输入要校验,失败要处理。
公众号:蓝海资料掘金营,微信deep3321