文章目录
  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 Cocos on 2021/4/17.
// Copyright © 2020 Cocos. 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 Cocos on 2020/7/13.
// Copyright © 2020 Cocos. 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. 推荐阅读