Skip to main content
文章目录
  1. 1. 前言
  2. 2. 防静态分析
    1. 2.1. 移除符号
    2. 2.2. 代码混淆
    3. 2.3. 防动态调试
    4. 2.4. 防注入
  3. 3. 总结
  4. 4. 推荐阅读

前言

提高程序的逆向成本,是很多保密性高的代码所必备操作。其中高成本的方案,有加壳,虚拟机保护等,这些方案对项目的改动较大。低成本的方案一般是代码混淆之类,可以最大程度减少代码改动。

最近项目刚好需要增强代码安全性,提高逆向难度,经过几天的摸索已经找到了一个低成本高收益的方案。下面会从防静态分析,防止动态调试两个方面入手。

本文针对C语言做处理,其他语言思路一样,可能还有更便利的方法。只不过一般安全性要求高的代码都是比较底层C代码,上层应用级别的代码基本没有值得防护的,当然越高级的语言本身越复杂,静态分析也越难。

防静态分析

我们知道,Mac上的可执行程序一般是macho格式的二进制文件,现有的很多静态分析工具,比如MachOViewHoppernm等,都能轻松获取二进制文件中的符号表还有程序段的指令集。比如下面:

//
// main.c
// TestSymbol
//
// Created by Anubhav Gain on 2021/4/17.
// Copyright © 2020 Anubhav Gain. All rights reserved.
//

#include <stdio.h>
//#include <unistd.h>
#include <stdlib.h>

__attribute__ ((visibility("hidden")))
int (*localPrintf)(const char * __restrict, ...);

const char* str1 = "Hello, World\n";
const char* str2 = "Hello, Boy\n";

static int ggIntVal = 0;

static void static_say() {
printf("static hello\n");
}

void say() {
printf("hello\n");
}

int main(int argc, const char * argv[], char **envp, char **apple) {
// insert code here...
localPrintf = printf;
localPrintf("%s", str1);

char *tiny = malloc(sizeof(int));
free(tiny);

say();
static_say();

for (int i = 1; i < 10 ; i++) {
if (i % 2) {
ggIntVal *= i;
}
}
printf("\n%d\n", ggIntVal);

return 0;
}

上面两个图可以看出用工具能够轻松防汇编出指令集,Hopper还能强大能直接把指令集解释成伪代码,基本和源码一致。

所以对于保密性比较高的代码,需要尽力提高逆向难度,保护公司的代码财产。

移除符号

防止静态分析,首先要做的就是移除内部符号。内部符号,包括数据段符号(全局变量),代码段符号(内部函数名)。

移除符号可以使用strip命令

strip -x TestSymbol -o TestSymbol_nosymbol

Xcode支持配置strip,只需要配置成下面这样即可

运行一下代码,用nm看一下符号,可以发现内部符号全部消失了(类型小写的是内部符号)

敲重点了,可以看到两个全局变量的符号还可以看到(str1,str2),还有一个函数符号(say)。可以在代码中加上attribute告诉编译器把符号隐藏掉,这样符号会变成内部符号然后被剔除。

__attribute__ ((visibility("hidden")))

编译后,再一次执行nm,可以看到str1, str2两个符号已经消失了。

代码混淆

代码混淆可以有两部分,一个是加密代码中的常量字符串,另一个是在逻辑代码中增加垃圾代码,这样编译出来的指令里面就多了很多垃圾指令,别人逆向的时候看得头昏眼花的,增加逆向难度。

先说一下加密字符串的,我在知乎上看到一个手工加密并且用宏的方式实现,最大程度较少代码的改动,感觉不错,具体细节可以看这篇文章:# 纯手工混淆C/C++代码(下) ,思路就是写一个小工具,把事先写好的字符串宏,给自动生成另一个同名的宏(成为加密宏),代码中如果要加密字符串的话,就用加密宏即可。具体细节可以看上面文章,很简单的不多说了。

这样编译出来的二进制文件,也看不到明文字符串了,而且代码的改动也非常少,只需要把原来的字符串写成对于宏就可以,连工程配置都不需要修改。

此外还可以通过工具对生成一些垃圾代码,并用宏的形式附加到需要保护的函数中,这些都能提高静态分析的成本。

防动态调试

防止动态调试,主要是防止应用程序被Xcode中的attach功能附加上,这样即便没有源码,也可以对进程进行调试。要防止,那么需要先知道attach的原理。

attach大概原理其实我还没怎么研究,可以试一下,在Xcode上对还没有运行的程序执行attach操作后,运行程序时,程序的父进程是debugserverdebugserver 会利用ptrace系统调用对子进程进行调试。

如果attach正在运行的进程,其父进程不变依然是launchd ,这种情况下具体如何调试我还没弄清楚,不过最终也会使用到ptrace。

实际上,Mac还是提供了标准的接口供程序阻止其他进程对自己进行动态调试的,具体方法可以参考这篇文章,实现起来就是一个系统调用即可。

下面代码演示了两种防动态调试的方法,一种是直接调用指定标号的系统调用,另一种是调用ptrace函数

//
// main.m
// singleWindow
//
// Created by Anubhav Gain on 2020/7/13.
// Copyright © 2020 Anubhav Gain. All rights reserved.
//

#import <Cocoa/Cocoa.h>
#import <dlfcn.h>
#import <sys/types.h>

//定义一个函数指针用来接收动态加载出来的函数ptrace
typedef int (*ptrace_ptr_t)(int _request, pid_t _pid, caddr_t _addr, int _data);

#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif

void DenyAppAttach() {
//动态加载并链接指定的库
//第一个参数path为0时, 它会自动查找 $LD_LIBRARY_PATH,$DYLD_LIBRARY_PATH, $DYLD_FALLBACK_LIBRARY_PATH 和 当前工作目录中的动态链接库.
void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);

//动态加载ptrace函数,ptrace函数的参数个数和类型,及返回类型跟ptrace_ptr_t函数指针定义的是一样的
ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");

//执行ptrace_ptr相当于执行ptrace函数
ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);

//关闭动态库,并且卸载
dlclose(handle);
}

int main(int argc, const char *argv[]) {
DenyAppAttach();
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
return NSApplicationMain(argc, argv);
}

也可以直接调用指定编号的系统调用

syscall(26, 31, 0,0,0);

注意,上面代码可以另外加debug宏,不然程序也没法正常debug了。

防注入

注入这个之前的文章说过很多次了,我们的程序在运行的时候会调用依赖的动态库,这种属于dyld对程序的合法注入,当然还有一类是入侵形式的注入,以到达对源程序进行hook的效果,进而改变源程序的功能,防注入也可以在编译时带上参数实现,在 Other Linker Flags 配置上添加下面内容即可:

-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null

总结

本文从代码混淆,移除符号,配置工程三个方面提升程序的逆向成本,增加一些良好的安全防护功能,仅对项目工程做了轻微改动,可以说是低成本高收益的解决方案。

推荐阅读

纯手工混淆C/C++代码(下)

iOS安全之防止调试与防止注入 | 小镇青年

文章目录
  1. 1. 前言
  2. 2. 防静态分析
    1. 2.1. 移除符号
    2. 2.2. 代码混淆
    3. 2.3. 防动态调试
    4. 2.4. 防注入
  3. 3. 总结
  4. 4. 推荐阅读