- What: Discussion on instrumenting Qt6 desktop apps with Frida
- Impact: Developers and security researchers working with Qt6 applications
Tooling · frida · 2026-05-17 Runtime Instrumentation of Qt6 Apps with Frida - Part 2: Building the Bypass Chain Contents 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 example PremiumGate ). qt-property-write.js - listProperties(qobj) + writeBoolProperty(qobj, idx, value) . Reaches Q_PROPERTY setters through Qt’s qt_metacall at vtable slot 2 - same shape as Part 1’s qt-invokable-call.js , just with QMetaObject::Call::WriteProperty instead of InvokeMethod . 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. javascript ⧉ copy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // qt-find-by-class.js (excerpt) function looksLikeHpVtable ( vtable ) { if ( ! inAny ( vtable , hpReadOnly )) return false ; for ( let i = 0 ; i < 6 ; i ++ ) { const slot = vtable . add ( i * 8 ). readPointer (); if ( ! inAny ( slot , hpExec )) return false ; } return true ; } globalThis . findByClass = function ( className ) { const matches = [], seen = new Set (); Process . enumerateRanges ({ protection : 'rw-' , coalesce : true }). forEach ( r => { if ( r . size > 0x4000000 ) return ; const end = r . base . add ( r . size ). sub ( 8 ); let p = r . base ; while ( p . compare ( end ) < 0 ) { const vtable = p . readPointer (); if ( looksLikeHpVtable ( vtable ) && classNameOf ( p ) === className && ! seen . has ( p . toString ())) { seen . add ( p . toString ()); matches . push ( p ); } p = p . add ( 8 ); } }); return matches ; }; Run. bash ⧉ copy 1 frida -l qt-signal-tap.js -l qt-metaobject-walker.js -l qt-invokable-call.js -l qt-find-by-class.js HackPass.exe After HackPass is up: text ⧉ copy [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_...