原文出自看雪论坛 http://bbs.pediy.com/thread-217424.htm
继续解决难题
在我们找到摆脱反调试的可能性后,我们来看看要怎么处理。这个app会做一个root检测,当我们在模拟器上运行的时候,只要我们按下OK按钮,就会退出。我们已经从UnCrackable1
看到过这样的情况。同样,我们可以修改这个行为,删掉对System.exit的调用。但这次我们打算用Frida来解决。查看反编译后的代码,我们可以看到并没有OnClickListener类,只有一个匿名的内部类。因为OnClickListener实现System.exit的调用,我们可以简单的hook这个函数,然后让它失效。
这是做这些操作的Frida脚本:
setImmediate(function() {
console.log(
"[*] Starting script"
);
Java.perform(function() {
exitClass = Java.use(
"java.lang.System"
);
exitClass.exit.implementation = function() {
console.log(
"[*] System.exit called"
);
}
console.log(
"[*] Hooking calls to System.exit"
);
});
});
再次关掉UnCrackable 2,然后用Frida来打开它:
frida -U -f sg.vantagepoint.uncrackable2 -l uncrackable2.js --no-pause
等,直到App启动并且Frida在控制台显示Hooking calls……信息。然后按下“OK”。你会得到类似下面的信息:
这样,这个app就不会被退出来了。我们可以输入一个secret string:
Uncrackable root
但我们在这输入了什么?来看MainActivity里的Android代码是如何检查正确的输入的:
用到了CodeCheck类:
我们可以看到我们在文本框输入的信息—我们的“secret string”会被传送到一个名为bar的native函数中。我们在libfoo.so库中再次找到这个函数。查找这个函数的地址(像我们之前找init函数那样),然后用radare2来反编译它:
仔细观察这些汇编代码,我们可以看到有一些字符串的比较操作,还看到一个很有意思的明文字符串 Thanks for all t. 我们在文本框里输入这个字符串发现并不是这个crackme的答案,所以我们还得继续。
查看在0x000010d8的汇编代码,我们可以看到:
所以,这里比较了eax和0x17,也就是十进制的23。如果比较不成功,就不会调用strncmp。我们也注意到在0x00010e1处,0x17作为strncmp的一个参数:
0x000010e1 ba17000000 mov edx, 0x17
要知道,按照64位linux的调用惯例,函数参数是放在——至少参数1到6——寄存器中的。尤其是前三个参数是按序放在RDI,RSI和RDX中(具体的可以看这里[PDF], p. 20 )。strncmp的头部是这样的:
int strncmp ( const char * str1, const char * str2, size_t num );
所以我们的strncmp函数会比较0x17=23个字符。我们可以推断出我们的secret string长度应该是23.
最后让我们尝试这去hook这个strncmp函数,输出它的参数。我们期望这样能给出解密后的字符串。我们要做的是:
找到strncmp在libfoo.so中的内存地址。
用Interceptor.attach来hook libfoo.so中的strncmp函数,并dump它的参数。
如果你这么做了,你会发现很多地方都有调用strncmp,所以我们要进一步限制输出。这是一段Frida代码:
在这段代码中有几点需要注意的:
这段代码调用了
Module.enumerateImportsSync
来检索对象数组,这些对象中包含了从libfoo.so导入的信息(具体请看文档)。我们迭代这个数组直至我们找到strncmp和它的地址。然后我们给它关联一个拦截器(Interceptor)。
Java中的字符串不是以null来终止的。当我们用Frida的Memory.readUtf8String方法且不提供长度来读取strncmp内存中的字符串时,Frida会以为有\0来终止,不然就一直返回一些内存垃圾,因为它不知道字符串的终点在哪里。如果我们在第二个参数中明确给出要读取的字符串长度,我们就不会遇到这个问题。
如果我们不在判断条件那里作限制,限制我们要dump的strncmp参数,我们会看到很多输出。所以我们只在strncmp的第三个参数size_t
是23,且第一个参数指向我们的输入框的时候输出。在输入框中我们会输入01234567890123456789012
(这个字符串有23个字符)。
我是怎么知道args[0]指向我们的输入,args[1]指向那个secret string的?事实上,我并不知道。我只是测试,然后在满屏的输出中找到我的输入。如果你不想跳过这部分,你可以把上面代码中的if语句删掉,然后使用Frida的hexdump输出。
这样每次调用strncmp都会输出很多hexdump,要小心哦。
这是代码的完整版本,用这个版本输出那些参数会更直观一些:
现在,打开Frida然后加载这个脚本:
frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js
输入字符串并按下verify:
Uncrackable input
在控制台,你将会看到:
很直观,我们可以看到secret string是Thanks for all the fish。把它填入输入框就能看到成功的消息啦。
解决修改的方案
最后,一些关于修改值得注意的事以及为什么我们不能用修改过的apk得到secret string。libfoo.so中的init函数包含一些初始化逻辑,这些逻辑会阻止我们去查看secret string。
如果我们再认真看看反编译后的init函数,我们会看到一行很有意思的代码:
0x00001056 c605af2f0000. mov byte [0x0000400c], 1
这个变量在后面libfoo.so的bar函数里也有用到,如果它未曾设置,代码就会跳过strncmp。
0x0000107d 803d882f0000.
cmp
byte [0x0000400c], 1 ; [0x1:1]=69
0x00001084 7570 jne 0x10f6 ;[1]
所以在它之后应该是一些布尔变量记录init函数有没有运行。如果我们希望修改过的版本能够调用strncmp,我们应该设置这个变量,或者至少阻止它跳过strncmp调用。
现在我们要重新修改,反编译apk,重写jmp指令,然后重新编译。好麻烦。因为这是Frida教程,我们将会用Frida动态改变内存。
因此,我们需要:
获取已经加载好的foo库的基址。
找到那个变量相对于库基址的偏移量(我们从反编译后的代码中可以看到这个偏移量是0x400C位)
设置这个变了为1.
所以,在Frida这边:
下面是针对这个apk修改后版本的完整代码:
现在运行这个app,用Frida加载上面的脚本,然后再次输入
01234567890123456789012
。按下Verify。app会调用strncmp,然后我们就能够看到那个secret string。
愿你能从中获得乐趣。
评论、批评、建议等请移步Twitter。感谢阅读。
注:感谢@oleavr帮我指出一个bug已经告诉我在Frida中正确处理指针的方法。
本文由 看雪翻译小组 lumou 编译