I have done this challenge with briskness-alive-tinderbox
.
Can you find what lays behind this simple game ?
We also have a file named app-release.apk
.
Let’s first decompile the APK file using jadx
to get the Java source code with jadx app-release.apk
. The source code we are interested in is in the sources/com/example/oxidized_intentions
directory.
$ tree .
.
├── ComposableSingletons$MainActivityKt.java
├── MainActivity.java
├── MainActivityKt$GameCanvas$1$1.java
├── MainActivityKt.java
├── Native.java
├── Particle.java
├── Poof.java
├── R.java
├── TicketReceiver.java
└── ui
└── theme
├── ColorKt.java
├── ThemeKt.java
└── TypeKt.java
3 directories, 12 files
By looking at the files, we find two of them that seem interesting:
Native.java
package com.example.oxidized_intentions;
import android.content.Context;
import kotlin.Metadata;
import kotlin.jvm.JvmStatic;
/* compiled from: Native.kt */
@Metadata(d1 = {"\u0000&\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\b\n\u0000\bÇ\u0002\u0018\u00002\u00020\u0001B\t\b\u0002¢\u0006\u0004\b\u0002\u0010\u0003J\b\u0010\u0004\u001a\u00020\u0005H\u0007J)\u0010\u0006\u001a\u00020\u00072\u0006\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u00072\u0006\u0010\u000b\u001a\u00020\u00072\u0006\u0010\f\u001a\u00020\rH\u0087 ¨\u0006\u000e"}, d2 = {"Lcom/example/oxidized_intentions/Native;", "", "<init>", "()V", "initAtLaunch", "", "getFlag", "", "ctx", "Landroid/content/Context;", "seed", "part", "check", "", "app_release"}, k = 1, mv = {2, 0, 0}, xi = 48)
/* loaded from: classes2.dex */
public final class Native {
public static final int $stable = 0;
public static final Native INSTANCE = new Native();
@JvmStatic
public static final native String getFlag(Context ctx, String seed, String part, int check);
@JvmStatic
public static final void initAtLaunch() {
}
private Native() {
}
static {
System.loadLibrary("oxi");
}
}
TicketReceiver.java
package com.example.oxidized_intentions;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
/* compiled from: TicketReceiver.kt */
@Metadata(d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\b\u0007\u0018\u0000 \n2\u00020\u0001:\u0001\nB\u0007¢\u0006\u0004\b\u0002\u0010\u0003J\u0018\u0010\u0004\u001a\u00020\u00052\u0006\u0010\u0006\u001a\u00020\u00072\u0006\u0010\b\u001a\u00020\tH\u0016¨\u0006\u000b"}, d2 = {"Lcom/example/oxidized_intentions/TicketReceiver;", "Landroid/content/BroadcastReceiver;", "<init>", "()V", "onReceive", "", "context", "Landroid/content/Context;", "intent", "Landroid/content/Intent;", "Companion", "app_release"}, k = 1, mv = {2, 0, 0}, xi = 48)
/* loaded from: classes2.dex */
public final class TicketReceiver extends BroadcastReceiver {
public static final int $stable = 0;
public static final String ACTION_FLAGGED = "com.example.oxidized_intentions.FLAGGED";
public static final String EXTRA_FLAG = "flag";
private static final String PART_J = "oxidized-";
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
Intrinsics.checkNotNullParameter(context, "context");
Intrinsics.checkNotNullParameter(intent, "intent");
String stringExtra = intent.getStringExtra("seed");
if (stringExtra == null) {
return;
}
Log.d("OXI", "Got broadcast, seed=" + stringExtra);
String str = stringExtra;
int iCharAt = 0;
for (int i = 0; i < str.length(); i++) {
iCharAt ^= str.charAt(i);
}
String flag = Native.getFlag(context, stringExtra, PART_J, iCharAt & 255);
Toast.makeText(context, flag, 1).show();
Log.d("OXI", "FLAG=" + flag);
Intent intent2 = new Intent(ACTION_FLAGGED);
intent2.setPackage(context.getPackageName());
intent2.putExtra(EXTRA_FLAG, flag);
context.sendBroadcast(intent2);
}
}
Native.java
seems to import a library named oxi
that contains a method named getFlag
. By looking at the files in resources/lib/arm64-v8a
, we can see that there is a file named liboxi.so
. This is for sure a library that we will need to analyse.
TicketReceiver.java
is a BroadcastReceiver that listens for an intent with a seed, computes a checksum from this seed, and calls the getFlag
method from the Native
class with the seed, a constant string oxidized-
, and the computed checksum. It then displays the returned flag in the logs.
After this first static analysis, it was time to run the application to see how it works. For that, I will be using an Android emulator on my computer with a rooted Android, just in case we need to do some advanced stuff.
To install the application, just run adb install app-release.apk
. You can now launch it on your emulator.
This app is a simple game where you have to click on the dots coming to the big circle in the middle before they touch it. You have 3 lives, and each time you click on a dot you earn one point. When you lose all your lives, the game is over and you can start a new game.
It is clearly written in the application that we don’t have to win the game in order to get the flag.
Okay so the idea is to find a way to call the getFlag
method, but it is not being called anywhere in the code. We will now use Frida
to tricker the onReceive
method of the TicketReceiver
class to call the getFlag
method with our own seed.
This was my first time using Frida
, so I had to learn how to use it. The first thing to know about Frida
is that when you want to, for example, hook or call a method, you need to run a server on the Android device. Therefore, after downloading the right version of the Frida
server on my computer, I pushed it to the Android device with adb push frida-server-17.2.17-android-x86_64 /tmp
and then I ran it with adb shell
.
We are now set up and we can start writing our Frida
script. To run it, we do frida -U -f com.example.oxidized_intentions -l call_function.js
and to check if it works, we can use adb logcat | grep OXI
to see the logs of the application, as we have seen in the Java code that there are some logs.
After looking at the documentation, at some code on https://codeshare.frida.re/, and some debugging, I came up with this script that works:
Java.perform(function () {
var it = setInterval(function () {
try {
var Intent = Java.use('android.content.Intent');
var ActivityThread = Java.use('android.app.ActivityThread');
var StringCls = Java.use('java.lang.String');
var context = ActivityThread.currentApplication().getApplicationContext();
var intent = Intent.$new();
intent.setClassName(context.getPackageName(), 'com.example.oxidized_intentions.TicketReceiver');
intent.putExtra.overload('java.lang.String', 'java.lang.String')
.call(intent, 'seed', StringCls.$new('fe2o3rust'));
context.sendBroadcast(intent);
console.log('Broadcast sent');
} catch (error) {
console.error('Error in Frida script:', error);
}}, 1000);
});
The code seems quite simple, but it took me a bit of time to write it. The seed here is fe2o3rust
, as we can see on the logs that this is the seed required:
09-01 14:59:39.902 9489 9489 D OXI : Seed 'hello' is not expected; required seed is 'fe2o3rust'
With the right seed set, we get this in the logs:
09-01 15:01:10.977 9557 9557 D OXI : Got broadcast, seed=fe2o3rust
09-01 15:01:10.977 9557 9557 D OXI : Computing flag for seed='fe2o3rust' ...
09-01 15:01:11.978 9557 9557 D OXI : anti_hook_check elapsed=1000ms
09-01 15:01:21.675 9557 9557 D OXI : HACKER bit not set -> returning FAKE
09-01 15:01:21.679 9557 9557 D OXI : FLAG=FAKE{2152411021524119}
So now we need to understand how the liboxi.so
library works to get the real flag.
After opening the library in Ghidra
, let’s have a look at the exported functions:
We do not see here the getFlag
function, but we can see the JNI_OnLoad
function that is called when the library is loaded. After looking a bit at it, we find the function getFlag
. By just defining the strings (as it is some Rust code, so the strings are not null-terminated), we can have an idea of the workflow of the function:
code * FUN_00111a4c(long *param_1,undefined8 param_2,undefined8 param_3,undefined8 param_4,
undefined8 param_5,uint param_6)
{
uint uVar1;
uint uVar2;
void *pvVar3;
long lVar4;
undefined4 uVar5;
undefined1 uVar6;
int iVar7;
undefined8 uVar8;
code *pcVar9;
undefined8 extraout_x1;
undefined8 extraout_x1_00;
undefined8 extraout_x1_01;
uint uVar10;
undefined8 *extraout_x8;
code *extraout_x8_00;
code *pcVar11;
ulong uVar12;
undefined8 extraout_x9;
long lVar13;
ulong uVar14;
bool bVar15;
ulong **unaff_x25;
undefined1 auVar16 [16];
undefined1 auVar17 [12];
undefined8 local_218;
undefined8 uStack_210;
ulong local_208;
void *local_200;
ulong local_1f8;
ulong uStack_1f0;
byte *local_1e8;
long lStack_1e0;
undefined8 local_1d8 [2];
undefined8 local_1c8;
undefined8 local_1c0 [2];
undefined8 local_1b0;
undefined1 auStack_1a8 [24];
undefined1 auStack_190 [24];
long local_178;
long local_170;
long local_168;
ulong local_160;
long lStack_158;
long local_150;
undefined1 auStack_148 [24];
ulong *local_130;
code *pcStack_128;
undefined8 *local_120;
code *pcStack_118;
undefined8 *local_110;
undefined8 uStack_108;
ulong local_f0;
undefined8 local_e8;
code *local_e0;
ulong *puStack_d8;
code *local_d0;
ulong local_c8;
undefined8 local_c0;
code *local_b8;
ulong **local_b0;
ulong *local_a8;
code *pcStack_a0;
undefined8 *local_98;
undefined8 uStack_90;
undefined8 local_88;
undefined8 local_78;
undefined8 uStack_70;
undefined8 local_68;
undefined8 uStack_60;
undefined1 local_58;
/* try { // try from 00111a70 to 00111a7f has its CatchHandler @ 001122ac */
local_218 = param_4;
uStack_210 = param_5;
FUN_00119610(&local_c0,param_1,&local_218);
if ((char)local_c0 == '\x0f') {
/* try { // try from 00111a98 to 00111ab7 has its CatchHandler @ 001122b0 */
FUN_0014752c(&local_c0);
local_130 = (ulong *)CONCAT71(local_130._1_7_,0xf);
}
else {
FUN_00147448();
}
FUN_001113a4(&local_208,&local_130);
/* try { // try from 00111ab8 to 00111aff has its CatchHandler @ 001122b4 */
FUN_00119610(&local_c0,param_1,&uStack_210);
if ((char)local_c0 == '\x0f') {
FUN_0014752c(&local_c0);
local_130 = (ulong *)CONCAT71(local_130._1_7_,0xf);
}
else {
FUN_00147448();
}
FUN_001113a4(&uStack_1f0,&local_130);
local_e8 = &local_208;
local_130 = &local_e8;
pcStack_128 = FUN_00118f48;
local_c0 = &PTR_s_Computing_flag_for_seed=''_..._0014fb10;
local_b8 = (code *)0x2;
local_a8 = (ulong *)0x1;
pcStack_a0 = (code *)0x0;
/* try { // try from 00111b34 to 00111b3b has its CatchHandler @ 001122b8 */
local_b0 = &local_130;
uVar8 = FUN_001473f8(local_1d8);
/* try { // try from 00111b44 to 00111b47 has its CatchHandler @ 001122a0 */
FUN_0014746c(uVar8,local_1c8);
FUN_00147394(local_1d8[0]);
pvVar3 = local_200;
uVar6 = local_1f8 == 9;
if (((bool)uVar6) && (iVar7 = memcmp(local_200,"fe2o3rust",9), iVar7 == 0)) {
uVar10 = 0;
for (lVar13 = 0; lVar13 != 9; lVar13 = lVar13 + 1) {
uVar10 = *(byte *)((long)pvVar3 + lVar13) ^ uVar10;
}
if (param_6 != uVar10) {
FUN_0011187c("Bad checksum; taking slow path.",0x1f);
FUN_001119f0();
}
auVar16 = FUN_001468ac();
FUN_001119cc(1);
auVar17 = FUN_00146a78(auVar16._0_8_,auVar16._8_8_ & 0xffffffff);
local_e8 = &local_160;
uVar14 = auVar17._0_8_ * 1000;
uVar12 = (ulong)(auVar17._8_4_ / 1000000);
auVar16._8_8_ = 0;
auVar16._0_8_ = auVar17._0_8_;
lStack_158 = SUB168(auVar16 * ZEXT816(1000),8);
local_e0 = FUN_00115d8c;
local_c0 = &PTR_s_anti_hook_check_elapsed=ms_0014faf0;
local_b8 = (code *)0x2;
local_160 = uVar14 + uVar12;
if (CARRY8(uVar14,uVar12)) {
lStack_158 = lStack_158 + 1;
}
local_b0 = (ulong **)&local_e8;
local_a8 = (ulong *)0x1;
pcStack_a0 = (code *)0x0;
uVar8 = FUN_001473f8(&local_130);
/* try { // try from 00111d18 to 00111d1b has its CatchHandler @ 00112288 */
FUN_0014746c(uVar8,local_120);
FUN_00147394(local_130);
if (lStack_158 != 0 || CARRY8(lStack_158 - 1,(ulong)(499 < local_160))) {
/* try { // try from 00111dcc to 00111f7f has its CatchHandler @ 001122b8 */
FUN_001119f0();
if (HACKER == 1) {
uVar12 = 0xcbf29ce484222325;
for (; lStack_1e0 != 0; lStack_1e0 = lStack_1e0 + -1) {
uVar12 = (uVar12 ^ *local_1e8) * 0x1000001b3;
local_1e8 = local_1e8 + 1;
}
local_130 = (ulong *)0x0;
pcStack_128 = (code *)0x0;
local_120 = (undefined8 *)((ulong)local_120 & 0xffffffffffff0000);
uVar12 = uVar12 ^ 0xc0ffee123456789b;
for (lVar13 = 0; lVar13 != 0x12; lVar13 = lVar13 + 1) {
uVar14 = (&DAT_001046a0)[lVar13];
if (0x11 < uVar14) {
/* WARNING: Subroutine does not return */
FUN_00113690(uVar14,0x12,&PTR_s_src/lib.rs_0014fad8);
}
uVar12 = uVar12 ^ uVar12 >> 0xc;
uVar12 = uVar12 ^ uVar12 << 0x19;
uVar12 = (uVar12 ^ uVar12 >> 0x1b) * 0x2545f4914f6cdd1d;
uVar10 = (uint)(byte)(&DAT_00104730)[uVar14] ^ (uint)uVar12;
uVar2 = (uint)uVar12 >> 8 & 0xf;
uVar1 = (uint)(uVar12 >> 0x3d) | 1;
uVar10 = (uVar10 << (ulong)uVar1 | (uVar10 & 0xff) >> (ulong)(-uVar1 & 7)) + uVar2;
*(byte *)((long)&local_130 + lVar13) =
((byte)((uVar10 & 0xff) >> (ulong)uVar1) | (byte)(uVar10 << (ulong)(-uVar1 & 7))) -
(char)uVar2 ^ (byte)uVar12 ^ 0xaa;
}
FUN_00112e30(&local_c0,&local_130,0x12);
FUN_00119040(&local_e8,&local_c0);
local_130 = &uStack_1f0;
pcStack_128 = FUN_001115dc;
local_b0 = &local_130;
pcStack_118 = FUN_001115dc;
local_c0 = (undefined **)&DAT_00104748;
local_b8 = (code *)0x2;
local_a8 = (ulong *)0x2;
pcStack_a0 = (code *)0x0;
/* try { // try from 00111fb0 to 00111fb7 has its CatchHandler @ 00112250 */
local_120 = &local_e8;
FUN_001473f8(&local_160);
FUN_001184e4(local_e8,local_e0);
lVar4 = lStack_158;
uVar12 = local_160;
local_178 = 0;
local_170 = 1;
local_168 = 0;
/* try { // try from 00111fd8 to 001120f3 has its CatchHandler @ 0011227c */
FUN_0011267c(&local_178,local_200,(long)local_200 + local_1f8);
lVar13 = local_168;
if (local_168 == local_178) {
FUN_001129b8(&local_178,&PTR_s_src/lib.rs_0014fb40);
}
local_168 = lVar13 + 1;
*(undefined1 *)(local_170 + lVar13) = 0x3a;
FUN_0011267c(&local_178,lVar4,lVar4 + local_150);
local_f0 = 0x1234567890abcdef;
for (lVar13 = 0; local_168 != lVar13; lVar13 = lVar13 + 1) {
local_f0 = (local_f0 >> 0x39 | (local_f0 ^ *(byte *)(local_170 + lVar13)) << 7) +
0x9e3779b97f4a7c15;
}
local_c8 = ~local_f0;
local_e8 = &local_f0;
puStack_d8 = &local_c8;
local_e0 = FUN_00115b00;
local_d0 = FUN_00115b00;
local_b0 = (ulong **)0x0;
local_a8 = (ulong *)0x10;
local_78 = 0;
uStack_70 = 0x10;
local_110 = &local_c0;
uStack_108 = 2;
local_120 = &local_e8;
local_c0 = (undefined **)0x2;
pcStack_a0 = (code *)0x0;
local_98 = (undefined8 *)0x800000020;
uStack_90 = CONCAT71(uStack_90._1_7_,3);
local_88 = 2;
local_68 = 1;
uStack_60 = 0x800000020;
local_58 = 3;
local_130 = (ulong *)&DAT_00104748;
pcStack_128 = (code *)0x2;
pcStack_118 = (code *)0x2;
FUN_001113e4(&local_160,&local_130);
local_c0 = &PTR_s_TFCCTF{_0014fb58;
local_b8 = (code *)0x2;
pcStack_128 = FUN_001115dc;
local_a8 = (ulong *)0x1;
pcStack_a0 = (code *)0x0;
/* try { // try from 00112114 to 00112127 has its CatchHandler @ 00112264 */
local_130 = &local_160;
local_b0 = &local_130;
FUN_001473f8(auStack_148);
FUN_001474d4(&local_130,extraout_x1_01,auStack_148);
pcVar9 = pcStack_128;
if ((char)local_130 != '\x0f') {
FUN_001472c8();
/* try { // try from 00112230 to 0011223f has its CatchHandler @ 00112244 */
FUN_001472e4();
FUN_001472f8();
goto LAB_00112240;
}
FUN_001184e4(local_160,lStack_158);
FUN_00147578();
FUN_00147394(uVar12);
goto LAB_00112184;
}
FUN_0011187c("HACKER bit not set -> returning FAKE",0x24);
local_e8 = &local_160;
local_e0 = FUN_00115b00;
local_160 = local_1f8 ^ 0x2152411021524110;
local_b0 = (ulong **)0x0;
local_a8 = (ulong *)&DAT_00000010;
local_c0 = (undefined **)0x2;
pcStack_a0 = (code *)0x0;
local_98 = (undefined8 *)0x800000020;
FUN_00147354();
FUN_001113e4(auStack_190,&local_130);
FUN_001474d4(&local_130,extraout_x1_00,auStack_190);
pcVar9 = pcStack_128;
if ((char)local_130 == '\x0f') goto LAB_00112184;
FUN_001472c8();
/* try { // try from 00112218 to 00112227 has its CatchHandler @ 00112258 */
FUN_001472e4();
FUN_001472f8();
goto LAB_00112240;
}
local_160 = local_1f8 * 0x5f3759df;
local_e8 = &local_160;
local_e0 = FUN_00115b00;
pcStack_a0 = (code *)0x0;
local_98 = (undefined8 *)0x800000020;
local_c0 = (undefined **)0x2;
local_b0 = (ulong **)0x0;
local_a8 = (ulong *)&DAT_00000010;
FUN_00147354();
/* try { // try from 00111d78 to 00111d8f has its CatchHandler @ 001122b8 */
FUN_001113e4(auStack_1a8,&local_130);
FUN_001474d4(&local_130,extraout_x1,auStack_1a8);
pcVar9 = pcStack_128;
if ((char)local_130 != '\x0f') {
FUN_001472c8();
/* try { // try from 00111da0 to 00111daf has its CatchHandler @ 00112270 */
FUN_001472e4();
FUN_001472f8();
goto LAB_00112240;
}
goto LAB_00112184;
}
local_130 = &local_208;
pcStack_128 = FUN_001115dc;
FUN_001475c4();
local_c0 = &PTR_s_Seed_''_is_not_expected;_require_0014fbe0;
local_b8 = (code *)0x3;
local_b0 = &local_130;
local_a8 = (ulong *)0x2;
pcStack_a0 = (code *)0x0;
/* try { // try from 00111bb0 to 00111bb7 has its CatchHandler @ 001122b8 */
local_120 = extraout_x8;
pcStack_118 = (code *)extraout_x9;
uVar8 = FUN_001473f8(local_1c0);
/* try { // try from 00111bc0 to 00111bc3 has its CatchHandler @ 0011229c */
FUN_0014746c(uVar8,local_1b0);
FUN_00147394(local_1c0[0]);
/* try { // try from 00111bcc to 00111bdb has its CatchHandler @ 001122b8 */
auVar16 = FUN_00119164("FAKE{wrong_seed}",0x10);
if (param_1 == (long *)0x0) {
pcVar9 = (code *)&UNK_00105bb2;
uVar6 = 8;
unaff_x25 = (ulong **)0x6;
bVar15 = false;
}
else if (*param_1 == 0) {
LAB_00111c44:
bVar15 = false;
pcVar9 = (code *)&DAT_00105bb8;
uVar6 = 8;
unaff_x25 = (ulong **)&DAT_00000007;
}
else if (*(long *)(*param_1 + 0x538) == 0) {
bVar15 = false;
pcVar9 = (code *)&DAT_00105c22;
uVar6 = 6;
unaff_x25 = (ulong **)0xc;
}
else {
FUN_001474bc();
pcVar9 = (code *)(*extraout_x8_00)();
if (*param_1 == 0) goto LAB_00111c44;
pcVar11 = *(code **)(*param_1 + 0x720);
if (pcVar11 == (code *)0x0) {
bVar15 = false;
pcVar9 = (code *)&DAT_00105bd0;
uVar6 = 6;
unaff_x25 = (ulong **)0xe;
}
else {
(*pcVar11)(param_1);
FUN_00147400();
if ((bool)uVar6) {
bVar15 = false;
uVar6 = 5;
}
else if (pcVar9 == (code *)0x0) {
bVar15 = false;
pcVar9 = (code *)&DAT_00105c2e;
uVar6 = 7;
unaff_x25 = (ulong **)0x13;
}
else {
uVar6 = 0xf;
bVar15 = true;
}
}
}
FUN_001112e8(auVar16._0_8_,auVar16._8_8_);
if (!bVar15) {
uVar5 = (undefined4)local_e8;
local_c0._0_5_ = CONCAT41(uVar5,uVar6);
local_c0 = (undefined **)CONCAT44(local_e8._3_4_,(undefined4)local_c0);
pcStack_a0 = pcStack_128;
local_a8 = local_130;
uStack_90 = pcStack_118;
local_98 = local_120;
local_b8 = pcVar9;
local_b0 = unaff_x25;
/* try { // try from 00112200 to 0011220f has its CatchHandler @ 00112290 */
FUN_001472e4();
FUN_001472f8();
LAB_00112240:
/* WARNING: Does not return */
pcVar9 = (code *)SoftwareBreakpoint(1,0x112244);
(*pcVar9)();
}
LAB_00112184:
FUN_00147558();
FUN_001184e4(local_208,local_200);
return pcVar9;
}
We can now understand the logs we have seen earlier, and we can see that we are right now blocked by that check:
if (HACKER == 1) {
As the name suggests, we have to modify the lib to set the HACKER
variable to 1
. We will have to patch the library in order to pass this check.
The idea is the following:
NOP
instructions.HACKER
variable to be set to 1
, but the check to change it from ne
to eq
.It is pretty easy to patch the library using Ghidra
. You just have to right click on the instruction you want to patch, and then click on Patch Instruction
. This is for example how I have done it to patch the if (HACKER == 1)
check:
Before:
Patching:
After:
With the same method, we patched the checksum check.
Let’s now export the patched library to be able to use it for a new APK. To do so, just go to File -> Export Program
and select Original File
. You also have to check that you have Export User Byte Modifications
on the Options...
tab when exporting.
The first thing we need to do is to use apktool
to decompile the original APK with apktool d app-release.apk -o apktool_out
. We can now replace the liboxi.so
file in apktool_out/lib/arm64-v8a/
with our patched library.
After that, we need to rebuild it with apktool b apktool_out
, and we will find the new APK in apktool_out/dist/app-release.apk
.
We can now uninstall the previous application with adb uninstall com.example.oxidized_intentions
. But we need to sign the new APK before installing it.
To do so, I usually run frida-gadget --sign app-release.apk
, but I wasn’t able to do so here, as the library was in ARM64
, and the emulator is x86_64
. The issue is that frida-gadget
creates a library in x86_64
that is not compatible with the ARM64
library already in the APK. Therefore, I decided to use another method to sign the APK.
I used zipalign -p -f 4 app-release.apk app-align.apk
to align the APK, and then I signed it with apksigner sign --ks my.keystore --ks-key-alias myalias --out app-signed.apk app-align.apk
as I had already created a keystore for another challenge.
After that, we can install the new APK with adb install app-signed.apk
.
With our new application, we will now pass the HACKER
check. Let’s use the same Frida
script as before to call the getFlag
method, and let’s get the logs with adb logcat | grep OXI
:
09-01 17:18:45.021 17127 17127 D OXI : Got broadcast, seed=fe2o3rust
09-01 17:18:45.470 17127 17127 D OXI : Required seed is: fe2o3rust
09-01 17:18:45.474 17127 17127 D OXI : Computing flag for seed='fe2o3rust' ...
09-01 17:18:46.480 17127 17127 D OXI : anti_hook_check elapsed=1000ms
09-01 17:18:55.950 17127 17127 D OXI : FLAG=TFCCTF{167e3ce3c65387c6e981c31c39ac7839}
We have now passed the check and we have the flag: TFCCTF{167e3ce3c65387c6e981c31c39ac7839}
.
This challenge was the first time I used Frida
, and it was a good opportunity to practice basic function calls.