genliese 发表于 2021-2-5 17:11

Google CTF 2020 资格赛 reverse_android

本帖最后由 genliese 于 2021-2-5 17:23 编辑

## 1. 背景

这是2020年Google CTF资格赛中的一道安卓reverse题,纯java,没有so。虽然很简单,但因为是我第一次做题,所以并不顺利。在此给大家分享一下我的思路,也算是一次总结吧。

## 2. 定位验证代码

![](https://genliesephotos.genliese.cn/Markdown/pixel3_install_xposed/check_pic.png)

**首先使用特征函数定位——OnClick**

使用jadx反编译apk(无壳无混淆),搜索`OnClick`,定位到了验证的位置

![](https://genliesephotos.genliese.cn/Markdown/pixel3_install_xposed/OnClick.png)

验证部分所在文件的完整代码如下:

```C
package com.google.ctf.sandbox;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

/* renamed from: com.google.ctf.sandbox.őreason: contains not printable characters */
public class ActivityC0000 extends Activity {

    /* renamed from: classreason: not valid java name */
    long[] f0class;

    /* renamed from: őreason: contains not printable characters */
    int f1;

    /* renamed from: őreason: contains not printable characters and collision with other field name */
    long[] f2;

    public ActivityC0000() {
      try {
            this.f0class = new long[]{40999019, 2789358025L, 656272715, 18374979, 3237618335L, 1762529471, 685548119, 382114257, 1436905469, 2126016673, 3318315423L, 797150821};
            this.f2 = new long;
            this.f1 = 0;
      } catch (I unused) {
      }
    }

    /* access modifiers changed from: protected */
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      final EditText editText = (EditText) findViewById(R.id.editText);
      final TextView textView = (TextView) findViewById(R.id.textView);
      ((Button) findViewById(R.id.button)).setOnClickListener(new View.OnClickListener() {
            /* class com.google.ctf.sandbox.ActivityC0000.AnonymousClass1 */

            public void onClick(View v) {
                ActivityC0000.this.f1 = 0;
                try {
                  StringBuilder keyString = new StringBuilder();
                  for (Object chr : new Object[]{65, 112, 112, 97, 114, 101, 110, 116, 108, 121, 32, 116, 104, 105, 115, 32, 105, 115, 32, 110, 111, 116, 32, 116, 104, 101, 32, 102, 108, 97, 103, 46, 32, 87, 104, 97, 116, 39, 115, 32, 103, 111, 105, 110, 103, 32, 111, 110, 63}) {
                        keyString.append(((Character) chr).charValue());
                  }
                  if (editText.getText().toString().equals(keyString.toString())) {
                        textView.setText("🚩");
                  } else {
                        textView.setText("❌");
                  }
                } catch (J | Error | Exception unused) {
                  String flagString = editText.getText().toString();
                  if (flagString.length() != 48) {
                        textView.setText("❌");
                        return;
                  }
                  for (int i = 0; i < flagString.length() / 4; i++) {
                        ActivityC0000.this.f2 = (long) (flagString.charAt((i * 4) + 3) << 24);
                        long[] jArr = ActivityC0000.this.f2;
                        jArr = jArr | ((long) (flagString.charAt((i * 4) + 2) << 16));
                        long[] jArr2 = ActivityC0000.this.f2;
                        jArr2 = jArr2 | ((long) (flagString.charAt((i * 4) + 1) << '\b'));
                        long[] jArr3 = ActivityC0000.this.f2;
                        jArr3 = jArr3 | ((long) flagString.charAt(i * 4));
                  }
                  ActivityC0000 r6 = ActivityC0000.this;
                  if (((R.m0(ActivityC0000.this.f2, 4294967296L) % 4294967296L) + 4294967296L) % 4294967296L != ActivityC0000.this.f0class) {
                        textView.setText("❌");
                        return;
                  }
                  ActivityC0000.this.f1++;
                  if (ActivityC0000.this.f1 >= ActivityC0000.this.f2.length) {
                        textView.setText("&#128681;");
                        return;
                  }
                  throw new RuntimeException();
                }
            }
      });
    }
}
```

## 3. 分析验证代码

"&#128681;"代表验证成功,两处地方有"&#128681;",我们先分析上面一个"&#128681;"附近的代码。

代码如下:

```C
try {
   StringBuilder keyString = new StringBuilder();
   for (Object chr : new Object[]{65, 112, 112, 97, 114, 101, 110, 116, 108, 121, 32, 116, 104, 105, 115, 32, 105, 115, 32, 110, 111, 116, 32, 116, 104, 101, 32, 102, 108, 97, 103, 46, 32, 87, 104, 97, 116, 39, 115, 32, 103, 111, 105, 110, 103, 32, 111, 110, 63}) {
         keyString.append(((Character) chr).charValue());
   }
   if (editText.getText().toString().equals(keyString.toString())) {
         textView.setText("&#128681;");
   } else {
         textView.setText("❌");
   }
} catch (J | Error | Exception unused) {
   ......
   }
```

代码解读:
该部分代码根据作用分为两部分,第一部分生成`Key`,第二部分验证`Key`。

**第一部分**
首先构造一个`StringBuilder`来存放`Key`,接着`new`了一个Object对象数组并初始化,循环取数组中的元素强转为`Character`类型,并调用`charValue()`方法得到字符添加到`Key`中。

第一部分会出现`java.lang.ClassCastException`异常,`java.lang.Integer`类不能转换为`java.lang.Character`类,因为他们并不存在子父类关系,所以此处是一个障眼法。

通过代码发现,出现的异常的代码被`try-catch`结构包裹了起来,所以出现的异常会被捕获,所以我们继续看`catch`部分的代码

代码如下:

```C
String flagString = editText.getText().toString();
if (flagString.length() != 48) {
      textView.setText("❌");
      return;
}
for (int i = 0; i < flagString.length() / 4; i++) {
      ActivityC0000.this.f2 = (long) (flagString.charAt((i * 4) + 3) << 24);
      long[] jArr = ActivityC0000.this.f2;
      jArr = jArr | ((long) (flagString.charAt((i * 4) + 2) << 16));
      long[] jArr2 = ActivityC0000.this.f2;
      jArr2 = jArr2 | ((long) (flagString.charAt((i * 4) + 1) << '\b'));
      long[] jArr3 = ActivityC0000.this.f2;
      jArr3 = jArr3 | ((long) flagString.charAt(i * 4));
}
ActivityC0000 r6 = ActivityC0000.this;
if (((R.m0(ActivityC0000.this.f2, 4294967296L) % 4294967296L) + 4294967296L) % 4294967296L != ActivityC0000.this.f0class) {
      textView.setText("❌");
      return;
}
ActivityC0000.this.f1++;
if (ActivityC0000.this.f1 >= ActivityC0000.this.f2.length) {
      textView.setText("&#128681;");
      return;
}
throw new RuntimeException();
```

代码解读:

**第一部分:flag初步处理**

```C
String flagString = editText.getText().toString();
if (flagString.length() != 48) {
      textView.setText("❌");
      return;
}
for (int i = 0; i < flagString.length() / 4; i++) {
      ActivityC0000.this.f2 = (long) (flagString.charAt((i * 4) + 3) << 24);
      long[] jArr = ActivityC0000.this.f2;
      jArr = jArr | ((long) (flagString.charAt((i * 4) + 2) << 16));
      long[] jArr2 = ActivityC0000.this.f2;
      jArr2 = jArr2 | ((long) (flagString.charAt((i * 4) + 1) << '\b'));
      long[] jArr3 = ActivityC0000.this.f2;
      jArr3 = jArr3 | ((long) flagString.charAt(i * 4));
}
```

首先得到的信息是flag的长度必须为**48**,紧接着是`for`循环,每次对flag中的4个字符进行如下处理后,放到`this.f2`中

```C
//等价的python实现
//'\b'<=>8
_value = (((ord(_index_3) << 24) | (ord(_index_2) << 16)) | (ord(_index_1) << 8)) | ord(_index_0)
```

补充一个信息:
`this.f2`在`ActivityC0000`的构造函数中被new了12个元素;`this.f0class`被初始化,一共12个元素;`this.f1`被初始化为0

```C
this.f0class = new long[]{40999019, 2789358025L, 656272715, 18374979, 3237618335L, 1762529471, 685548119, 382114257, 1436905469, 2126016673, 3318315423L, 797150821};
this.f2 = new long;
this.f1 = 0;
```

所以对`flag`初步处理后,我们得到了12个`long`型数据

**第二部分:flag进一步处理**

```C
if (((R.m0(ActivityC0000.this.f2, 4294967296L) % 4294967296L) + 4294967296L) % 4294967296L != ActivityC0000.this.f0class) {
      textView.setText("❌");
      return;
    }
```

代码中用到的递归函数`R.m0(a,b)`的代码如下:

```C
public static long[] m0(long a, long b) {
    if (a == 0) {
      return new long[]{0, 1};
    }
    long[] r = m0(b % a, a);
    return new long[]{r - ((b / a) * r), r};
}
```

首先是把**上面flag初步处理后得到的数据`this.f2`中的一个元素(`this.f1`作为索引)和4294967296L**一起放入递归函数`R.m0(a,b)`中进行处理,返回一个`long`型数组,并取其中的第一个元素,接着把这第一个元素做一系列的算术运算,将最终得到的运算结果和`this.f0class`中的元素(`this.f1`作为索引)进行对比,如果不相等则**验证失败**。如果**验证成功**,则继续**第三部分**

**第三部分:判断flag是否验证完毕**

**第二部分**中的验证,每次只验证1个元素(即验证flag的4个字符),所以需要循环12次才能验证完毕

```C
ActivityC0000.this.f1++;
if (ActivityC0000.this.f1 >= ActivityC0000.this.f2.length) {
    textView.setText("&#128681;");
    return;
}
throw new RuntimeException();
```

`this.f1`在**第二部分作为数组索引**,同时在这里也作为循环判断条件,`this.f2.length`是12,如果循环次数`<`12,则主动抛出异常,但没有捕获异常,到这里程序就崩了。站在程序设计者的角度来看,抛出异常之后,应该捕获异常,然后继续验证flag。所以我觉得异常应该是被捕获并处理的了,只是可能反编译出来的`Java`代码有问题,所以直接看`smali`代码

```C
......
new-instance v8, Ljava/lang/RuntimeException;

invoke-direct {v8}, Ljava/lang/RuntimeException;-><init>()V

throw v8

:goto_2c4
return-void

nop

.array-data 8
    0x1
.end array-data
:try_end_2ce
.catch Ljava/lang/Exception; {:try_start_205 .. :try_end_2ce} :catch_11
```

这是抛出异常`throw new RuntimeException();`附近的代码,我们发现,异常是被`catch_11`捕获了的

```C
:catch_11
const/16 v2, 0x31

const/4 v3, 0x0

const/4 v4, 0x3

const/4 v5, 0x2

const/4 v6, 0x1

const/4 v7, 0x4

goto/16 :goto_205
```

在`catch_11`处,我们发现又直接跳转到`goto_205`

```C
:goto_205
:try_start_205
iget-object v3, v1, Lcom/google/ctf/sandbox/ő$1;->val$editText:Landroid/widget/EditText;

invoke-virtual {v3}, Landroid/widget/EditText;->getText()Landroid/text/Editable;

move-result-object v3

invoke-virtual {v3}, Ljava/lang/Object;->toString()Ljava/lang/String;

move-result-object v3

.line 61
.local v3, "flagString":Ljava/lang/String;
invoke-virtual {v3}, Ljava/lang/String;->length()I

move-result v5
```

我们发现`goto_205`处的代码正是**第一部分:flag初步处理**处的代码,所以循环成立,**flag的循环验证通过异常来进行循环的**

通过上面的分析,可以得出flag的验证流程图如下:

![](https://genliesephotos.genliese.cn/Markdown/pixel3_install_xposed/validate_flag.png)

## 4. flag生成算法

通过分析可知,我们的flag是由12组,每组4个字符组成的。每次循环取其中的4个字符通过一系列的运算,最终得到一个结果并与数组`{40999019, 2789358025L, 656272715, 18374979, 3237618335L, 1762529471, 685548119, 382114257, 1436905469, 2126016673, 3318315423L, 797150821}`中的元素做比较,一共12次循环。

我采用暴力枚举的方式算出flag,算法如下:

```Python
from itertools import permutations
import sys
import time


def m0(a, b):
    if a == 0:
      return

    r = m0(b % a, a)
    return - ((b // a) * r), r]


def calculate(_index_3, _index_2, _index_1, _index_0):
    _value = (((ord(_index_3) << 24) | (ord(_index_2) << 16)) | (ord(_index_1) << 8)) | ord(_index_0)
    _value = (m0(_value, 4294967296) % 4294967296 + 4294967296) % 4294967296
    sys.stdout.write(' Trying key: {}{}{}{}\r'.format(index_0, index_1, index_2, index_3))
    sys.stdout.flush()
    return _value


magic = [40999019, 2789358025, 656272715, 18374979, 3237618335, 1762529471, 685548119, 382114257, 1436905469,
         2126016673, 3318315423, 797150821]
solved = []
flag = ['*'] * 48
possibilities = permutations('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?_{}', 4)
time_begin = time.time()
for p in possibilities:
    index_0, index_1, index_2, index_3 = p
    value = calculate(index_3, index_2, index_1, index_0)

    for m in magic:
      if value == m:
            flag = index_0, index_1, index_2, index_3
            break
    sys.stdout.write(' Flag: {} '.format(''.join(flag)))
    sys.stdout.flush()
time_end = time.time()
cost_time = time_end - time_begin
print('cost time: ' + str(cost_time // 60) + 'min')
```

之所以采用`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?\_{}`作为迭代数,是因为这些是flag中常见的字符

最终得到的flag为`CTF{y0u_c4n_k3ep_y0u?_m4gic_1_h4Ue_laser_b3ams!}`,说一个小技巧,`adb shell input text "CTF{y0u_c4n_k3ep_y0u?_m4gic_1_h4Ue_laser_b3ams!}"`可以快速输入字符到编辑框

![](https://genliesephotos.genliese.cn/Markdown/pixel3_install_xposed/get_flag.png)

## 5. 参考

(https://blackbeard666.github.io/pwn_exhibit/content/2020_CTF/GoogleCTF/re_android/android_writeup.html)

## 6. 附件

apk见附件


genliese 发表于 2021-2-7 14:14

5ud0 发表于 2021-2-7 12:43
楼主你好,为啥我的jadx分析报错,Activity反编译不完整,JADX ERROR: Method load error

我用的是jadx 1.2版本(最新版本),除了处理Activity末尾的`throw new RuntimeException();`的代码没有被反编译外,其余部分都被反编译了。倒是用jeb和d2j-dex2jar反编译会出现大问题,如图



5ud0 发表于 2021-2-8 10:01

genliese 发表于 2021-2-7 14:14
我用的是jadx 1.2版本(最新版本),除了处理Activity末尾的`throw new RuntimeException();`的代码没有 ...

嗷嗷,谢谢楼主,可能是版本低的原因我的jadx还是1.1

十一七 发表于 2021-2-5 18:08

顶一个{:301_1009:}{:301_996:}
学习ing...

chaselove 发表于 2021-2-5 18:19

牛逼plus

绝地飞鸿 发表于 2021-2-5 18:27

xiaolinzzz 发表于 2021-2-6 10:52

感谢分享,学习一下

stczb 发表于 2021-2-6 11:53


感谢分享.支持下

ALBERTLEE 发表于 2021-2-6 14:39

感谢分享,吾爱破解

a2392692065 发表于 2021-2-6 18:06

羡慕大佬的技术

马猴煮酒 发表于 2021-2-6 22:10

学习中,感谢分享

柠檬Cat 发表于 2021-2-7 11:45

同样是学java为何你这么厉害
页: [1] 2 3
查看完整版本: Google CTF 2020 资格赛 reverse_android