Oxidized Intentions, TFC CTF 2025
Oxidized intentions
I have done this challenge with briskness-alive-tinderbox.
Handout
1
Can you find what lays behind this simple game ?
We also have a file named app-release.apk.
First analysis
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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.
Frida script
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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:
1
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:
1
2
3
4
5
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.
Library analysis
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
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:
1
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:
- As there is a checksum check just before, we will patch the library to skip this check, by replacing the check with
NOPinstructions. - We will after not patch the
HACKERvariable to be set to1, but the check to change it fromnetoeq.
Patching the library
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.
Rebuild the APK
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.
Getting the flag
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:
1
2
3
4
5
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}.
Conclusion
This challenge was the first time I used Frida, and it was a good opportunity to practice basic function calls.





