We now have our very own trivia app! Solve 1000 questions and win a flag!
1 file: client.apk
Writeup
I solved this challenge together with @sen.
We have an apk, so the first thing I did was to try the application using an android 9 device with genymotion.
This is the main view:
We are asked an username and then:
Let the quiz starts:
Basically we need to answer correctly 1000 times, and we have 10 seconds for each question. Since it’s a bit hard to do it manually we can try to reverse the application and check if there are some vulnerability or magic trick that we can do to solve all the trivia questions.
These are the main classes of the app obtained using jadx:
And this is the MainActivity decompiled:
Without spending too much time reading the code it’s very easy to spot that the app tries to open a connection to a server using either http or https.
There are these methods which handle the connections:
We didn’t spend too much readin the MainActivity and we have focused on the Game:
We can see that the last instruction is a Log, maybe the application logs some useful informations. Let’s use the app while we intercept the log with adb.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
$ adb logcat
04-26 15:34:02.432 2024 2693 W eacatpanda.qui: Got a deoptimization request on un-deoptimizable method void libcore.io.Linux.connect(java.io.FileDescriptor, java.net.InetAddress, int) 04-26 15:34:02.672 2024 2693 E WS : OUT: {"method":"ident","userToken":"2c19a4f08f15049c6aa7f60a3e448f949653963b2d218dd2eb1f194322bafd56"} 04-26 15:34:02.787 2024 2693 E WS : IN: {"method":"ident","success":true} 04-26 15:34:02.788 486 1874 I ActivityManager: START u0 {cmp=wtf.riceteacatpanda.quiz/.LoggedIn (has extras)} from uid 10070 04-26 15:34:02.805 2024 2024 W ActivityThread: handleWindowVisibility: no activity for token android.os.BinderProxy@394d403 04-26 15:34:03.023 486 509 I ActivityManager: Displayed wtf.riceteacatpanda.quiz/.LoggedIn: +215ms 04-26 15:34:04.268 413 2700 D NuPlayerDriver: notifyListener_l(0xe9173c00), (1, 0, 0, -1), loop setting(0, 0) 04-26 15:34:04.277 2024 2024 E XO : isFirst = false 04-26 15:34:04.387 2024 2693 E WS : IN: {"method":"start","success":true} 04-26 15:34:04.389 2024 2693 E WS : IN: {"method":"question","id":"06c09777-db20-41dc-949b-f9739fd02304","questionText":"Pff91G6VGfv3scsbU8jCn8bt+TPZiiBrjKLoJyXUlkIFHJzs+byYgJRbvTBSQMWmcdQdGKEat7ihPrCY6fFtMepw0c41NEg40jc7agIc5ht49QzJMrh4H3BdMoBvsQfOgdhOVN2QDdSgGBUt4iD/yg==","options":["CwgURuYmO8M/cXsv0IVB1A==","s6OkXzIY9Sf5IVOa31lqew==","P/oEdn2xEgpWYI4ORb7r2urye5MxjsDRV2GNr4hod6c=","cf3yETb4O+NAsgWObp+i2w=="],"correctAnswer":"o1LcVqNplAatnDMh1MsKRc3f0Hwoh/hQ6jH6TaY34MI=","requestIdentifier":"d1df4a572d853f88f5f47fae3418f8b4"} 04-26 15:34:04.396 413 1087 D NuPlayerDriver: start(0xe9173c00), state is 4, eos is 0 04-26 15:34:04.426 359 359 I chatty : uid=1000(system) allocator@1.0-s identical 6 lines 04-26 15:34:04.426 359 359 W AshmemAllocator: ashmem_create_region(65536) returning hidl_memory(0xe759c180, 65536) 04-26 15:34:04.429 413 2704 I NuPlayerDecoder: [audio] saw output EOS 04-26 15:34:05.558 413 2700 D NuPlayerDriver: notifyListener_l(0xe9173c00), (2, 0, 0, -1), loop setting(0, 0) 04-26 15:34:05.559 413 2700 D NuPlayerDriver: notifyListener_l(0xe9173c00), (211, 0, 0, 20), loop setting(0, 0) 04-26 15:34:08.165 2024 2693 E WS : IN: {"method":"question","id":"3805db30-f0c2-41c6-8d27-04d0e0f40257","questionText":"Y5az9q2GsTP8+WAPdMrpLudbjl4E2HKFDcImkeaD1JJ2A7NX2M8gwxX+PQdPZjtHUO3mAgxSioGlx+0hl5X+3Holb9Y8iqGILSUEUcN7r6g=","options":["khxj/WC/p5rLsah6MIe8sg==","w3I7kGDVMf6sBPmIIgfaD7ewrpI5s5VelKri2fFweWM=","Z3mjJ5b8/s5N9NgQPa8q0w==","IRlRyZsD4cH27ox2pwfw+w=="],"correctAnswer":"z+HmZx47pPt3a7Qp7MsYKknQFRwHgM11j2DPAaRNZnY=","requestIdentifier":"cf1e832cbc42556ded7009e4e60aebf8"}
Yeah we can see the JSON, and we can also see the field correctAnswer. However all fields are encrypted using AES-256-CBC.
What can we do?
Well, we can use frida. To use it with android there is a specialized doc.
We can try to use the SecretKeySpec dumper to get the AES key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Java.perform(function () { var SecretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec'); SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function(p0, p1) { console.log('SecretKeySpec.$init("' + bytes2hex(p0) + '", "' + p1 + '")'); returnthis.$init(p0, p1); }; }); functionbytes2hex(array) { var result = ''; console.log('len = ' + array.length); for(var i = 0; i < array.length; ++i) result += ('0' + (array[i] & 0xFF).toString(16)).slice(-2); return result; }
Launch it:
1 2 3 4 5 6 7 8 9 10 11 12 13
frida -l magic.js -U wtf.riceteacatpanda.quiz --no-pause ____ / _ | Frida 12.8.20 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://www.frida.re/docs/home/ [Houseplant::wtf.riceteacatpanda.quiz]-> len = 32 SecretKeySpec.$init("14001d5c791ab9cb6bb714d71324544f6a2acdea8c80f4417f376c6b7bc4902e", "AES")
And there it is. However we need to view the decrypted JSON and to autosubmit the answer, so we need to interact more with the app. The submition of the answer is handled with:
Create an object of the class nw (onClick handler).
Create an object of the class Base64 (to decode JSON fields).
Create an object of the class String (to create custom string).
Overload the constructor of JSONObject with our custom implementation.
Overload the method Cipher.init() with our custom implementation to beloved able to read the content in real time and submit the answer with nw.a().
Exploit
This is our final script. It was very important to put Java.deoptimizeEverything since frida after 10/20 correct answers wasn’t able to overload the method Cipher.init().
console.log("loaded successful"); Java.perform(functionx() { Java.deoptimizeEverything() var json; var done = 1; var b64 = Java.use('android.util.Base64'); var stringJava = Java.use('java.lang.String'); var nwClass = Java.use('nw'); var jsonObject = Java.use('org.json.JSONObject'); jsonObject.$init.overload('java.lang.String').implementation = function(s) { console.log("calling jsonObject, with arg:" + s); // duplicate this object for later use outside the overload json = Java.retain(this); returnthis.$init(s); }
var cipher = Java.use('javax.crypto.Cipher'); cipher.init.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function(a1, a2, a3) { console.log("calling cipher, with arg:"); // initialize the cipher var a = this.init(a1, a2, a3); // read the correct answer var dec = this.doFinal(b64.decode(json.getString("correctAnswer"), 0)); var answer = stringJava.$new(dec); console.log("done: " + done); done = done + 1; // submit correct answer nwClass.a().a("{\"method\":\"answer\",\"answer\":" + answer + "}"); return a; }; });