問題
Well, they look so similar... siblangs.apk
(SHA-1 hash: c08d002c5837ad39d509a1d09ed623003ae97229)
解法
Step 1. apktool
apk ファイルのリバースエンジニアリング問題
まずは apktool を使って中のファイルを取り出す
apktool d -s -o decompiled siblangs.apk
classes.dex が出てくるほか、rg ctf4b
を実行すると assets/index.android.bundle がヒットする
Step 2. assets/index.android.bundle の解析
ファイルを眺めていると React
の文字が出てくるので React Native
で作ったアプリだと思われる
ripgrep で ctf4b を検索すると出てきたところを抜粋
function v() {
var t;
(0, l.default)(this, v);
for (var o = arguments.length, n = new Array(o), c = 0; c < o; c++)
n[c] = arguments[c];
return (
((t = y.call.apply(y, [this].concat(n))).state = {
flagVal: "ctf4b{",
xored: [
34,
63,
3,
77,
36,
20,
24,
8,
25,
71,
110,
81,
64,
87,
30,
33,
81,
15,
39,
90,
17,
27,
],
}),
(t.handleFlagChange = function (o) {
t.setState({ flagVal: o });
}),
(t.onPressValidateFirstHalf = function () {
if ("ios" === h.Platform.OS) {
for (
var o = "AKeyFor" + h.Platform.OS + "10.3",
l = t.state.flagVal,
n = 0;
n < t.state.xored.length;
n++
)
if (
t.state.xored[n] !==
parseInt(l.charCodeAt(n) ^ o.charCodeAt(n % o.length), 10)
)
return void h.Alert.alert("Validation A Failed", "Try again...");
h.Alert.alert(
"Validation A Succeeded",
"Great! Have you checked the other one?"
);
} else
h.Alert.alert(
"Sorry!",
"Run this app on iOS to validate! Or you can try the other one :)"
);
}),
(t.onPressValidateLastHalf = function () {
"android" === h.Platform.OS
? p.default.validate(t.state.flagVal, function (t) {
t
? h.Alert.alert(
"Validation B Succeeded",
"Great! Have you checked the other one?"
)
: h.Alert.alert(
"Validation B Failed",
"Learn once, write anywhere ... anywhere?"
);
})
: h.Alert.alert(
"Sorry!",
"Run this app on Android to validate! Or you can try the other one :)"
);
}),
t
);
}
onPressValidateFirstHalf で入力を XOR でチェックしているのでもとに戻す
a = [34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27]
key = "AKeyForios10.3"
ans = ""
for i in range(len(a)):
ans += chr(a[i] ^ ord(key[i % len(key)]))
print(ans)
# ctf4b{jav4_and_j4va5cr
onPressValidateLastHalf で p.default.validate を呼んでいるがこのファイルの中にはないので java で処理していると見当をつけて先程出てきた classes.dex を解析する
Step 3. classes.dex の解析
dex2jar で dex ファイルを jar ファイルに戻す
https://sourceforge.net/projects/dex2jar/
chmod +x d2j-dex2jar.sh
./d2j-dex2jar.sh ../classes.dex
classes-dex2jar.jar が生成される
JD-GUI で javaのソースコードを調べる
http://java-decompiler.github.io/
JD-GUI でclasses-dex2jar.jarを開くとes.o0i.challengeapp/nativemodule/ValidateFlagModule
にてValidateするコードがある
package es.o0i.challengeapp.nativemodule;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ValidateFlagModule extends ReactContextBaseJavaModule {
private static final int GCM_IV_LENGTH = 12;
private final ReactApplicationContext reactContext;
private final SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
private final SecureRandom secureRandom = new SecureRandom();
public ValidateFlagModule(ReactApplicationContext paramReactApplicationContext) {
super(paramReactApplicationContext);
this.reactContext = paramReactApplicationContext;
}
public String getName() {
return "ValidateFlagModule";
}
@ReactMethod
public void validate(String paramString, Callback paramCallback) {
byte[] arrayOfByte = new byte[43];
arrayOfByte[0] = 95;
arrayOfByte[1] = -59;
arrayOfByte[2] = -20;
arrayOfByte[3] = -93;
arrayOfByte[4] = -70;
arrayOfByte[5] = 0;
arrayOfByte[6] = -32;
arrayOfByte[7] = -93;
arrayOfByte[8] = -23;
arrayOfByte[9] = 63;
arrayOfByte[10] = -9;
arrayOfByte[11] = 60;
arrayOfByte[12] = 86;
arrayOfByte[13] = 123;
arrayOfByte[14] = -61;
arrayOfByte[15] = -8;
arrayOfByte[16] = 17;
arrayOfByte[17] = -113;
arrayOfByte[18] = -106;
arrayOfByte[19] = 28;
arrayOfByte[20] = 99;
arrayOfByte[21] = -72;
arrayOfByte[22] = -3;
arrayOfByte[23] = 1;
arrayOfByte[24] = -41;
arrayOfByte[25] = -123;
arrayOfByte[26] = 17;
arrayOfByte[27] = 93;
arrayOfByte[28] = -36;
arrayOfByte[29] = 45;
arrayOfByte[30] = 18;
arrayOfByte[31] = 71;
arrayOfByte[32] = 61;
arrayOfByte[33] = 70;
arrayOfByte[34] = -117;
arrayOfByte[35] = -55;
arrayOfByte[36] = 107;
arrayOfByte[37] = -75;
arrayOfByte[38] = -89;
arrayOfByte[39] = 3;
arrayOfByte[40] = 94;
arrayOfByte[41] = -71;
arrayOfByte[42] = 30;
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, arrayOfByte, 0, 12);
cipher.init(2, this.secretKey, gCMParameterSpec);
arrayOfByte = cipher.doFinal(arrayOfByte, 12, arrayOfByte.length - 12);
byte[] arrayOfByte1 = paramString.getBytes();
for (int i = 0;; i++) {
if (i < arrayOfByte.length) {
if (arrayOfByte1[i + 22] != arrayOfByte[i]) {
paramCallback.invoke(new Object[] { Boolean.valueOf(false) });
return;
}
} else {
paramCallback.invoke(new Object[] { Boolean.valueOf(true) });
return;
}
}
} catch (Exception exception) {
paramCallback.invoke(new Object[] { Boolean.valueOf(false) });
return;
}
}
}
arrayOfByte を出力して正しいフラグを調べる
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Generate{
public static void main(String[] args){
SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
byte[] arrayOfByte = new byte[43];
arrayOfByte[0] = 95;
arrayOfByte[1] = -59;
arrayOfByte[2] = -20;
arrayOfByte[3] = -93;
arrayOfByte[4] = -70;
arrayOfByte[5] = 0;
arrayOfByte[6] = -32;
arrayOfByte[7] = -93;
arrayOfByte[8] = -23;
arrayOfByte[9] = 63;
arrayOfByte[10] = -9;
arrayOfByte[11] = 60;
arrayOfByte[12] = 86;
arrayOfByte[13] = 123;
arrayOfByte[14] = -61;
arrayOfByte[15] = -8;
arrayOfByte[16] = 17;
arrayOfByte[17] = -113;
arrayOfByte[18] = -106;
arrayOfByte[19] = 28;
arrayOfByte[20] = 99;
arrayOfByte[21] = -72;
arrayOfByte[22] = -3;
arrayOfByte[23] = 1;
arrayOfByte[24] = -41;
arrayOfByte[25] = -123;
arrayOfByte[26] = 17;
arrayOfByte[27] = 93;
arrayOfByte[28] = -36;
arrayOfByte[29] = 45;
arrayOfByte[30] = 18;
arrayOfByte[31] = 71;
arrayOfByte[32] = 61;
arrayOfByte[33] = 70;
arrayOfByte[34] = -117;
arrayOfByte[35] = -55;
arrayOfByte[36] = 107;
arrayOfByte[37] = -75;
arrayOfByte[38] = -89;
arrayOfByte[39] = 3;
arrayOfByte[40] = 94;
arrayOfByte[41] = -71;
arrayOfByte[42] = 30;
System.out.println(arrayOfByte);
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, arrayOfByte, 0, 12);
cipher.init(2, secretKey, gCMParameterSpec);
arrayOfByte = cipher.doFinal(arrayOfByte, 12, arrayOfByte.length - 12);
for (int i = 0;; i++) {
if (i < arrayOfByte.length) {
System.out.println(arrayOfByte[i]);
}
}
} catch (Exception exception) {
System.out.println("Fail");
return;
}
}
}
b = [49, 112, 116, 95, 51, 118, 101, 114, 121, 119, 104, 101, 114, 101, 125]
ans = ""
for i in range(len(b)):
ans += chr(b[i])
print(ans)
print(ans)
# 1pt_3verywhere}
よってフラグは
ctf4b{jav4_and_j4va5cr1pt_3verywhere}