常规手段还原出 smali 文件(这里偷了个懒,直接用的 apktool),没有混淆也没有 so 加固,只需要理出 smali 处理流程即可。根据 AndroidManifest.xml 找到 com.example.ring.myapplication.MainActivity, 故从对应的 MainActivity.smali 作为起点分析。
进行了一系列的初始化操作,然后理出比较重要的代码:进入 p 函数、绑定类 d 为点击事件。
.method protected onCreate(Landroid/os/Bundle;)V ... invoke-direct {p0}, Lcom/example/ring/myapplication/MainActivity;->p()V ... new-instance v1, Lcom/example/ring/myapplication/d; invoke-direct {v1, p0}, Lcom/example/ring/myapplication/d;->(Lcom/example/ring/myapplication/MainActivity;)V invoke-virtual {v0, v1}, Landroid/widget/Button;->setOnClickListener(Landroid/view/View$OnClickListener;)V .end method
跟进一下 p() 函数:
.method private p()V ... const-string v1, "url.png" invoke-virtual {v0, v1}, Landroid/content/res/AssetManager;->open(Ljava/lang/String;)Ljava/io/InputStream; ... invoke-virtual {v0, v2, v3, v1}, Ljava/io/InputStream;->read([BII)I ... const/16 v1, 0x90 const/4 v3, 0x0 const/16 v4, 0x10 invoke-static {v2, v1, v0, v3, v4}, Ljava/lang/System;->arraycopy(Ljava/lang/Object;ILjava/lang/Object;II)V ... iput-object v1, p0, Lcom/example/ring/myapplication/MainActivity;->v:Ljava/lang/String; ... .end method
可以看到这里打开了 asset 目录下的 “url.png”,然后读取了 0x90 ~ 0xa0 位置的内容保存到 MainActivity->v 里,下面看下 d 类:
.method public onClick(Landroid/view/View;)V ... invoke-virtual {v0}, Landroid/widget/EditText;->getText()Landroid/text/Editable; move-result-object v0 invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String; move-result-object v0 ... # 下行是用来获取 MainActivity->v 的 invoke-static {v2}, Lcom/example/ring/myapplication/MainActivity;->a(Lcom/example/ring/myapplication/MainActivity;)Ljava/lang/String; move-result-object v2 invoke-static {v1, v2, v0}, Lcom/example/ring/myapplication/MainActivity;->a(Lcom/example/ring/myapplication/MainActivity;Ljava/lang/String;Ljava/lang/String;)Z move-result v0 ... if-eqz v0, :cond_0 ... const-string v2, "Congratulations!"
好的,发现成功条件了,当 MainActivity->a(String,String) 返回结果为真时程序成功,看下这里的两个 String,一个是 MainActivity->v(上面说过,其实是 url.png 的 0x90 ~ 0xa0 位置), 一个是用户输入的字符串(即为 Flag),下面先看一下 MainActivity->v :
xxd 可以看到 MainActivity->v 即是 "this_is_the_key.",然后跟进 MainActivity->a(String,String) 函数,看后续流程,由下面的代码可以看出,当用户输入的内容经过转换后和 array_0 位置的内容相同为成功条件。
.method private a(Ljava/lang/String;Ljava/lang/String;)Z ... new-instance v0, Lcom/example/ring/myapplication/c; invoke-direct {v0}, Lcom/example/ring/myapplication/c;->()V invoke-virtual {v0, p1, p2}, Lcom/example/ring/myapplication/c;->a(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; ... const/16 v1, 0x20 new-array v1, v1, [B fill-array-data v1, :array_0 new-instance v2, Ljava/lang/String; invoke-direct {v2, v1}, Ljava/lang/String;-> ([B)V # 将 array_0 位置的内容转化成字符串 invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;) move-result v0 return v0 .end method
在比较之前,函数初始化了类 c,然后执行 c->a(String,String) ,这里有点复杂,偷个懒使用 jeb 还原 java 代码看,这里。
可以看到做的主要的事情有:进入 c->a(String) 函数将 bytes 分为两个一组,然后组内交换顺序,那么现在的 MainActivity->v 就变成了 "htsii__sht_eek.y";另外初始化了类 a,并执行了 a->a(byte[]) 和 a->b(byte[]) 操作,看下类 a 的代码。
可以看出这里其实是以 MainActivity->v 作为了 AES 加密 Key,加密了用户输入的字符串这个操作。所以最后的比较即是:AES 密文和 array_0 位置数据的比较,写了个解密代码,运行出来了 Flag。
import java.io.*; import javax.crypto.spec.SecretKeySpec; import javax.crypto.Cipher; class test { public static void main (String[] args) throws java.lang.Exception { String key = "htsii__sht_eek.y"; SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); Cipher cipher = Cipher.getInstance("AES");// 创建密码器 byte[] bye = {21,-93,-68,-94,86,117,-19,-68,-92,33,50,118,16,13,1,-15,-13,3,4,103,-18,81,30,68,54,-93,44,-23,93,98,5,59}; /* byte[] byteContent = content.getBytes("utf-8"); cipher.init(Cipher.ENCRYPT_MODE, keySpec);// 初始化 byte[] result = cipher.doFinal(byteContent); String str2 = new String(result,"UTF-8"); System.out.println(str2); // 加密 */ cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] result2 = cipher.doFinal(bye); String str3 = new String(result2, "utf-8"); System.out.println(str3); } } # LCTF{1t's_rea1ly_an_ea3y_ap4}
* 逆向的 apk 上传到了 Github 。