文章目录
- 前言
- APK反编译
- SO层反汇编
- C伪代码分析
- 总结
前言
前面我在 移动安全-APK反编译 一文中引用郭霖老师的《Android第一行代码》一书介绍了 Android 的 So 层文件的作用和意义,先进行回顾一下:
本文的目的在于记录攻防世界中一道 CTF 逆向题目 easy-so,从中学习如何借助 IDA 反汇编神器对 Android SO 文件进行反汇编和分析。
APK反编译
1、题目链接以再上方,附件 APK的下载地址,题目如下:
2、下载后在模拟器进行安装后,主界面如下:
在输入框输入任意字符后点击 Check 按钮,显示“验证失败”,显然题目需要逆向分析 APK 文件并输入正确的字符串进行 Check 才算成功!3、开始逆向分析,先将 APK 文件拖入 jadx-gui
反编译神器,查看 Java 源码,找到主活动类:
源码如下:
package com.testjava.jack.pingan2;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
/* access modifiers changed from: protected */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView((int) R.layout.activity_main);
((Button) findViewById(R.id.button)).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (cyberpeace.CheckString(((EditText) MainActivity.this.findViewById(R.id.editText)).getText().toString()) == 1) {
Toast.makeText(MainActivity.this, "验证通过!", 1).show();
} else {
Toast.makeText(MainActivity.this, "验证失败!", 1).show();
}
}
});
}
}
4、跟进并查看cyberpeace.CheckString
函数,看到了熟悉的native
关键字,CheckString
函数从 So 层的cyberpeace
库中加载:
完整代码如下:
package com.testjava.jack.pingan2;
public class cyberpeace {
public static native int CheckString(String str);
static {
System.loadLibrary("cyberpeace");
}
}
那么,接下来的任务,自然就是上 IDA 对 So 文件进行反汇编并寻找对应的CheckString
函数并分析其实现逻辑。
SO层反汇编
IDA 反汇编神器是无法直接加载 APK 文件的,那么,如何先获取到目标 APK 的 So 层文件呢?不知道读者有没有注意到,实际上我上面的截图已经出现了咱们想要的 So 文件了:
没错,APK 解压缩后的lib
资源文件夹即为应用所使用到的库文件(.so
文件,C/C++
代码库文件,JNI
部分),为了获取该 So 文件可以修改 apk 文件的后缀为 zip 并解压缩:
随后拖进将 32 位或者 64 位的 So 文件拖入 IDA 进行反汇编即可:
1、使用 IDA 的快捷键 Alt+T
打开搜索功能,搜索上述的CheckString
函数的关键字,将自动定位到Java_com_testjava_jack_pingan2_cyberpeace_CheckString
处:
2、按下 Tab
或者 F5
键,获得上述关键函数的伪代码:
完整的伪代码如下:
_BOOL4 __cdecl Java_com_testjava_jack_pingan2_cyberpeace_CheckString(int a1, int a2, int a3)
{
size_t v3; // edi
char *v4; // esi
size_t v5; // edi
char v6; // al
char v7; // al
size_t v8; // edi
char v9; // al
const char *v11; // [esp+18h] [ebp-14h]
v11 = (const char *)(*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);
v3 = strlen(v11);
v4 = (char *)malloc(v3 + 1);
memset(&v4[v3], 0, v3 != -1);
memcpy(v4, v11, v3);
if ( strlen(v4) >= 2 )
{
v5 = 0;
do
{
v6 = v4[v5];
v4[v5] = v4[v5 + 16];
v4[v5++ + 16] = v6;
}
while ( v5 < strlen(v4) >> 1 );
}
v7 = *v4;
if ( *v4 )
{
*v4 = v4[1];
v4[1] = v7;
if ( strlen(v4) >= 3 )
{
v8 = 2;
do
{
v9 = v4[v8];
v4[v8] = v4[v8 + 1];
v4[v8 + 1] = v9;
v8 += 2;
}
while ( v8 < strlen(v4) );
}
}
return strcmp(v4, "f72c5a36569418a20907b55be5bf95ad") == 0;
}
可以看到,关键是最终要使得strcmp(v4, "f72c5a36569418a20907b55be5bf95ad") == 0
为真。
C伪代码分析
上面获得 So 层的核心处理函数的逻辑代码之后,下面开始进行代码分析。
1、大胆推测const char * v11
是传入的字符串,接下来逐个分析代码逻辑:
v11 = (const char *)(*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);
v3 = strlen(v11); //取变量v3=v11的字符串长度,假设v11="abcd",v3=4
v4 = (char *)malloc(v3 + 1); //为字符指针v4请求一块长度为v3+1的内存空间
memset(&v4[v3], 0, v3 != -1); //将v4扩增一倍并后面扩增的部分初始化为0,此行代码结束,v4=----0000
memcpy(v4, v11, v3); //将v11的内容复制到v4中
if ( strlen(v4) >= 2 ) //若v4的长度大于等于2则执行花括号内的内容
{
v5 = 0; //初始化v5=0
do //执行循环
{
v6 = v4[v5]; //从第0个开始读取v4的每个字符
v4[v5] = v4[v5 + 16]; //逐个将v4的第v5个字符与第v5+16个字符交换位置
v4[v5++ + 16] = v6; //v5自增1
}
while ( v5 < strlen(v4) >> 1 );
}
假设传入字符串为abcd
,则上述代码执行完之后的v4
为cdab
。
2、继续分析接下来的代码:
v7 = *v4; //指针v7指向v4
if ( *v4 ) //v4若存在,执行花括号内的逻辑
{
*v4 = v4[1];
v4[1] = v7;
if ( strlen(v4) >= 3 )
{
v8 = 2; //初始化v8=2
do
{
v9 = v4[v8]; //取v4[v8]的地址
v4[v8] = v4[v8 + 1]; //逐个将v4的第v8个字符与第v8+1个字符交换位置
v4[v8 + 1] = v9;
v8 += 2;
}
while ( v8 < strlen(v4) );
}
}
这段代码很简单,就是两两交换。综上,以上代码转换的关键就是两个循环体,第一个循环体将字符串从中间砍断,头拼接到尾,第二个循环体将字符串两两交换。
根据上述逻辑分析,我们直接可手动转换得到flag的值:
- 将
f72c5a36569418a20907b55be5bf95ad
两两交换得到7fc2a5636549812a90705bb55efb59da
; - 将
7fc2a5636549812a90705bb55efb59da
从中间砍断,头拼接到尾,得到90705bb55efb59da7fc2a5636549812a
; - 将上述得到的字符串换成 “flag{XXXX}” 的形式就是需要提交平台的 flag 值。
返回到 APP 中输入上述得到的字符串,验证成功:
【附】有个大佬写了个解密的 Py 脚本:
data = list('f72c5a36569418a20907b55be5bf95ad') //把字符串传入,并转为列表
for i in range(len(data)//2): //第一个循环体
a = data[i]
data[i] = data[i+16]
data[i+16] = a
for i in range(0,len(data),2): //第二个循环体
a2 = data[i]
data[i]=data[i+1]
data[i+1]=a2
key=''.join(data) //拼接字符串
print("flag{"+key+"}") //把题目中的flag{}和最终字符串拼接并输出
执行效果如下:
总结
至此,该题逆向分析结束,从中可以看到如何对 Android APP 的 So 层函数进行基础的逆向分析。过程对于初学 IDA 逆向的本菜鸟来说稍微有点繁琐复杂……那么问题来了,能不能直接 Frida hook 该 So 层函数并直接修改返回值(就像 hook Java 层的函数一样),从而直接绕过校验呢?等待进一步学习……