很多应用都需要用户登录或者签名认证,这可能需要在客户端保存登录信息、签名密钥、加密算法等。如何保证这些重要信息不被窃取,算法不被破解,这些成为应用开发中很重要的内容,同样也是最容易忽视的地方。一个小小的细节可能就成为整个系统的突破口,这里从实际工程角度总结了一些容易忽视的细节和常用的方法。
钥匙扣
密钥保存在Keychain并不安全,iOS越狱后可以导出Keychain的内容。应该尽量避免存放重要信息(如:token、用户名、密码等)在Keychain中,即使要存放,也一定要加密后存放。参考http://blog.csdn.net/yiyaaixuexi/article/details/18404343
文件
保存在应用程序bundle、plist等配置文件更不安全,但可以使用隐写术等方式迷惑hackers。有请Lena示范:
两张图片看起来是一模一样的,但是右边的图片里却夹带了一些其他内容,这就是潜伏在Lena中的密码,用diff工具比较下这两张图片,你会发现不同的地方是右边的图片最后附加了一串字符:app秘诀就是“abcdefg123456”
这里的隐写方式很简单:cat文件>> Lena.jpg
,既不破坏图片原本的信息(或者损失一点点原有信息),又能附加额外的信息,这就是隐写术的原理。这里只是一个简单的例子,没有人真这么使用。有很多更隐蔽的做法,比如把要隐藏的信息分散到图片的每个像素中,例如RGB888的图片,对红蓝分量最后一个bit位进行修改并不会影响图片的质量(因为人眼对对红蓝不敏感),这样一个像素(3byte)就可以存储2bit的信息,4个像素(12byte)就可以夹带1byte的信息了。
Xcode打包时会对png图片做特殊处理,如果将密码携带在png中,可能会在使用的时候无法复原。当然现在的隐写术非常多,不只是图片能作为载体,视频、音乐等文件都可以,隐写的方法也多种多样,选择适合自己的就行,据说基地组织就是通过岛国电影传递信息的。
下面的代码很常见
这是非常危险的,因为常量会被直接编译到可执行文件的data段,只要对生成的可执行文件使用strings
、otool
等命令就可以dump出原始字符串。
为了使密码不直接出现在可执行文件中,可以对密码加密存储,使用的时候再解密。例如用AES对密码abcd1234
加密,对称密钥为kCipherKey="abcdefgh12345678"
,加密后的密码用kSecret
表示。使用密码时,再通过kCipherKey
和kSecret
计算出来:
上面的代码不再明文出现abcd1234
,而是被加密算子kCipherKey
和加密后的密钥kSecret
替代,密码只是在需要的时候临时计算出来。但是这里仍然有缺陷:加密算子kCipherKey
和加密后的密钥kSecret
仍然存储在可执行文件的data段中,留下了蛛丝马迹。我们可以给kCipherKey
取一个有迷惑性的字符串,比如"network错误, timeout"
或者使用非字符值,使其不可读。但这都不完美,不在data段中存储这些信息最好。
上面的代码稍做修改
snippet2看似和上面代码没什么区别,除了传入的参数类型变了,其余没什么变化。正是这一点带来了巨大的变化,对比一下调用CCCryptorCreate时的汇编代码:
snippet1拆机 snippet2拆机 注意CCCryptorCreate的第四个参数,对应寄存器r3
,第一段代码的r3
的值是从text段直接获取,因为这只是data段的相对地址,编译时就确定了。而再看第二段代码,出现了大量的strb
指令,分析知这段指令是把abcdefgh12345678
每个字符逐个压进执行栈的连续地址中,然后r3
取相应的连续地址的首地址。也就是说kCipherKey
不再直接存储在data段,而是打散到多个指令中,成为指令的一部分(指令在text段),当代码运行时,这些指令再把kCipherKey
原始内容逐个压入执行栈中构成字符串,然后用栈中字符串首地址作为参数传给CCCryptorCreate
,这使得每次调用时传入的字符串地址都不同。函数CCCryptorUpdate
原理也是一样。函数getSecret()
执行完之后,他的执行栈被清空,kCipherKey
和kSecret
原始信息也一起从栈中清楚,这样重要信息不会常驻内存,只是用到时才进入内存,用完立即清除,这可以有效预防内存扫描器。
上面的代码仍然不够完美,首先
getSecret
是函数形式、而且密码通过返回值传递,容易被分析破解;其次返回的密码的buffer内存需要调用者释放,代码不够整洁,而且调用者容易忘记。
这段代码稍微改造了一下,加入了一些必要的检测,让调用者更加简单,宏kAppSecret
将密码包装成NSString对象。更重要的是,buf
的内存不再是malloc
到堆上,而是alloca
到栈上(或者使用C99的变长数组),确切的说是调用者的栈,调用者不再需要手动释放内存;另外,因为kAppSecret
是宏,没有有明确的入口,静态分析更加困难。
这里用了宏定义的两个技巧:
最后一个表达式的值就是宏的返回值,使用时更像函数的返回值。
局部标签用__label__
定义。如果标签end
没有__label__
修饰,在同一个函数中多次使用kAppSecret
将产生编译错误,因为宏展开后相当于定义了多个end
标签,标签重复定义。
在客户端访问网络Server的时候,Server往往要验证请求是否来自合法的客户端,而不是攻击者伪造的请求,这就需要客户端签名。例如OAuth的签名算法。如果自己定义签名算法,不希望别人知道签名的过程,就需要保护算法不被破解。例如签名算法是HMAC-SHA1(key,MD5(data))
:
这段代码本身没有问题,但是对系统函数的直接调用导致代码容易被静态分析,用IDA、otool等静态分析工具可以很容易的知道这个函数的workflow,签名过程被轻易破解。为了防备静态分析,可以使用函数指针间接调用函数:
签名2签名类初始化的时候,保存了HASH函数的地址值,执行签名的时,通过HASH函数的地址间接调用,这样静态分析工具分析到这里的时候,只能看到调用了某个地址,而不知道调用的具体函数,隐藏了真实目的。
这里不是直接将函数地址赋值给对象属性,而是用属性的地址与函数的地址做抑或运算。这样做主要有两个原因:
直接赋值可能被编译器优化,编译器自动将使用该属性的地方替换成函数本身;
类实例的创建有随机性,属性的内存地址也具有随机性,用属性地址加密函数地址,这样属性值在每次运行时都不一样;
在安卓或其他平台还可以用对dlsym来获取函数地址:
伪代码 因为objc代码的动态性,编译器会在binary中留下类名、函数名等信息,这些信息是可以被class-dump-z
等工具提取的,友好的命名让程序猿更方便,但同时也方便了破解者。对安全相关的重要模块类,可以故意混淆类名,让人不容易轻易联想到该的真实目的。比如把类名SecurityService
改为FIFA
。一些重要模块可以使用C/C++语言实现,编译器对C/C++并不会保留类名、方法名等信息。
使用混淆的名字对使用者很不方便,例如
[[国际足联页头] initWithMD5Function:CC_MD5
SecurityService国际足联
除了静态分析,破解者还可以使用gdb的动态调试,西奥斯hook来分析代码,常用的系统加密函数、HASH函数都可能成为监控的对象,只要监控传递给他们的参数、调用栈就能轻松分析出密钥、算法等。所以使用系统的加密函数虽然节省开发时间、执行效率高,但并不是很安全,有些算法可能需要自己重写。
可以用ptrace等方法阻止gdb注入,但ptrace本身也可以被静态修改或hook。只好从多方面考虑,尽量提高安全性,比如检查binary签名是否匹配;检查手机是否越狱,越机做特殊处理等。参考http://blog.csdn.net/yiyaaixuexi/article/details/20286929
类似UPX等加壳技术在iOS中无法使用,因为iOS堆、栈内存都没有执行权限,这也是jit技术无法在iOS中使用的原因(除非苹果自己或越狱系统)。
将算法用脚本实现,脚本被编译成bytecode后,app解释执行bytecode指令,可以有效的防止动态调试,因为hackers看到的将是一条条的指令在switch case中执行,就像把图片的像素逐个地放给别人看,当他看完全部的像素后也不一定知道整张图片是什么样子。当然用脚本方式会增大开发成本,对执行效率也有一定影响,需要开发者在安全、开发成本、性能三者之间找个平衡点。
软件保护技术多种多样,比如构造花指令,甚至有硬件级的加密模块TPM(Trusted平台Module)。总之没有绝对的安全,但危险显然也只是相对的,只要提高编码意识,注意防护就可以把风险降到最低。
转自:http://tanqisen.github.io/blog/2014/06/06/how-to-prevent-app-crack/