commit cfe4a969707be78040f2e85c72c15a24bdad7bfc Author: Tine Jozelj Date: Mon Sep 25 01:47:25 2023 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e819bcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +NewFolder +Go* +frida-server diff --git a/README.md b/README.md new file mode 100644 index 0000000..b60520d --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# Reverse engineering Slovenian Railways API + + +### Endpoints + +__Base URL:__ `https://eshop.sz.si/SZMobile/` + +``` +/* renamed from: n2 */ +/* loaded from: classes.dex */ +public interface SZEshopService { + @POST("authenticate_enc/") + ta1 authenticateDevice(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("GetCard_enc/") + ta1 getCard(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("GetCatalogs/") + ta1 getCatalogs(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("get_messages_enc/") + ta1 getMessages(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("GetOvire_enc/") + ta1 getObstacles(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @Headers({"CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000"}) + @GET("https://eshop.sz.si/Purchase/DownloadReceiptAsPdf") + Call getPdfTicketFile(@Query("receiptNumber") String str); + + @POST("GetProducts_enc/") + ta1 getProducts(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("GetReceipt_enc/") + ta1 getReceipt(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("GetRoutesSz_enc/") + ta1 getRoutes(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("GetStations_enc/") + ta1 getStations(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("GetSzCardProducts_enc/") + ta1 getSzCardProducts(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("SetReceipt_enc/") + ta1 setReceipt(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("SetReceiptSz_enc/") + ta1 setReceiptNew(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); + + @POST("update_message_enc/") + ta1 updateMessage(@Body SZEshopEncryptedRequestBody sZEshopEncryptedRequestBody); +} +``` + +#### `SZEshopEncryptedRequestBody` + +``` +{ + "req": "encrypted contents", + "auth_token": "plain text token, looks like uuid" +} +``` + +### How to get token? + +``` + @Override // p000.InterfaceSZEshopClient + public void authenticateDevice(SZResponse sZResponse, String serial) { + CryptoUtil.configurePublicAndPrivateKeys(this.sharedPreferences); + SZAuthenticateDeviceRequest sZAuthenticateDeviceRequest = new SZAuthenticateDeviceRequest(); + sZAuthenticateDeviceRequest.setDevice_serial(serial); // can be anything? + sZAuthenticateDeviceRequest.setDevice_description(C1954lk.f7875a); // "Android " + Build.VERSION.SDK_INT + "/" + Build.MANUFACTURER + " " + Build.MODEL + sZAuthenticateDeviceRequest.setApp_version("1.0.26"); + sZAuthenticateDeviceRequest.setPlatform_id(C2407pb.f9245a.intValue()); // 1 + sZAuthenticateDeviceRequest.setKey(CryptoUtil.encryptPartAES()); + sZAuthenticateDeviceRequest.setPublic_key(CryptoUtil.getShortPublicKey()); + getData(this.service.authenticateDevice(getRequest(sZAuthenticateDeviceRequest)), sZResponse, SZResponseAuthnticateDeviceResult.class, true); + } +``` + +### How to perform encryption and decryption? + +``` + /* renamed from: a */ + public static byte[] AESDecryptResponse(byte[] response, Context context) { + if (deviceAuthenticationKey != null) { + try { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); + cipher.init(2, new SecretKeySpec(deviceAuthenticationKey, "AES"), new IvParameterSpec(m9812g())); + return cipher.doFinal(response); + } catch (Exception e) { + String str = className; + Logger.verbose(str, "AESDecryptResponse: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + return m9802p(m9799v(response, context)); + } + + /* renamed from: b */ + public static String AESEncryptRequest(String request, Context context) { + if (deviceAuthenticationKey != null) { + try { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); + cipher.init(1, new SecretKeySpec(deviceAuthenticationKey, "AES"), new IvParameterSpec(m9812g())); + return Base64.encodeToString(cipher.doFinal(request.getBytes()), 2); + } catch (Exception e) { + String str = className; + Logger.verbose(str, "AESEncryptRequest: " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + return Base64.encodeToString(m9798w(m9814e(request.getBytes()), context), 2); + } +``` diff --git a/client.js b/client.js new file mode 100644 index 0000000..e69de29 diff --git a/frida-script.js b/frida-script.js new file mode 100644 index 0000000..c66ebbd --- /dev/null +++ b/frida-script.js @@ -0,0 +1,669 @@ +function rootChecking() { + var RootPackages = [ + "com.noshufou.android.su", + "com.noshufou.android.su.elite", + "eu.chainfire.supersu", + "com.koushikdutta.superuser", + "com.thirdparty.superuser", + "com.yellowes.su", + "com.koushikdutta.rommanager", + "com.koushikdutta.rommanager.license", + "com.dimonvideo.luckypatcher", + "com.chelpus.lackypatch", + "com.ramdroid.appquarantine", + "com.ramdroid.appquarantinepro", + "com.devadvance.rootcloak", + "com.devadvance.rootcloakplus", + "de.robv.android.xposed.installer", + "com.saurik.substrate", + "com.zachspong.temprootremovejb", + "com.amphoras.hidemyroot", + "com.amphoras.hidemyrootadfree", + "com.formyhm.hiderootPremium", + "com.formyhm.hideroot", + "me.phh.superuser", + "eu.chainfire.supersu.pro", + "com.kingouser.com", + "com.topjohnwu.magisk", + ]; + + var RootBinaries = [ + "su", + "busybox", + "supersu", + "Superuser.apk", + "KingoUser.apk", + "SuperSu.apk", + "magisk", + ]; + + var RootProperties = { + "ro.build.selinux": "1", + "ro.debuggable": "0", + "service.adb.root": "0", + "ro.secure": "1", + }; + + var RootPropertiesKeys = []; + + for (var k in RootProperties) RootPropertiesKeys.push(k); + + var PackageManager = Java.use("android.app.ApplicationPackageManager"); + + var Runtime = Java.use("java.lang.Runtime"); + + var NativeFile = Java.use("java.io.File"); + + var String = Java.use("java.lang.String"); + + var SystemProperties = Java.use("android.os.SystemProperties"); + + var BufferedReader = Java.use("java.io.BufferedReader"); + + var ProcessBuilder = Java.use("java.lang.ProcessBuilder"); + + var StringBuffer = Java.use("java.lang.StringBuffer"); + + var loaded_classes = Java.enumerateLoadedClassesSync(); + + send("Loaded " + loaded_classes.length + " classes!"); + + var useKeyInfo = false; + + var useProcessManager = false; + + send("loaded: " + loaded_classes.indexOf("java.lang.ProcessManager")); + + if (loaded_classes.indexOf("java.lang.ProcessManager") != -1) { + try { + //useProcessManager = true; + //var ProcessManager = Java.use('java.lang.ProcessManager'); + } catch (err) { + send("ProcessManager Hook failed: " + err); + } + } else { + send("ProcessManager hook not loaded"); + } + + var KeyInfo = null; + + if (loaded_classes.indexOf("android.security.keystore.KeyInfo") != -1) { + try { + //useKeyInfo = true; + //var KeyInfo = Java.use('android.security.keystore.KeyInfo'); + } catch (err) { + send("KeyInfo Hook failed: " + err); + } + } else { + send("KeyInfo hook not loaded"); + } + + PackageManager.getPackageInfo.overload( + "java.lang.String", + "int" + ).implementation = function (pname, flags) { + var shouldFakePackage = RootPackages.indexOf(pname) > -1; + if (shouldFakePackage) { + send("Bypass root check for package: " + pname); + pname = "set.package.name.to.a.fake.one.so.we.can.bypass.it"; + } + return this.getPackageInfo + .overload("java.lang.String", "int") + .call(this, pname, flags); + }; + + NativeFile.exists.implementation = function () { + var name = NativeFile.getName.call(this); + var shouldFakeReturn = RootBinaries.indexOf(name) > -1; + if (shouldFakeReturn) { + send("Bypass return value for binary: " + name); + return false; + } else { + return this.exists.call(this); + } + }; + + var exec = Runtime.exec.overload("[Ljava.lang.String;"); + var exec1 = Runtime.exec.overload("java.lang.String"); + var exec2 = Runtime.exec.overload("java.lang.String", "[Ljava.lang.String;"); + var exec3 = Runtime.exec.overload( + "[Ljava.lang.String;", + "[Ljava.lang.String;" + ); + var exec4 = Runtime.exec.overload( + "[Ljava.lang.String;", + "[Ljava.lang.String;", + "java.io.File" + ); + var exec5 = Runtime.exec.overload( + "java.lang.String", + "[Ljava.lang.String;", + "java.io.File" + ); + + exec5.implementation = function (cmd, env, dir) { + if ( + cmd.indexOf("getprop") != -1 || + cmd == "mount" || + cmd.indexOf("build.prop") != -1 || + cmd == "id" || + cmd == "sh" + ) { + var fakeCmd = "grep"; + send("Bypass " + cmd + " command"); + return exec1.call(this, fakeCmd); + } + if (cmd == "su") { + var fakeCmd = + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; + send("Bypass " + cmd + " command"); + return exec1.call(this, fakeCmd); + } + return exec5.call(this, cmd, env, dir); + }; + + exec4.implementation = function (cmdarr, env, file) { + for (var i = 0; i < cmdarr.length; i = i + 1) { + var tmp_cmd = cmdarr[i]; + if ( + tmp_cmd.indexOf("getprop") != -1 || + tmp_cmd == "mount" || + tmp_cmd.indexOf("build.prop") != -1 || + tmp_cmd == "id" || + tmp_cmd == "sh" + ) { + var fakeCmd = "grep"; + send("Bypass " + cmdarr + " command"); + return exec1.call(this, fakeCmd); + } + + if (tmp_cmd == "su") { + var fakeCmd = + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; + send("Bypass " + cmdarr + " command"); + return exec1.call(this, fakeCmd); + } + } + return exec4.call(this, cmdarr, env, file); + }; + + exec3.implementation = function (cmdarr, envp) { + for (var i = 0; i < cmdarr.length; i = i + 1) { + var tmp_cmd = cmdarr[i]; + if ( + tmp_cmd.indexOf("getprop") != -1 || + tmp_cmd == "mount" || + tmp_cmd.indexOf("build.prop") != -1 || + tmp_cmd == "id" || + tmp_cmd == "sh" + ) { + var fakeCmd = "grep"; + send("Bypass " + cmdarr + " command"); + return exec1.call(this, fakeCmd); + } + + if (tmp_cmd == "su") { + var fakeCmd = + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; + send("Bypass " + cmdarr + " command"); + return exec1.call(this, fakeCmd); + } + } + return exec3.call(this, cmdarr, envp); + }; + + exec2.implementation = function (cmd, env) { + if ( + cmd.indexOf("getprop") != -1 || + cmd == "mount" || + cmd.indexOf("build.prop") != -1 || + cmd == "id" || + cmd == "sh" + ) { + var fakeCmd = "grep"; + send("Bypass " + cmd + " command"); + return exec1.call(this, fakeCmd); + } + if (cmd == "su") { + var fakeCmd = + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; + send("Bypass " + cmd + " command"); + return exec1.call(this, fakeCmd); + } + return exec2.call(this, cmd, env); + }; + + exec.implementation = function (cmd) { + for (var i = 0; i < cmd.length; i = i + 1) { + var tmp_cmd = cmd[i]; + if ( + tmp_cmd.indexOf("getprop") != -1 || + tmp_cmd == "mount" || + tmp_cmd.indexOf("build.prop") != -1 || + tmp_cmd == "id" || + tmp_cmd == "sh" + ) { + var fakeCmd = "grep"; + send("Bypass " + cmd + " command"); + return exec1.call(this, fakeCmd); + } + + if (tmp_cmd == "su") { + var fakeCmd = + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; + send("Bypass " + cmd + " command"); + return exec1.call(this, fakeCmd); + } + } + + return exec.call(this, cmd); + }; + + exec1.implementation = function (cmd) { + if ( + cmd.indexOf("getprop") != -1 || + cmd == "mount" || + cmd.indexOf("build.prop") != -1 || + cmd == "id" || + cmd == "sh" + ) { + var fakeCmd = "grep"; + send("Bypass " + cmd + " command"); + return exec1.call(this, fakeCmd); + } + if (cmd == "su") { + var fakeCmd = + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"; + send("Bypass " + cmd + " command"); + return exec1.call(this, fakeCmd); + } + return exec1.call(this, cmd); + }; + + String.contains.implementation = function (name) { + if (name == "test-keys") { + send("Bypass test-keys check"); + return false; + } + return this.contains.call(this, name); + }; + + var get = SystemProperties.get.overload("java.lang.String"); + + get.implementation = function (name) { + if (RootPropertiesKeys.indexOf(name) != -1) { + send("Bypass " + name); + return RootProperties[name]; + } + return this.get.call(this, name); + }; + + Interceptor.attach(Module.findExportByName("libc.so", "fopen"), { + onEnter: function (args) { + var path = Memory.readCString(args[0]); + path = path.split("/"); + var executable = path[path.length - 1]; + var shouldFakeReturn = RootBinaries.indexOf(executable) > -1; + if (shouldFakeReturn) { + Memory.writeUtf8String(args[0], "/notexists"); + send("Bypass native fopen"); + } + }, + onLeave: function (retval) {}, + }); + + Interceptor.attach(Module.findExportByName("libc.so", "system"), { + onEnter: function (args) { + var cmd = Memory.readCString(args[0]); + send("SYSTEM CMD: " + cmd); + if ( + cmd.indexOf("getprop") != -1 || + cmd == "mount" || + cmd.indexOf("build.prop") != -1 || + cmd == "id" + ) { + send("Bypass native system: " + cmd); + Memory.writeUtf8String(args[0], "grep"); + } + if (cmd == "su") { + send("Bypass native system: " + cmd); + Memory.writeUtf8String( + args[0], + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled" + ); + } + }, + onLeave: function (retval) {}, + }); + + /* + + TO IMPLEMENT: + + Exec Family + + int execl(const char *path, const char *arg0, ..., const char *argn, (char *)0); + int execle(const char *path, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]); + int execlp(const char *file, const char *arg0, ..., const char *argn, (char *)0); + int execlpe(const char *file, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]); + int execv(const char *path, char *const argv[]); + int execve(const char *path, char *const argv[], char *const envp[]); + int execvp(const char *file, char *const argv[]); + int execvpe(const char *file, char *const argv[], char *const envp[]); + + */ + + BufferedReader.readLine.overload("boolean").implementation = function () { + var text = this.readLine.overload("boolean").call(this); + if (text === null) { + // just pass , i know it's ugly as hell but test != null won't work :( + } else { + var shouldFakeRead = text.indexOf("ro.build.tags=test-keys") > -1; + if (shouldFakeRead) { + send("Bypass build.prop file read"); + text = text.replace( + "ro.build.tags=test-keys", + "ro.build.tags=release-keys" + ); + } + } + return text; + }; + + var executeCommand = ProcessBuilder.command.overload("java.util.List"); + + ProcessBuilder.start.implementation = function () { + var cmd = this.command.call(this); + var shouldModifyCommand = false; + for (var i = 0; i < cmd.size(); i = i + 1) { + var tmp_cmd = cmd.get(i).toString(); + if ( + tmp_cmd.indexOf("getprop") != -1 || + tmp_cmd.indexOf("mount") != -1 || + tmp_cmd.indexOf("build.prop") != -1 || + tmp_cmd.indexOf("id") != -1 + ) { + shouldModifyCommand = true; + } + } + if (shouldModifyCommand) { + send("Bypass ProcessBuilder " + cmd); + this.command.call(this, ["grep"]); + return this.start.call(this); + } + if (cmd.indexOf("su") != -1) { + send("Bypass ProcessBuilder " + cmd); + this.command.call(this, [ + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled", + ]); + return this.start.call(this); + } + + return this.start.call(this); + }; + + if (useProcessManager) { + var ProcManExec = ProcessManager.exec.overload( + "[Ljava.lang.String;", + "[Ljava.lang.String;", + "java.io.File", + "boolean" + ); + var ProcManExecVariant = ProcessManager.exec.overload( + "[Ljava.lang.String;", + "[Ljava.lang.String;", + "java.lang.String", + "java.io.FileDescriptor", + "java.io.FileDescriptor", + "java.io.FileDescriptor", + "boolean" + ); + + ProcManExec.implementation = function (cmd, env, workdir, redirectstderr) { + var fake_cmd = cmd; + for (var i = 0; i < cmd.length; i = i + 1) { + var tmp_cmd = cmd[i]; + if ( + tmp_cmd.indexOf("getprop") != -1 || + tmp_cmd == "mount" || + tmp_cmd.indexOf("build.prop") != -1 || + tmp_cmd == "id" + ) { + var fake_cmd = ["grep"]; + send("Bypass " + cmdarr + " command"); + } + + if (tmp_cmd == "su") { + var fake_cmd = [ + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled", + ]; + send("Bypass " + cmdarr + " command"); + } + } + return ProcManExec.call(this, fake_cmd, env, workdir, redirectstderr); + }; + + ProcManExecVariant.implementation = function ( + cmd, + env, + directory, + stdin, + stdout, + stderr, + redirect + ) { + var fake_cmd = cmd; + for (var i = 0; i < cmd.length; i = i + 1) { + var tmp_cmd = cmd[i]; + if ( + tmp_cmd.indexOf("getprop") != -1 || + tmp_cmd == "mount" || + tmp_cmd.indexOf("build.prop") != -1 || + tmp_cmd == "id" + ) { + var fake_cmd = ["grep"]; + send("Bypass " + cmdarr + " command"); + } + + if (tmp_cmd == "su") { + var fake_cmd = [ + "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled", + ]; + send("Bypass " + cmdarr + " command"); + } + } + return ProcManExecVariant.call( + this, + fake_cmd, + env, + directory, + stdin, + stdout, + stderr, + redirect + ); + }; + } + + if (useKeyInfo) { + KeyInfo.isInsideSecureHardware.implementation = function () { + send("Bypass isInsideSecureHardware"); + return true; + }; + } +} + +var config = { + package: "com.margento.slovenskezeleznice", + printSummary: true, + printDefinition: true, + printArgs: true, + printReturn: true, + excludeStrings: [], + timeout: 500, +}; +var levels = { + info: "INFO", + warn: "WARN", + error: "ERROR", +}; +var colors = { + reset: "\x1b[39;49;00m", + black: "\x1b[30;01m", + blue: "\x1b[34;01m", + cyan: "\x1b[36;01m", + gray: "\x1b[37;11m", + green: "\x1b[32;01m", + purple: "\x1b[35;01m", + red: "\x1b[31;01m", + yellow: "\x1b[33;01m", + light: { + black: "\x1b[30;11m", + blue: "\x1b[34;11m", + cyan: "\x1b[36;11m", + gray: "\x1b[37;01m", + green: "\x1b[32;11m", + purple: "\x1b[35;11m", + red: "\x1b[31;11m", + yellow: "\x1b[33;11m", + }, +}; + +function log(level, text) { + console.log( + `${ + level === levels.info + ? colors.light.blue + : level === levels.warn + ? colors.light.yellow + : colors.light.red + }[${level}]${colors.reset} ${text}` + ); +} + +function inspectClass(obj) { + var methods = obj.class.getDeclaredMethods(); + methods.forEach((m) => { + var name = m.getName(); + var method = obj[name]; + if (!method) { + return; + } + + if (config.printSummary) { + log(levels.info, `\t\t- Method: ${name}`); + } + + var overloads = method.overloads; + for (var overload of overloads) { + overload.implementation = function () { + log( + levels.warn, + `+ Call: ${ + config.printDefinition + ? m + : m.getDeclaringClass().getName() + "." + name + }` + ); + + if (config.printArgs) { + var i = 0; + for (var arg of arguments) { + if (arg !== null && arg !== undefined) { + log(levels.warn, `\t\t- Args[${i}]: ${arg.toString()}`); + } + + i++; + } + } + + var ret = this[name].apply(this, arguments); + if (config.printReturn) { + if (ret !== null && ret !== undefined) { + log(levels.warn, `\t\t- Return: ${ret.toString()}`); + } + } + return ret; + }; + } + }); +} + +function tracePackage() { + console.log(); + Java.enumerateLoadedClassesSync() + .filter((c) => c.includes(config.package)) + .forEach((c) => { + try { + if (config.printSummary) { + log(levels.info, `+ Class: ${c}`); + } + + if ( + config.excludeStrings.filter((e) => c.toString().includes(e)).length + ) { + return; + } + + var obj = Java.use(c); + inspectClass(obj); + } catch (e) { + log(levels.error, e); + } + }); +} + +Java.perform(function () { + rootChecking(); +}); + +Java.perform(function () { + //tracePackage(); +}); + +function triggerClient() { + Java.perform(function () { + Java.choose("u11", { + onMatch: (instance) => { + inspectClass(instance); + instance.getRequest.implementation = function (request) { + const body = this.getRequest(request); + console.log(body); + return body; + }; + return instance; + }, + onComplete: () => {}, + }); + }); +} + +Java.perform(function () { + const JString = Java.use("java.lang.String"); + const JBase64 = Java.use("android.util.Base64"); + const JStandardCharsets = Java.use("java.nio.charset.StandardCharsets"); + const cryptoUtil = Java.use( + "com.margento.slovenskezeleznice.util.CryptoUtil" + ); + // Decryption + cryptoUtil.a.implementation = function (encrypted, context) { + const jsonByteArray = this.a(encrypted, context); + const json = JString.$new(jsonByteArray); + console.log("Response decrypted: ", json); + console.log("Response encrypted: ", JBase64.encodeToString(encrypted, 2)); + + return jsonByteArray; + }; + // Encryption + cryptoUtil.b.implementation = function (json, context) { + const encrypted = this.b(json, context); + console.log("Request decrypted: ", json); + console.log("Request encrypted: ", JString.$new(encrypted)); + return encrypted; + }; + + cryptoUtil.r.implementation = function (authnticateDeviceResult) { + this.r(authnticateDeviceResult); + console.log("TOKEN: ", authnticateDeviceResult.getToken()); + }; +});