Google CTF 2020 资格赛 reverse_android
2021-03-04 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:171 收藏

本文为看雪论坛优秀文章

看雪论坛作者ID:genliese

一、背景

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

二、定位验证代码

 
首先使用特征函数定位——OnClick。
 
使用jadx反编译apk(无壳无混淆),搜索OnClick,定位到了验证的位置。
 
 
验证部分所在文件的完整代码如下:
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: class  reason: 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[12];            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[i] = (long) (flagString.charAt((i * 4) + 3) << 24);                        long[] jArr = ActivityC0000.this.f2;                        jArr[i] = jArr[i] | ((long) (flagString.charAt((i * 4) + 2) << 16));                        long[] jArr2 = ActivityC0000.this.f2;                        jArr2[i] = jArr2[i] | ((long) (flagString.charAt((i * 4) + 1) << '\b'));                        long[] jArr3 = ActivityC0000.this.f2;                        jArr3[i] = jArr3[i] | ((long) flagString.charAt(i * 4));                    }                    ActivityC0000 r6 = ActivityC0000.this;                    if (((R.m0(ActivityC0000.this.f2[ActivityC0000.this.f1], 4294967296L)[0] % 4294967296L) + 4294967296L) % 4294967296L != ActivityC0000.this.f0class[ActivityC0000.this.f1]) {                        textView.setText("验证失败");                        return;                    }                    ActivityC0000.this.f1++;                    if (ActivityC0000.this.f1 >= ActivityC0000.this.f2.length) {                        textView.setText("验证成功");                        return;                    }                    throw new RuntimeException();                }            }        });    }}

三、分析验证代码

“验证成功”代表验证成功,两处地方有“验证成功”,我们先分析上面一个“验证成功”附近的代码。
 
代码如下:
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) {     ......     }
代码解读:

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

首先构造一个StringBuilder来存放Key,接着new了一个Object对象数组并初始化,循环取数组中的元素强转为Character类型,并调用charValue()方法得到字符添加到Key中。
 
第一部分会出现java.lang.ClassCastException异常,java.lang.Integer类不能转换为java.lang.Character类,因为他们并不存在子父类关系,所以此处是一个障眼法。
 
通过代码发现,出现的异常的代码被try-catch结构包裹了起来,所以出现的异常会被捕获,所以我们继续看catch部分的代码。
 
代码如下:
String flagString = editText.getText().toString();  if (flagString.length() != 48) {      textView.setText("验证失败");      return;  }  for (int i = 0; i < flagString.length() / 4; i++) {      ActivityC0000.this.f2[i] = (long) (flagString.charAt((i * 4) + 3) << 24);      long[] jArr = ActivityC0000.this.f2;      jArr[i] = jArr[i] | ((long) (flagString.charAt((i * 4) + 2) << 16));      long[] jArr2 = ActivityC0000.this.f2;      jArr2[i] = jArr2[i] | ((long) (flagString.charAt((i * 4) + 1) << '\b'));      long[] jArr3 = ActivityC0000.this.f2;      jArr3[i] = jArr3[i] | ((long) flagString.charAt(i * 4));  }  ActivityC0000 r6 = ActivityC0000.this;  if (((R.m0(ActivityC0000.this.f2[ActivityC0000.this.f1], 4294967296L)[0] % 4294967296L) + 4294967296L) % 4294967296L != ActivityC0000.this.f0class[ActivityC0000.this.f1]) {      textView.setText("验证失败");      return;  }  ActivityC0000.this.f1++;  if (ActivityC0000.this.f1 >= ActivityC0000.this.f2.length) {      textView.setText("验证成功");      return;  }  throw new RuntimeException();
代码解读:
 
