背景 && 摘要

就是想多做几道 Android 逆向题目了.

环境

macos 10.13 + IDA 7.0 + Jadx

纯静态分析求解 flag

模拟器安装程序后直接挂掉,运行不了,于是静态分析之. 用 jadx 打开改程序,如下 没啥东西,就是加载了一个 so 库, 用 IDA 打开这个 so 库. 定位到 JNI_OnLoad 函数中, 映入眼帘的是一大坨数字,如下 后来分析后,这个函数式用于从这一大坨数值中解密出来一个字符串, 第一个参数为数值的个数,其他参数就是数值了. 用 IDA F5 得到这个函数如下


_BYTE *decrypt_str(int a1, ...)
{
  int v1; // r4
  _BYTE *result; // r0
  _BYTE *mem; // r2
  int i; // r3
  int num; // r1
  int varg_r0; // [sp+28h] [bp-10h]
  va_list varg_r1; // [sp+2Ch] [bp-Ch]

  va_start(varg_r1, a1);
  varg_r0 = a1;
  v1 = 2 * a1;
  result = j_malloc(2 * a1 + 1);
  mem = result;
  i = 0;
  while ( i < varg_r0 )
  {
    num = *(varg_r1 + i++);
    *mem = ~((~num | ((num & 0xFF00) >> 8)) & (~((num & 0xFF00) >> 8) | num));
    mem[1] = HIBYTE(num) ^ ((num & 0xFF0000u) >> 16);
    mem += 2;
  }
  result[v1] = 0;
  return result;
}

然后手动用 python 实现一个


#! /usr/bin/env python3
#! -*- coding:utf-8 -*-

def solve(lst):
    result = []
    for numstr in lst:
        num = int(numstr)
        chaint = ~((~num | ((num & 0xFF00) >> 8)) & (~((num & 0xFF00) >> 8) | num))
        cha = chaint & 0xff
        chbint = ((num & 0xFF000000) >> 24) ^ ((num & 0xFF0000) >> 16)
        chb = chbint & 0xff
        result.append(chr(cha))
        result.append(chr(chb))
    rst = ''.join(result)
    print(rst)
    return rst

传入的参数为数值列表,比如 solve([1142966885, 90266995, 459477109])输出的解密字符串为 /d.dex. 转换到 python 代码的过程是这样的,我们看 C 代码中是对每一个整数做一下组合运算赋值给 *mem, 而我们由 mem += 2 实际上可以确定一次得到两个字节, *mem 处确定的是第一个字节, 由于 mem 的元素是字节类型的,所以 *mem 处实际上得到了表达式结果的最低字节, 而 mem[1] 则确定第二个字节. 在 IDA 中, HIBYTE(num) 取得整数 num 的最高一字节. 所以, 相应的在 python 中就是 (num & 0xFF000000) >> 24,剩下的转换就是照搬了.

我们可以对所有字符串进行解密,解密后大致浏览一下 JNI_OnLoad 中的代码, 可以看到这里转储生成一个 dex 文件,如下: 根据 fwrite 的参数,我们知道是从 libcook.so 偏移 0x2F18 处读入 0x15A8 个字节到某个文件, 这个文件是真实的 dex 文件,但这个文件实际上并不能操作成功, 不过这里我们不关心,因为我们可以手动提取. 然后后面是一系列其他操作,比如移除生成的 dex 文件操作,获取类加载器等, 不过对我们用处不大,在 JNI_OnLoad 末尾有这么一个操作如下 这里的 decrypt_app 函数实际上是对 dex 文件进行了进一步解密,看看这个函数的样子, 如下

因为在 JNI_OnLoad 中移除了 dex 文件,但是该 dex 文件还位于内存中, 为了得到 dex 的内容,遍历进程空间 /proc/self/maps 搜索 d.dex, 并且只有当搜索到的内存区域前四个字节为 'dex\n0' 时才算找到. 主要解密代码就是红框部分. 上图中有一个很奇怪的地方是, IDA 给了我们一个 v9=&i[-v6], -v6 是什么鬼,此时回到汇编中看一下 红框中的部分就是上面的伪代码部分,也就是说 v9 实际上是 0. 然后根据伪代码可以知道复制 libcook.so 偏移 0x2E88 处的 0x90 个字节, 将每个字节和 0x5A 异或处理后依次存放到 dex 偏移 0x720 处, 所以我们可以用 python 来实现这一功能:


