Analyzing SSL Pinning on Viber 26.6.4.0 with Frida: A 101 Walkthrough
How I analyzed the SSL pinning behavior of Viber 26.6.4.0 with Frida: pulling the APK, finding the relevant methods in JADX, and writing my own Frida script.

In this post I walk through how I analyzed the SSL pinning behavior of Viber 26.6.4.0 with Frida: how I pulled the APK, which points I looked at in JADX, and how I ended up writing my own Frida script. My goal was to understand how SSL pinning can be analyzed in an app, not to attack or abuse anything.
- Frida 16.4.5runtime instrumentation, server mode
- JADXstatic analysis / decompile of the APK
- ADBdevice access and pulling the APK
Lab Environment
I used a physical Android device for this work.
Device: Samsung Galaxy S8
Device state: Rooted
Frida mode: Frida server
Frida version: 16.4.5
Target package: com.viber.voip
Target app version: Viber 26.6.4.0I find physical devices more practical for this kind of analysis. Especially with Frida server, it is easier to see device behavior directly, control the app start/stop flow, and pull the APK off the device.
1. Verifying the Target App on the Device
First I checked that the device was visible to the computer.
adb devicesThen I checked, on the Frida side, whether the target app showed up on the device:
frida-ps -Uai | grep viberThis command confirms two things for me: whether Frida can talk to the device properly, and whether the target package name is what I expect. In this work the target package name is:
com.viber.voip2. Finding the APK Path and Pulling the APK
To do static analysis I needed the APK file. First I found the APK path on the device:
adb shell pm path com.viber.voipThis returns the APK location on the device. Then I pulled the APK to my computer:
adb pull /data/app/com.viber.voip-rLR9OmRlPkluCQxsG4wyhg==/base.apk viber.apkAn important point here: the APK path can change from device to device or install to install. So instead of memorizing the path, it is better to get the current one with pm path first. The more general usage looks like this:
adb shell pm path com.viber.voip
adb pull <APK_PATH> viber.apkAfter getting the APK I opened it in JADX for static analysis.
3. Trying Existing CodeShare Scripts First
At first I did not write my own script straight away. I wanted to see whether existing general SSL bypass scripts would work on this app. So I tried a couple of CodeShare scripts:
frida --codeshare fdciabdul/tiktokssl -f com.viber.voip -Ufrida --codeshare kaiserBloo/ssl-and-root-bypass -f com.viber.voip -UThese attempts were a starting point for me. Ready-made scripts sometimes give quick results, but they may not be enough for every app and every version. At this point I realized:
Trying general scripts is useful, but the right approach is to understand the app’s own network stack and pinning behavior.
So I went back to static analysis to see which classes and methods were actually in play.
4. Searching for the Relevant Points in JADX
In JADX I did not jump straight to a single point. First I searched for keywords that might relate to SSL pinning and network behavior:
pin
public key
certificate
trust anchor
cronet
ssl
x509During these searches the class org.chromium.net.impl.CronetEngineBuilderImpl caught my eye. The class name itself was already a strong hint:
org.chromium.net.impl.CronetEngineBuilderImplThis class was related to Chromium/Cronet based network behavior. I figured the logic I was looking for on the SSL pinning side might be here.
5. Looking at the addPublicKeyPins(...) Method
One of the first methods that caught my attention in JADX was this:
@Override
public CronetEngineBuilderImpl addPublicKeyPins(String str, Set<byte[]> set, boolean z, Date date) {
if (str == null) {
throw new NullPointerException("The hostname cannot be null");
}
if (set == null) {
throw new NullPointerException("The set of SHA256 pins cannot be null");
}
if (date == null) {
throw new NullPointerException("The pin expiration date cannot be null");
}
String strValidateHostNameForPinningAndConvert = validateHostNameForPinningAndConvert(str);
HashMap map = new HashMap();
for (byte[] bArr : set) {
if (bArr == null || bArr.length != 32) {
throw new IllegalArgumentException("Public key pin is invalid");
}
map.put(Base64.encodeToString(bArr, 0), bArr);
}
this.mPkps.add(new Pkp(
strValidateHostNameForPinningAndConvert,
(byte[][]) map.values().toArray(new byte[map.size()][]),
z,
date
));
return this;
}In short, this method follows this sequence:
- Checks whether the hostname is null.
- Checks whether the pin set is null.
- Checks whether the expiration date is null.
- Normalizes the hostname for pinning.
- Validates the public key pin values.
- Adds the pin info to the mPkps list.
- Returns the builder instance.
The most critical line for me was:
this.mPkps.add(new Pkp(...));Because this is where the public key pin info gets added to the app’s network builder. So this was the first method I targeted on the Frida side. My goal was to stop the real body from running when this method is called, and to skip the pin-adding step.
6. Looking at enablePublicKeyPinningBypassForLocalTrustAnchors(...)
The second method that caught my attention was this:
@Override
public CronetEngineBuilderImpl enablePublicKeyPinningBypassForLocalTrustAnchors(boolean z) {
this.mPublicKeyPinningBypassForLocalTrustAnchorsEnabled = z;
return this;
}This method is quite simple. It takes an incoming boolean and writes it to this field:
mPublicKeyPinningBypassForLocalTrustAnchorsEnabledIf this behavior is controlled by a boolean, I can force that value to true at runtime.
So in the second part of the script I targeted this method.
7. Writing the Frida Script
After finding the two important points in JADX, I wrote the Frida script. The goal had two parts: catch addPublicKeyPins(...) calls and skip the pin-adding step, and force the value to true in enablePublicKeyPinningBypassForLocalTrustAnchors(...). The script I used:
/*
Android Viber 26.6.4.0 SSL certificate pinning
by Yasar Kahramaner
Run with:
frida -U -f com.viber.voip -l viber-26-6-4-0-ssl-pinning.js
*/
Java.perform(() => {
const B = Java.use('org.chromium.net.impl.CronetEngineBuilderImpl');
B.addPublicKeyPins.overloads.forEach(o => {
o.implementation = function(host, set, enforce, date) {
console.log('skip pins for', host);
return this;
};
});
B.enablePublicKeyPinningBypassForLocalTrustAnchors
.overload('boolean')
.implementation = function(_) {
console.log('force bypass local trust anchors');
return this.enablePublicKeyPinningBypassForLocalTrustAnchors(true);
};
});8. Breaking the Script Down Piece by Piece
First I make the code run once the Java runtime is ready:
Java.perform(() => {
// hooks
});Then I grab the target class. This was the class I examined in JADX and believed was related to pinning behavior:
const B = Java.use('org.chromium.net.impl.CronetEngineBuilderImpl');Then I hook all overloads of the addPublicKeyPins(...) method:
B.addPublicKeyPins.overloads.forEach(o => {
o.implementation = function(host, set, enforce, date) {
console.log('skip pins for', host);
return this;
};
});The aim here is to log when the method is called, not run the original body, and return this so the builder chain behavior is not broken. Normally this method added a pin to the mPkps list; by directly doing return this; I skipped the pin-adding step.
The second hook targets the local trust anchor bypass behavior:
B.enablePublicKeyPinningBypassForLocalTrustAnchors
.overload('boolean')
.implementation = function(_) {
console.log('force bypass local trust anchors');
return this.enablePublicKeyPinningBypassForLocalTrustAnchors(true);
};In this part, no matter what value is passed to the method, I forced it to true.
9. Running the Script
The command I used to run the script:
frida -U -f com.viber.voip -l ./viber.jsThe flags in short: -U targets the USB-connected device, -f com.viber.voip spawns the app, and -l ./viber.js loads the script I wrote. This flow was cleaner for me because I could start the app with Frida from the beginning and have the hooks kick in early.
10. Verification
In this kind of work, saying the script ran is not enough. You need to verify that it actually intervened at the right point. During verification I especially looked at:
- Does the script load without errors?
- Is the addPublicKeyPins(...) hook triggered?
- Do the skip pins for ... logs show up in the console?
- Is the enablePublicKeyPinningBypassForLocalTrustAnchors(...) hook triggered?
- Does the app keep working in its normal flow?
- Does the intervention cause a wider effect than expected?
I do not share any real host, endpoint, token, request or response here. What mattered to me was not the traffic content, but seeing whether the target methods actually get triggered at runtime.
11. Why My Own Script Instead of a Ready-Made One?
Ready-made CodeShare scripts can sometimes work. But for me the real learning is in understanding the app’s own behavior and writing a target-specific script. After trying the general SSL bypass scripts, I realized:
Without understanding which network stack an app uses, where pinning is added, and which methods are triggered at runtime, scripts often stay guesswork.
So I kept my own script more targeted. This is not a works-on-every-app solution; it is a piece of work tailored to the Cronet-based behavior I observed specifically on Viber 26.6.4.0.
12. Problems You Might Hit
A few common problems can come up in this kind of analysis.
Frida does not see the device
First check that the device is visible to ADB, then check the Frida side:
adb devicesfrida-ps -UaiThe package name might be wrong
To verify the package name:
frida-ps -Uai | grep viberor:
adb shell pm list packages | grep viberThe APK path might have changed
The APK path is not fixed. Find the path first, then use the returned one:
adb shell pm path com.viber.voipadb pull <APK_PATH> viber.apkThe class or method name might have changed
If the app version changed, CronetEngineBuilderImpl, addPublicKeyPins or the related methods may differ. So in every analysis you should pin down the version info and search again in JADX.
The hook might not trigger
If the hook does not trigger there are a few possibilities: the app might use a different network stack, the method might not be called in that flow, the hook might load too late, the app version might have changed, or the behavior might happen on the native side.
This work reminded me of a few things. Ready-made bypass scripts can be useful starting points but are not always enough; the real learning starts when you try to understand the app’s own behavior. JADX and Frida together make a powerful flow: with JADX you find candidate points statically, with Frida you test at runtime whether they actually run. Version info matters a lot; this note is specifically for Viber 26.6.4.0. And more important than the script working is being able to explain what it intervened in; otherwise you end up with a result that works but you cannot explain.
Public Record
The public record of this work is on Frida CodeShare under this name:
YasarKah / viber-26-6-4-0-ssl-pinningThis post documents the analysis process behind that CodeShare script, the tools I used, and how I progressed.