Runtime Instrumentation of Qt6 Apps with Frida - Part 2: Building the Bypass Chain

In Part 1, we covered visibility - tracing QString, QMetaObject::activate, walking metaobjects, and triggering Q_INVOKABLE methods directly from Frida. In this part, we turn the same primitives into bypasses.
HackPass is again our target.
Two new scripts added to the set:
qt-find-by-class.js- scans rw memory for QObject instances belonging to HackPass classes and returns those whose className matches. Replaces the signal-tap script for when an object never emits a signal (for examplePremiumGate).qt-property-write.js-listProperties(qobj)+writeBoolProperty(qobj, idx, value). Reaches Q_PROPERTY setters through Qt’sqt_metacallat vtable slot 2 - same shape as Part 1’sqt-invokable-call.js, just withQMetaObject::Call::WritePropertyinstead ofInvokeMethod.
I’ve also added all the scripts to: github.com/samanL33T/qt-frida-scripts.
Let’s begin.
1. Enable premium feature without a license
Problem. HackPass has premium features (export, plaintext dump, sync) behind a PremiumGate bool. The UI never lets the user toggle it directly - it’s set by LicenseClient after a server-validated key. Our goal is to call setPremium(true) from outside the license workflow.

Solution. PremiumGate::setPremium(bool) is a Q_INVOKABLE slot. Part 1’s callBool() helper from qt-invokable-call.js already knows how to call it - you just need the object pointer and the local method index.
Why client-side and not via network? The other obvious path is intercepting the backend’s policy response and flipping "premium_active": false to true in transit. That works in principle, but HackPass has SSL Certificate pinnning - so it will need the SSL pinning bypass first (We do this in section 2).
Find PremiumGate PremiumGate is a Q_OBJECT like every other instrumented class. Usually we would grab its address from signal-tap script. It won’t work here since signal-tap script requires a signal to be tiggered from the app. Here, the default installation of HackPass has no license and without a license, premium never transitions from false, so premiumChanged method never gets triggered and signal-tap never picks PremiumGate up.
(In other scenarios, where the signal can be triggered from the app - signal-tap script will work as it is.)
Instead we will use qt-find-by-class.js - it scans rw memory for QObject instances whose vtable lives in HackPass.exe’s read-only section, then checks each one’s className. Since sthis is a full memory scan, please expect some slowness.
| |
Run.
| |
After HackPass is up:
[Local::HackPass.exe]-> findByClass('PremiumGate')
[+] scanned N QObjects, found 1 instance(s) of PremiumGate
[0] 0x<addr>
[Local::HackPass.exe]-> walk(ptr('0x<addr>'))
[PremiumGate] M methods (own start at abs K):
...
[K+0] (local 0) premiumChanged
[K+1] (local 1) isPremium
[K+2] (local 2) setPremium
[Local::HackPass.exe]-> callBool(ptr('0x<addr>'), 2, true) // setPremium(true)


One caveat - This is temporary premium. Right after the vault unlocks, HackPass talks to its backend and re-evaluates premium from the server reply. If the server says no, the premium is disabled at the next unlock of the vault. A quick patch is to re-do the flip on each unlock cycle (or schedule a repeating callBool on a Frida setInterval so the value stays true). Try the permanent premium yourself :)
2. Bypass TLS pinning by exposing its own re-learn switch
Problem. Point HackPass at a Burp proxy and the handshake refuses - the app pins the backend’s TLS cert. Signal-tap script shows the network class is PinnedNetworkAccessManager + using procmon shows it persisting fingerprints to HKCU\Software\HackPass\tofu. So this is TOFU pinning: first connection gets the cert hash, every subsequent connection rejects on mismatch.

Solution. findByClass('PinnedNetworkAccessManager') and walk it. The walker shows a Q_INVOKABLE setAllowTofuLearning(bool) accessor - Set this to true and the next handshake against a host with no stored fingerprint pins whatever cert it sees (Burp’s, in our case).
So, in already running HackPass, the TLS bypass requires two steps:
- Delete already setup cert: Delete
HKCU\Software\HackPass\tofu. In wondows, using powershell:Remove-Item HKCU:\Software\HackPass\tofu -Recurse -Force. HackPass forgets the original cert. - In Frida: Turn TOFU learning on against the live
PinnedNetworkAccessManagerand trigger any backend request. Burp’s cert gets pinned.
After clearing the registry entry and pointing HackPass at Burp:
Run.
| |
[Local::HackPass.exe]-> findByClass('PinnedNetworkAccessManager')
[+] scanned N QObjects, found 1 instance(s) of PinnedNetworkAccessManager
[0] 0x<addr>
[Local::HackPass.exe]-> walk(ptr('0x<addr>'))
[PinnedNetworkAccessManager] M methods (own start at abs K):
...
[K+0] (local 2) setAllowTofuLearning
[K+1] (local 3) allowTofuLearning
[Local::HackPass.exe]-> callBool(ptr('0x<addr>'), 2, true)
Now Settings → Test Connection (or any sync action) re-pins to Burp’s cert. From this point on, all backend traffic flows through Burp.

3. Defeat hardening through its own off switch
Problem. HackPass has a hardening subsystem (anti-debug, integrity checks, suspicious-process scan) that fires from several places. It can be enabled from Settings → Hardening toggle on. After enabling, it can be confirmed by running and attaching a debugger - as soon as the debugger is detected, the vault locks and does not accept any password including the real password.

(For Frida/runtime hooking checks, the app only looks for processes like frida-server.exe on windows, which automatically get bypassed since frida uses Frida as process name instead. This allows us to use Frida to hook)
Solution. Find the manager via findByClass, walk it, and look at what’s reachable. The walker shows a Q_PROPERTY named enabled - and Q_PROPERTY writes go through qt_metacall at vtable slot 2 with QMetaObject::Call::WriteProperty (the constant 2). Set enabled to false and the subsystem goes silent. The toggle is the bypass.
Run.
| |
After HackPass is up (toggle Settings → Hardening on in the UI, so that HardeningManager is alive):
[Local::HackPass.exe]-> findByClass('HardeningManager')
[+] scanned N QObjects, found 1 instance(s) of HardeningManager
[0] 0x<addr>
[Local::HackPass.exe]-> listProperties(ptr('0x<addr>'))
[HardeningManager] 1 own properties (use local index with writeBoolProperty):
[0] enabled
[
{
"localIndex": 0,
"name": "enabled"
}
]
[Local::HackPass.exe]-> writeBoolProperty(ptr('0x<addr>'), 0, false)We use qt-property-write.js here.
| |


4. Compose the chain end-to-end
The whole chain of bypasses can be set in one go with qt-hackpass-bypass-chain.js. After enabling Hardening in Settings and clearing the TOFU store (Remove-Item HKCU:\Software\HackPass\tofu -Recurse -Force), load the chain script alongside the other helpers:
| |
In Frida:
[Local::HackPass.exe]-> bypassChain()That disables HardeningManager, flips PremiumGate to premium, and arms TOFU re-learn on PinnedNetworkAccessManager - one command, three bypasses live for the session.

HackPass has plenty more to offer - capturing the master AES key, exploiting the GCM IV reuse, decrypting stored credentials offline, and so on. Some of those don’t need Qt-specific instrumentation (for example, the OpenSSL boundary is reachable from any libcrypto-using app). They’re left as exercises for the reader.
References
Qt6
Q_INVOKABLE- what makesPremiumGate::setPremiumreachable from outsideQNetworkAccessManager- the class HackPass’sPinnedNetworkAccessManagerextendsQSslError- the signalsslErrorscarries when pinning fails