文章目录

  • 前言
  • APK反编译
  • SO层反汇编
  • C伪代码分析
  • 总结


前言

前面我在 移动安全-APK反编译 一文中引用郭霖老师的《Android第一行代码》一书介绍了 Android 的 So 层文件的作用和意义,先进行回顾一下:

android 反编译so文件 手机反编译so_v8

android 反编译so文件 手机反编译so_反汇编_02

本文的目的在于记录攻防世界中一道 CTF 逆向题目 easy-so,从中学习如何借助 IDA 反汇编神器对 Android SO 文件进行反汇编和分析。

APK反编译

1、题目链接以再上方,附件 APK的下载地址,题目如下:

android 反编译so文件 手机反编译so_反汇编_03

2、下载后在模拟器进行安装后,主界面如下:

android 反编译so文件 手机反编译so_字符串_04


在输入框输入任意字符后点击 Check 按钮,显示“验证失败”,显然题目需要逆向分析 APK 文件并输入正确的字符串进行 Check 才算成功!3、开始逆向分析,先将 APK 文件拖入 jadx-gui 反编译神器,查看 Java 源码,找到主活动类:

android 反编译so文件 手机反编译so_v8_05

源码如下:

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库中加载:

android 反编译so文件 手机反编译so_android 反编译so文件_06

完整代码如下:

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 文件了:

android 反编译so文件 手机反编译so_v8_07

没错,APK 解压缩后的lib 资源文件夹即为应用所使用到的库文件(.so文件,C/C++代码库文件,JNI 部分),为了获取该 So 文件可以修改 apk 文件的后缀为 zip 并解压缩:

android 反编译so文件 手机反编译so_字符串_08


随后拖进将 32 位或者 64 位的 So 文件拖入 IDA 进行反汇编即可:

android 反编译so文件 手机反编译so_android 反编译so文件_09


1、使用 IDA 的快捷键 Alt+T 打开搜索功能,搜索上述的CheckString函数的关键字,将自动定位到Java_com_testjava_jack_pingan2_cyberpeace_CheckString处:

android 反编译so文件 手机反编译so_反汇编_10

2、按下 Tab 或者 F5 键,获得上述关键函数的伪代码:

android 反编译so文件 手机反编译so_v8_11

完整的伪代码如下:

_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,则上述代码执行完之后的v4cdab

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的值:

  1. f72c5a36569418a20907b55be5bf95ad两两交换得到7fc2a5636549812a90705bb55efb59da
  2. 7fc2a5636549812a90705bb55efb59da从中间砍断,头拼接到尾,得到90705bb55efb59da7fc2a5636549812a
  3. 将上述得到的字符串换成 “flag{XXXX}” 的形式就是需要提交平台的 flag 值。

返回到 APP 中输入上述得到的字符串,验证成功:

android 反编译so文件 手机反编译so_android 反编译so文件_12


【附】有个大佬写了个解密的 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 反编译so文件 手机反编译so_字符串_13

总结

至此,该题逆向分析结束,从中可以看到如何对 Android APP 的 So 层函数进行基础的逆向分析。过程对于初学 IDA 逆向的本菜鸟来说稍微有点繁琐复杂……那么问题来了,能不能直接 Frida hook 该 So 层函数并直接修改返回值(就像 hook Java 层的函数一样),从而直接绕过校验呢?等待进一步学习……