
This article is an excellent piece from the KX forum.
KX forum author ID: taobluesky
Recently encountered a shell with quite high intensity, sharing how to debug with the shell, purely a technical exchange, please do not use this article for illegal purposes!
1
Prepare Environment
frida 12.11.18 jeb-3.24 jadx-gui-1.3.3-1 xcube android 7.0 (real machine)
Here’s why you must use a real machine: this shell has an emulator detection, and this app does not compile x86 instructions in its so, so it cannot run on an x86 emulator at all. Let’s get straight to the point.
2
Initial Exploration
Attach the HTTP proxy and try to intercept traffic; the client directly reports an SSL certificate error, clearly indicating some form of SSL pinning validation.jadx loads the apk, manifest parsing fails, then pull in jeb for analysis.
Has anyone encountered the shell of com.vdog.VDogApplication? If you know, please enlighten me on what this shell is! I took a rough look at the Java part of the shell’s code:
The shell restored the ELF file header, which is not novel; just manually restore the first four bytes of the so to .ELF.
Then I pulled libvdog-x86.so into IDA for disassembly. The result is that this so is obfuscated quite beautifully; I tried several deobfuscation scripts but to no avail, so I temporarily do not wish to analyze this shell anymore.
So what’s next? I thought of a method to dump the dex first, then use frida for dynamic injection debugging.
Then as expected, there was code to detect frida; once spawn or attach, the app’s process immediately terminates.
[FRD AL00::com.**.*****]-> Process terminated[FRD AL00::com.**.*****]->
Next, I used xcube to load the script and found it feasible. Well, let’s first use FDex2 to dump the dex, a total of 7 dex were dumped.
3
Official Start
First step is to break through the HTTP packet capture problem, using fiddler to capture packets, seeing the HTTPS handshake request User-Agent: okhttp/4.9.1, confirming that the okhttp library is used.
After trying several SSL unpinning scripts with no results, I analyzed the okhttp/4.9.1 library and found an issue with the script provided online: the second parameter of CertificatePinner.check$okhttp’s overload is incorrect, and since this method is not overloaded, I directly removed the overload. Here’s an SSL unpinning script usable for okhttp 4.9.1:
// okhttp4try { var CertificatePinner = Java.use('okhttp3.CertificatePinner'); CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(str) { writeFile('! Intercepted okhttp4 in [check()]: ' + str); return; }; try {//.overload('java.lang.String', 'kotlin.jvm.functions.Function0') CertificatePinner.check$okhttp.implementation = function(str, _) { writeFile('! Intercepted okhttp4 in [check$okhttp]: ' + str); return; }; } catch (ex) { writeFile("is this Okhttp3 ?!"); } writeFile('* Setup okhttp4 pinning')} catch (err) { writeFile('* Unable to hook into okhttp4 pinner') writeFile(err);}
After testing, I successfully captured packets; below is the request body for the login interface:
You can see that the password for the login interface is encrypted, along with the sign field. First, look for the key code for password encryption as follows:
Then follow up:
Using RSA to encrypt the password, this unnamed method is a brilliant injection point; here’s the script to obtain the public key:
// Password encryption: output RSA's pubkeyvar this3 = Java.use("com.**.*****.encrypt.this3");this3.unname.implementation = function(str, str2){ writeFile('unname is called'); writeFile("pubkey:" + str2); var ret = this.unname(str, str2); writeFile('unname ret value is ' + ret); return ret;};
Using Java to restore the algorithm:
try { String password = "qed"; String publicKey = "MIGfMA0GCSqGSIb3D********qGWVMv5z6FwIDAQAB"; byte[] decoded = Base64.decode(publicKey, Base64.DEFAULT); RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); Cipher instance = Cipher.getInstance("RSA/ECB/PKCS1Padding"); instance.init(ENCRYPT_MODE, pubKey); String pwdenc = Base64.encodeToString(instance.doFinal(password.getBytes(StandardCharsets.UTF_8)), Base64.DEFAULT); Log.e(TAG, pwdenc); } catch (Exception e) { e.printStackTrace();}
Alright, the password encryption algorithm has been sorted out. Next, let’s look at the sign algorithm, searching for sign in the dex to find the key position:
Next, follow into the SignUtil class:
You can see that the key method handle is in native-lib; by hooking the get method, you can obtain the parameters passed for signature verification and the sign result:
var SignUtil = Java.use("com.**.****.encrypt.SignUtil");SignUtil.get.implementation = function(str, str2, str3, str4){ writeFile('get is called'); writeFile("str:" + str); writeFile("str2:" + str2); writeFile("str3:" + str3); writeFile("str4:" + str4); var ret = this.get(str, str2, str3, str4); writeFile('get ret value is ' + ret); return ret;};
Load libnative-lib.so into IDA for analysis, the analysis result is as follows:
Function sub_45A40:
The above figure has annotated the key methods and variables, and the logic is very clear: sign=sha256(_requestData+_requestDataList+_token+_clientTime+salt_key). To verify if the concatenation is correct, you can output temp_str and find a suitable hook point:
This can be hooked to output; the corresponding asm code is as follows:
Note that the type of temp_str is std::string; to print output, you need to convert it to cstring. We can use the string_to_cstr function analyzed above for conversion:
After analysis, let’s write the hook code:
var libnative_addr = Module.findBaseAddress("libnative-lib.so")writeFile("libnative_addr is: " + libnative_addr) // Internal std::string to cstring methodvar str_to_c = new NativeFunction(libnative_addr.add(0x45A85), "pointer", ["pointer"]); // Output signature stringtry{ var addr_45266 = libnative_addr.add(0x45267); writeFile("addr_45266: " + addr_45266); Interceptor.attach(addr_45266, { onEnter: function (args) { writeFile("ohwawawa"); var ret = str_to_c(this.context.r2); writeFile("addr_45266 OnEnter sign string:" + Memory.readCString(ret)); }, onLeave: function (retval) { //console.log("retval is :", retval) } });} catch(err) { writeFile("[!!!!!!!!!!!!] " + err);}
Thus, once the signature function is executed, all parameters of the signature, the concatenated signature string, and the return result can be perfectly output. This algorithm is relatively simple, so I won’t write the code to restore it.
The app itself has quite good protection measures, with many checks in place. Also, the code volume of this vdog shell is quite astonishing, with a lot of complex encryption and decryption operations and checks inside, as well as complex obfuscation. I can study how this guy protects against frida when I have time.
We cleverly used xcube to bypass the shell’s detection of frida’s spawn and attach; another point to note is that you can see that the frida scripts I wrote cannot use console.log to output, they all use file writing methods instead. This is because console.log cannot output any content at all; I suspect this is also a shield made by the shell, very annoying. Additionally, the code volume of this type of app is still quite astonishing, and it is split into many dex files, requiring patience during analysis with various cross-analysis across several dex files. Work done!
The original text has uploaded the libvdog; if anyone can figure out its key logic, please remember to @ me to learn!
KX ID: taobluesky
https://bbs.pediy.com/user-home-65525.htm
*This article is original by KX forum taobluesky, please indicate the source from KX community when reprinting.

# Previous Recommendations
1. CVE-2022-21882 Privilege Escalation Vulnerability Study Notes
2. Wibu Certificate – An Initial Exploration
3. Win10 1909 Reverse Engineering of APIC Interrupts and Experiments
4. Analysis of EAF Mechanism Under EMET and Simulation Implementation
5. SQL Injection Learning Sharing
6. Issues with V8 Array.prototype.concat Function and Their POCs
Click “Read the Original” for more information!