with open("./lib/armeabi/libcook.so""rb"as libcook:
    libcook.seek(0x2f18)
    patched_ddex = bytearray(libcook.read(0x15a8))

data = [
0x490x5E0x520x5A0x790x1B0x7B0x5A0x7C,
0x5B0x660x5A0x5A0x5A0x480x5A0x6F0x1A,
0x550x5A0x120x580x5B0x5A0xE90x5F0x5A,
0x120x590x590x5A0xED0x680xD70x780x15,
0x580x5B0x5A0x820x5A0x5A0x5B0x720xA8,
0x780x5A0x450x5A0x2A0x7A0x7E0x5A0x4A,
0x5A0x400x5B0x5A0x5A0x340x7A0x7F0x5A,
0x4A0x5A0x500x5A0x630x5A0x470x5A0xE ,
0xA0x580x5A0x340x4A0x5B0x5A0x5A0x5A ,
0x560x5A0x780x5B0x450x5A0x380x580x5E,
0x5A0xE90x5F0x5A0x2B0x7A0x780x5A0x68,
0x5A0x560x580x2A0x7A0x7E0x5A0x7B0x5A,
0x480x480x2B0x6A0x4F0x5A0x4A0x580x56,
0x5A0x340x4A0x4C0x5A0x5A0x5A0x540x5A,
0x5A0x590x5B0x5A0x520x5A0x5A0x5A0x40,
0x410x440x5E0x4F0x580x480x5D
]
for i in range(len(data)):
    patched_ddex[0x720 + i] = data[i] ^ 0x5A;
with open("./patached_ddex.dex""wb"as _:
    _.write(patched_ddex)

上述代码里面第一个 with 语句用于初步将 dex 文件读取出来, data 是从 0x2E88 处取出来的 0x90 个字节. 最后模拟执行异或运算即可,生成的 patched_ddex.dex 即为最终 dex 文件.

使用 jadx 反编译该 dex 文件,我们得到如下文件目录 主要是四个 java 文件. 看一下各个文件是什么作用.

S.java 文件


public class S {
    public static String I = "FLAG_FACTORY";
    public static Activity a;

    public S(final Activity activity) {
        int i = 0;
        a = activity;
        Context applicationContext = activity.getApplicationContext();
        GridLayout gridLayout = (GridLayout) activity.findViewById(R.id.foodLayout);
        String[] strArr = new String[]{"🍕""🍬""🍞""🍎""🍅""🍙""🍝",
            "🍓""🍈""🍉""🌰""🍗""🍤""🍦""🍇""🍌""🍣""🍄""
                🍊", "🍒", "🍠", "🍍", "🍆", "🍟", "🍔", "🍜", "🍩", "🍚", "🍨",
            "🌾""🌽""🍖"};
        while (i < 32) {
            View button = new Button(applicationContext);
            LayoutParams layoutParams = new GridLayout.LayoutParams();
            layoutParams.width = (int) TypedValue.applyDimension(160.0f, activity.getResources().getDisplayMetrics());
            layoutParams.height = (int) TypedValue.applyDimension(160.0f, activity.getResources().getDisplayMetrics());
            button.setLayoutParams(layoutParams);
            button.setText(strArr[i]);
            button.setOnClickListener(new OnClickListener() {
                public void onClick(View view) {
                    view.playSoundEffect(0);
                    Intent intent = new Intent(S.I);
                    intent.putExtra("id", i);
                    activity.sendBroadcast(intent);
                }
            });
            gridLayout.addView(button);
            i++;
        }
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(I);
        activity.registerReceiver(new F(activity), intentFilter);
    }
}

设置了 32 中食品,给没这个食品添加了一个事件监听器,每个食品有一个食品 id, 最终该 id 将会通过 intent 发送到 F.java 文件处理, 我们看看 F.java 文件.

F.java


public class F extends BroadcastReceiver {
    private static byte[] flag = new byte[]{(byte) -19, (byte116, (byte58,
        (byte108, (byte) -1, (byte33, (byte9, (byte61, (byte) -61,
        (byte) -37, (byte108, (byte) -123, (byte3, (byte35, (byte97,
        (byte) -10, (byte) -15, (byte15, (byte) -85, (byte) -66, (byte) -31,
        (byte) -65, (byte17, (byte79, (byte31, (byte25, (byte) -39,
        (byte95, (byte93, (byte1, (byte) -110, (byte) -103, (byte) -118,
        (byte) -38, (byte) -57, (byte) -58, (byte) -51, (byte) -79};
    private Activity a;
    private int c;
    private byte[] k = new byte[8];

    public F(Activity activity) {
        this.a = activity;
        for (int i = 0; i < 8; i++) {
            this.k[i] = (byte0;
        }
        this.c = 0;
    }

    public void onReceive(Context context, Intent intent) {
        this.k[this.c] = (byte) intent.getExtras().getInt("id");
        cc();
        this.c++;
        if (this.c == 8) {
            this.c = 0;
            this.k = new byte[8];
            for (int i = 0; i < 8; i++) {
                this.k[i] = (byte0;
            }
        }
    }

    public void cc() {
        byte[] bArr = new byte[]{(byte26, (byte27, (byte30, (byte4, (byte21, (byte2, (byte18, (byte7};
        for (int i = 0; i < 8; i++) {
            bArr[i] = (byte) (bArr[i] ^ this.k[i]);
        }
        if (new String(bArr).compareTo("\u0013\u0011\u0013\u0003\u0004\u0003\u0001\u0005") == 0) {
            Toast.makeText(this.a.getApplicationContext(), new String(ℝ.ℂ(flag, this.k)), 1).show();
        }
    }
}

仿佛看到了 flag, F 是在 onReceive 中收到来自 intent 的消息的,可以看到, 收到消息后对私有成员变量 k 设置了对应的食物 id, 然后调用 cc() 函数, 看到 cc 函数里面的 if 语句, 如果比较成功, 则调用 R.java 中的方法打印 flag, 我们看到 cc 函数实际上就是执行了异或操作,这些操作是可逆的, 所以我们知道 bArr 和 k 进行异或操作后得到的字节数组的字符串形式即为 "\u0013\u0011\u0013\u0003\u0004\u0003\u0001\u0005" , 那么我们得到该字符串的字节数组,再与 bArr 进行同样的异或操作, 我们便可以得到 k 的值,也就是正确的食物 id 序列, 将该序列带入 R.C 函数即可反向得到 flag. 我把 R.C 函数提取出来,然后单独写成一个 java 文件(solve.java) 如下:


/*
 * makefile
all:
    @javac solve.java
    @java solve
*/
public class solve {
    public static byte[] C(byte[] bArr, byte[] bArr2) {
        byte[] bArr3 = new byte[256];
        byte[] bArr4 = new byte[256];
        int i = 0;
        int i2 = 0;
        while (i2 != 256) {
            bArr3[i2] = (byte) i2;
            bArr4[i2] = bArr2[i2 % bArr2.length];
            i2++;
        }
        int i3 = i2 ^ i2;
        i2 = 0;
        while (i3 != 256) {
            i2 = ((i2 + bArr3[i3]) + bArr4[i3]) & 255;
            bArr3[i2] = (byte) (bArr3[i2] ^ bArr3[i3]);
            bArr3[i3] = (byte) (bArr3[i3] ^ bArr3[i2]);
            bArr3[i2] = (byte) (bArr3[i2] ^ bArr3[i3]);
            i3++;
        }
        bArr4 = new byte[bArr.length];
        i3 ^= i3;
        i2 ^= i2;
        while (i != bArr.length) {
            i3 = (i3 + 1) & 255;
            i2 = (i2 + bArr3[i3]) & 255;
            bArr3[i2] = (byte) (bArr3[i2] ^ bArr3[i3]);
            bArr3[i3] = (byte) (bArr3[i3] ^ bArr3[i2]);
            bArr3[i2] = (byte) (bArr3[i2] ^ bArr3[i3]);
            bArr4[i] = (byte) (bArr[i] ^ bArr3[(bArr3[i3] + bArr3[i2]) & 255]);
            i++;
        }
        return bArr4;
    }

    private static byte[] flag = new byte[]{(byte) -19, (byte116, (byte58,
        (byte108, (byte) -1, (byte33, (byte9, (byte61, (byte) -61,
        (byte) -37, (byte108, (byte) -123, (byte3, (byte35, (byte97,
        (byte) -10, (byte) -15, (byte15, (byte) -85, (byte) -66, (byte) -31,
        (byte) -65, (byte17, (byte79, (byte31, (byte25, (byte) -39,
        (byte95, (byte93, (byte1, (byte) -110, (byte) -103, (byte) -118,
        (byte) -38, (byte) -57, (byte) -58, (byte) -51, (byte) -79};

    public static void main(String []args) {
        byte[] k = "\u0013\u0011\u0013\u0003\u0004\u0003\u0001\u0005".getBytes();
        //System.out.println(k.length);
        byte[] bArr = new byte[]{(byte26, (byte27, (byte30, (byte4, (byte21, (byte2, (byte18, (byte7};
        for (int i = 0; i < 8; i++) {
            bArr[i] = (byte) (bArr[i] ^ k[i]);
            //System.out.println(k[i]);
        }
        System.out.println(new String(C(flag,bArr)));
    }
}

运行后即可得到 flag 为 CTF{bacon_lettuce_tomato_lobster_soul}.

Reference

  1. Google CTF 2017 : Food




Contact me by dXAyZ2Vla0AxNjMuY29tCg==
OR
Follow me on Sinablog

Copyright ©2017 by bugnofree All rights reserved.