就是想多做几道 Android 逆向题目了.
macos 10.13 + IDA 7.0 + Jadx
模拟器安装程序后直接挂掉,运行不了,于是静态分析之. 用 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 = [
0x49, 0x5E, 0x52, 0x5A, 0x79, 0x1B, 0x7B, 0x5A, 0x7C,
0x5B, 0x66, 0x5A, 0x5A, 0x5A, 0x48, 0x5A, 0x6F, 0x1A,
0x55, 0x5A, 0x12, 0x58, 0x5B, 0x5A, 0xE, 9, 0x5F, 0x5A,
0x12, 0x59, 0x59, 0x5A, 0xED, 0x68, 0xD7, 0x78, 0x15,
0x58, 0x5B, 0x5A, 0x82, 0x5A, 0x5A, 0x5B, 0x72, 0xA8,
0x78, 0x5A, 0x45, 0x5A, 0x2A, 0x7A, 0x7E, 0x5A, 0x4A,
0x5A, 0x40, 0x5B, 0x5A, 0x5A, 0x34, 0x7A, 0x7F, 0x5A,
0x4A, 0x5A, 0x50, 0x5A, 0x63, 0x5A, 0x47, 0x5A, 0xE ,
0xA, 0x58, 0x5A, 0x34, 0x4A, 0x5B, 0x5A, 0x5A, 0x5A ,
0x56, 0x5A, 0x78, 0x5B, 0x45, 0x5A, 0x38, 0x58, 0x5E,
0x5A, 0xE, 9, 0x5F, 0x5A, 0x2B, 0x7A, 0x78, 0x5A, 0x68,
0x5A, 0x56, 0x58, 0x2A, 0x7A, 0x7E, 0x5A, 0x7B, 0x5A,
0x48, 0x48, 0x2B, 0x6A, 0x4F, 0x5A, 0x4A, 0x58, 0x56,
0x5A, 0x34, 0x4A, 0x4C, 0x5A, 0x5A, 0x5A, 0x54, 0x5A,
0x5A, 0x59, 0x5B, 0x5A, 0x52, 0x5A, 0x5A, 0x5A, 0x40,
0x41, 0x44, 0x5E, 0x4F, 0x58, 0x48, 0x5D
]
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 文件. 看一下各个文件是什么作用.
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(1, 60.0f, activity.getResources().getDisplayMetrics());
layoutParams.height = (int) TypedValue.applyDimension(1, 60.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 文件.
public class F extends BroadcastReceiver {
private static byte[] flag = new byte[]{(byte) -19, (byte) 116, (byte) 58,
(byte) 108, (byte) -1, (byte) 33, (byte) 9, (byte) 61, (byte) -61,
(byte) -37, (byte) 108, (byte) -123, (byte) 3, (byte) 35, (byte) 97,
(byte) -10, (byte) -15, (byte) 15, (byte) -85, (byte) -66, (byte) -31,
(byte) -65, (byte) 17, (byte) 79, (byte) 31, (byte) 25, (byte) -39,
(byte) 95, (byte) 93, (byte) 1, (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] = (byte) 0;
}
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] = (byte) 0;
}
}
}
public void cc() {
byte[] bArr = new byte[]{(byte) 26, (byte) 27, (byte) 30, (byte) 4, (byte) 21, (byte) 2, (byte) 18, (byte) 7};
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, (byte) 116, (byte) 58,
(byte) 108, (byte) -1, (byte) 33, (byte) 9, (byte) 61, (byte) -61,
(byte) -37, (byte) 108, (byte) -123, (byte) 3, (byte) 35, (byte) 97,
(byte) -10, (byte) -15, (byte) 15, (byte) -85, (byte) -66, (byte) -31,
(byte) -65, (byte) 17, (byte) 79, (byte) 31, (byte) 25, (byte) -39,
(byte) 95, (byte) 93, (byte) 1, (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[]{(byte) 26, (byte) 27, (byte) 30, (byte) 4, (byte) 21, (byte) 2, (byte) 18, (byte) 7};
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}.