第一部分:flag初步处理
String flagString = editText.getText().toString();  if (flagString.length() != 48) {      textView.setText("验证失败");      return;  }  for (int i = 0; i < flagString.length() / 4; i++) {      ActivityC0000.this.f2[i] = (long) (flagString.charAt((i * 4) + 3) << 24);      long[] jArr = ActivityC0000.this.f2;      jArr[i] = jArr[i] | ((long) (flagString.charAt((i * 4) + 2) << 16));      long[] jArr2 = ActivityC0000.this.f2;      jArr2[i] = jArr2[i] | ((long) (flagString.charAt((i * 4) + 1) << '\b'));      long[] jArr3 = ActivityC0000.this.f2;      jArr3[i] = jArr3[i] | ((long) flagString.charAt(i * 4));  }
首先得到的信息是flag的长度必须为48,紧接着是for循环,每次对flag中的4个字符进行如下处理后,放到this.f2[i]中。
//等价的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。
this.f0class = new long[]{40999019, 2789358025L, 656272715, 18374979, 3237618335L, 1762529471, 685548119, 382114257, 1436905469, 2126016673, 3318315423L, 797150821};this.f2 = new long[12];this.f1 = 0;

所以对flag初步处理后,我们得到了12个long型数据。
 
第二部分:flag进一步处理
if (((R.m0(ActivityC0000.this.f2[ActivityC0000.this.f1], 4294967296L)[0] % 4294967296L) + 4294967296L) % 4294967296L != ActivityC0000.this.f0class[ActivityC0000.this.f1]) {        textView.setText("验证失败");        return;    }

代码中用到的递归函数R.m0(a,b)的代码如下:
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[1] - ((b / a) * r[0]), r[0]};}
首先是把上面flag初步处理后得到的数据this.f2中的一个元素(this.f1作为索引)和4294967296L一起放入递归函数R.m0(a,b)中进行处理,返回一个long型数组,并取其中的第一个元素,接着把这第一个元素做一系列的算术运算,将最终得到的运算结果和this.f0class中的元素(this.f1作为索引)进行对比,如果不相等则验证失败。如果验证成功,则继续第三部分。
 
第三部分:判断flag是否验证完毕
 
第二部分中的验证,每次只验证1个元素(即验证flag的4个字符),所以需要循环12次才能验证完毕。
ActivityC0000.this.f1++;if (ActivityC0000.this.f1 >= ActivityC0000.this.f2.length) {    textView.setText("验证成功");    return;}throw new RuntimeException();
this.f1在第二部分作为数组索引,同时在这里也作为循环判断条件,this.f2.length是12,如果循环次数<12,则主动抛出异常,但没有捕获异常,到这里程序就崩了。站在程序设计者的角度来看,抛出异常之后,应该捕获异常,然后继续验证flag。所以我觉得异常应该是被捕获并处理的了,只是可能反编译出来的Java代码有问题,所以直接看smali代码。
......new-instance v8, Ljava/lang/RuntimeException; invoke-direct {v8}, Ljava/lang/RuntimeException;-><init>()V throw v8 :goto_2c4return-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捕获了的。
:catch_11const/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。
:goto_205:try_start_205iget-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的验证流程图如下:

四、flag生成算法

通过分析可知,我们的flag是由12组,每组4个字符组成的。每次循环取其中的4个字符通过一系列的运算,最终得到一个结果并与数组{40999019, 2789358025L, 656272715, 18374979, 3237618335L, 1762529471, 685548119, 382114257, 1436905469, 2126016673, 3318315423L, 797150821}中的元素做比较,一共12次循环。
 
我采用暴力枚举的方式算出flag,算法如下:
from itertools import permutationsimport sysimport time  def m0(a, b):    if a == 0:        return [0, 1]     r = m0(b % a, a)    return [r[1] - ((b // a) * r[0]), r[0]]  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)[0] % 4294967296 + 4294967296) % 4294967296    sys.stdout.write('[i] 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 = ['*'] * 48possibilities = 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[magic.index(m) * 4: magic.index(m) * 4 + 4] = 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_beginprint('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!}"可以快速输入字符到编辑框。
 

参考

Google CTF 2020: Android

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

apk见附件(点击左下角阅读原文获取)

- End -

看雪ID:genliese

https://bbs.pediy.com/user-home-825187.htm

  *本文由看雪论坛 genliese 原创,转载请注明来自看雪社区。

# 往期推荐

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458382013&idx=1&sn=e03284feb6a40b09c6f2526b2a367161&chksm=b180de3786f75721d8dcb368b0f6323f8da1450df95c50d4401637e2358e921c84d45789a216#rd
如有侵权请联系:admin#unsafe.sh