WeChat RQTX Part 2: From VM Runtime Trace to Semantic Algorithm Recovery
WeChat RQTX Part 2: From VM Runtime Trace to Semantic Algorithm Recovery
RQTX sits in a WeChat request-chain signing and checking path. Its input comes from request-related data, and the visible output is a short integer. This second article focuses on what happens after the VM runtime becomes observable: how a long virtual-machine execution stream can be reduced into an algorithm that is readable, testable, and maintainable.
I prefer to call this step “from runtime to semantics.” At the beginning, the observable objects are VM input, output, registers, memory, and many blocks. At the end, the goal is to name ordinary engineering functions:
message_schedule()compress_sha_like()tail_transform()rolling133_to_0x44()When a register replay like r8/r9/ra/rb becomes algorithm objects such as state, block, digest, and finalizer, RQTX has effectively walked out of the VM.
The article first gives a page-level verification vector, then extracts a standard-library-like algorithm skeleton. After that, it explains how trace localizes the algorithm and where the result resembles or diverges from MD5, SHA-2, HMAC, CRC, and rolling-hash families.
Key Conclusions
- The input boundary for this path is not the raw request body. It is a normalized 32-byte lowercase MD5 hex string.
- VM trace should not be read as one long line. It should be aggregated into memory boundaries, dynamic blocks, context-slot diffs, and block-local instruction windows.
- The main algorithm is not CRC, standard MD5, standard SHA-256, or standard HMAC. It borrows recognizable structures and then applies profile-specific changes.
- Semantic recovery depends on naming blocks by behavior, validating them against standard algorithm shapes, and closing the loop with both a VM oracle and primitive tests.
- The VM runtime should not be discarded after the semantic implementation works. It remains useful as a trace oracle, regression oracle, and version-migration oracle.
The article moves through five layers: a refresh-generated vector, a publishable algorithm skeleton, trace localization, standard-algorithm comparison, and a verification loop.
Refresh-generated Verification Vector
The vector below is generated by an edge function when the page loads. Each refresh produces a new 32-byte ASCII hex input and the corresponding 0x44-prefixed RQTX result. It is meant for reverse comparison with local hooks, runtime trace, or a semantic implementation.
- input_kind
- md5_hex_32
- cmd
- -
- input
- loading
- rqtx
- -
- generated_at
- -
Local Reverse Validation With Frida
The page does not accept custom reader input. Instead, it publishes a fresh vector on each refresh. A reader can hook the relevant local boundary, replace the live input with the generated vector, and compare the device result with the page result.
For a jailbroken Frida flow, the validation shape is:
// Replace symbolic names with locally verified symbols.const target = Module.findExportByName(null, "rqtx_entry_like");Interceptor.attach(target, { onEnter(args) { const vector = "replace_with_page_generated_input"; Memory.writeUtf8String(args[0], vector); args[1] = ptr(vector.length); }, onLeave(retval) { console.log("device rqtx =", retval); },});For a repackaged Frida flow, inject the same boundary replacement into the app process and compare the result after the request path reaches RQTX. Keep the probe narrow: modify only the normalized input boundary and log only the short result. Avoid collecting raw request bodies or unrelated user data.
First, Shape The Algorithm Like A Standard Library
Before reading every trace line, it helps to define the kind of code we want to end with. The publishable semantic implementation should look like a normal small algorithm library, not like VM register replay.
A useful architecture is:
pub struct RqtxInput<'a> { pub md5_hex_32: &'a [u8; 32], pub cmd: u32,}
pub struct RqtxProfile { pub iv: [u32; 8], pub round_constants: &'static [u32; 64], pub finalizer_seed: u32,}
pub fn rqtx_870(input: &RqtxInput, profile: &RqtxProfile) -> u16 { let block = stage_md5_hex(input.md5_hex_32, input.cmd); let words = message_schedule(&block); let digest = compress_sha_like(profile.iv, &words, profile.round_constants); let tail = tail_transform(&digest, input.cmd); rolling133_to_0x44(&tail, profile.finalizer_seed)}This shape is intentionally close to standard cryptographic and checksum libraries. It separates input staging, message schedule, compression-like mixing, tail transformation, and final short-integer reduction.
That does not mean RQTX is SHA-256, HMAC, MD5, or CRC. It means the comparison target is clear. Once the semantic code has this shape, the analysis can say precisely which pieces resemble a standard primitive and which pieces are profile-specific.
Start By Accepting The VM As The Oracle
At the beginning of semantic recovery, the VM runtime is the only trustworthy oracle. The trace is noisy, the pseudo-code is distorted by CFG flattening, and a hand-written semantic implementation is not yet proven.
The first rule is simple: the semantic implementation is wrong until it matches the VM oracle for many inputs.
The oracle gives three kinds of evidence:
- input staging: where the normalized 32-byte input is placed;
- state evolution: how context fields and memory windows change across blocks;
- output write: where the short RQTX value is produced.
This prevents a common mistake: seeing SHA-like operations in the runner and immediately declaring the whole algorithm to be SHA-256. The VM oracle forces every abstraction to earn its name.
Trace Should Be Aggregated, Not Hoarded
More trace is not automatically better. Raw instruction trace becomes unreadable quickly. The useful trace is compressed into layers:
| Trace View | What It Answers |
|---|---|
| memory boundary trace | Where input is staged and output is written. |
| dynamic block trace | Which blocks form loops, rounds, and tails. |
| context slot diff | Which slots behave like state, block words, counters, or digest. |
| block-local instruction window | Which operations resemble known algorithm steps. |
The goal is to replace “thousands of native instructions” with “a small number of behavior-labeled blocks.” A block name should describe what the block does, not where it lives.
How Trace Localizes The Algorithm
Step 1: Lock The Wrapper Boundary
Start from the known request-chain entry and stop at the wrapper that receives the normalized RQTX input. This boundary keeps request normalization separate from the protected algorithm.
The wrapper boundary is proven when:
- repeated calls hit the same path;
- the normalized input appears at a stable memory boundary;
- replacing only that staged input changes the final RQTX result;
- unrelated request fields do not need to be dumped to reproduce the algorithm input.
Step 2: Use Boundary Memory Trace For Input Staging And Output Write
Memory boundary trace identifies where the 32-byte hex string enters VM-visible memory and where the short integer is written back.
This is the first semantic cut:
request material -> normalized md5_hex_32 -> VM staging buffer -> VM rounds -> short rqtx resultThe staging boundary tells us that the raw request body is not the algorithm input. The output boundary tells us which value the semantic implementation must reproduce.
Step 3: Use Dynamic Block Trace To Find Loop Skeletons
After staging, the VM runs repeated block groups. The important question is not the native block address. The important question is whether the block group behaves like a schedule loop, a compression round, a tail transformation, or a final reducer.
Dynamic block trace gives this shape:
stage_input -> prepare_words -> schedule_loop -> compression_round_loop -> tail_mix -> rolling_finalizer -> write_outputThis skeleton is the bridge between VM runtime and semantic algorithm.
Step 4: Use Context Slot Diffs To Name State, Block, And Digest
Context slot diffs turn blocks into objects. A slot that is written once after input staging is likely part of block. A group of eight 32-bit values updated across rounds behaves like state. A derived group that survives into the final reducer behaves like digest.
This naming step should be conservative. If a slot looks like a counter in one trace but behaves like a pointer in another, it should remain unnamed until the conflict is explained.
Step 5: Compare Block-local Windows With Standard Algorithms
Only after the block skeleton and context names are stable should standard algorithms enter the discussion. The comparison should be structural:
- Does the schedule expand 16 words into a larger word array?
- Does the round loop maintain eight working variables?
- Are rotate, shift, choose, majority, xor, or modular-add patterns present?
- Are constants used like SHA-2 round constants or profile-specific tables?
- Does the finalizer reduce a digest into a short result like a checksum or rolling hash?
This is where “similar to SHA-2” becomes a useful clue, and “not standard SHA-256” becomes a precise conclusion.
The First Main Line Seen In Runtime
The first stable line in runtime is input staging. The visible input is a lowercase 32-byte MD5 hex string. That string is not the final algorithm. It is a normalized boundary.
The VM then transforms it through a sequence that looks like:
md5_hex_32 -> profile-aware block staging -> message schedule -> SHA-like compression -> MD5-like or HMAC-like tail influence -> rolling reducer -> 0x44-prefixed short valueThis line explains why isolated observations can be misleading. Seeing MD5 at the input boundary does not make the core MD5. Seeing SHA-like rounds does not make it standard SHA-256. Seeing a short final result does not make it CRC.
Semantic Part 1: MD5 Hex Is Input Normalization, Not The Main Algorithm
The 32-byte lowercase MD5 hex string is a boundary artifact. It is how request-related material is normalized before it enters the protected path.
This matters because MD5 can appear in the trace and still not be the algorithm being recovered. The semantic code should represent this as staging:
fn stage_md5_hex(input: &[u8; 32], cmd: u32) -> ProfileBlock { // Normalize and place bytes into the profile-specific block layout. ProfileBlock::from_md5_hex_and_cmd(input, cmd)}The proof is behavioral: replacing the staged 32-byte value changes the final RQTX result in a controlled way, while the rest of the algorithm still executes through the VM path.
Semantic Part 2: It Looks Like SHA-2, But It Is Not SHA-256
The strongest structural similarity is SHA-2-like compression:
- words are expanded from an initial block;
- several 32-bit working values move through repeated rounds;
- rotate, shift, xor, choose-like, majority-like, and modular-add patterns appear;
- constants influence round behavior.
The differences are just as important:
- the input block is profile-staged, not a standard SHA-256 padded message block;
- constants and IV-like values are profile-specific;
- tail processing does not follow standard SHA-256 digest finalization;
- the output is reduced into a short RQTX value instead of a 256-bit digest.
So the correct description is SHA-2-like, not SHA-256.
Why The SHA-2-like Label Is Defensible
The label is defensible because it comes from multiple signals, not from one operation:
| Signal | Why It Matters |
|---|---|
| schedule expansion | Matches the idea of deriving later words from earlier words. |
| eight-lane state | Resembles a SHA-2 compression state shape. |
| rotate/shift/xor mix | Matches SHA-2-style bit mixing. |
| repeated round loop | Separates schedule, compression, and tail behavior. |
| profile constants | Explains why it is similar but not standard. |
The label would be weak if it came only from a rotate instruction or a single constant. It becomes useful when the whole block behavior aligns.
Semantic Part 3: It Looks Like HMAC, But It Is Not HMAC
HMAC-like signals can appear when the trace shows staged material, profile-derived constants, and inner/outer-looking influence. But standard HMAC has a clear construction: key normalization, ipad/opad, inner hash, and outer hash.
RQTX does not cleanly follow that construction. The better interpretation is that it borrows the idea of profile-conditioned mixing, not the exact HMAC algorithm.
In semantic code, that difference should be visible:
fn tail_transform(digest: &[u32; 8], cmd: u32) -> TailState { // Profile-specific tail mixing, not HMAC ipad/opad finalization. TailState::mix(digest, cmd)}Semantic Part 4: It Looks Like MD5, But It Is Not MD5
MD5 appears at the boundary and can also appear as a comparison shape because the input is a 32-byte hex MD5 string. But the algorithm’s core loop does not behave like standard MD5 compression.
The important distinction is:
- MD5 as a normalization source: yes;
- MD5 as the recovered VM core: no;
- MD5-like byte ordering or digest influence: possible, but must be tied to specific blocks.
This prevents the analysis from collapsing all “MD5-looking” artifacts into one incorrect conclusion.
Semantic Part 5: The Final Step Is Not CRC, But A Rolling Mixer
The final reducer is tempting to call CRC because it produces a short result. The trace does not support that conclusion.
A CRC-like reducer would normally show polynomial-style shifts and table or bit-level feedback. The observed finalizer is better modeled as a rolling mixer:
fn rolling133_to_0x44(tail: &TailState, seed: u32) -> u16 { let mut acc = seed; for b in tail.bytes() { acc = acc.wrapping_mul(133).wrapping_add(u32::from(b)); acc ^= acc >> 11; } 0x4400 | ((acc as u16) & 0x00ff)}The exact constants belong to the profile under analysis. The important public point is the shape: rolling accumulation and short reduction, not CRC polynomial computation.
From Register Replay To Semantic Code
Register replay is useful early because it stays close to the VM. It is bad as the final article-level abstraction because it hides meaning.
The migration path is:
r8/r9/ra/rb snapshots -> context slot names -> state/block/digest/tail objects -> standard-library-like functions -> tests against VM oracleA good semantic implementation should be boring to read. It should look like ordinary code with small functions and explicit profiles. The reverse-engineering work remains in the evidence chain and tests, not in exotic naming.
Standard Algorithm Comparison
“Similar” is a lead. “Different” is the result that matters.
| Standard Shape | RQTX Relationship |
|---|---|
| MD5 | Used as input normalization boundary; not the main VM core. |
| SHA-256 / SHA-2 | Strong structural resemblance in schedule and compression; not a standard digest implementation. |
| HMAC | Profile-conditioned tail influence resembles keyed mixing; not standard HMAC construction. |
| CRC | Short output is superficially similar; final reducer behaves more like a rolling mixer. |
This comparison is valuable because it lets the article say exactly what was borrowed and exactly what was changed.
Verification Loop
The verification loop has four layers:
- Page vector: edge-generated input/result pair.
- Device hook: replace the normalized input boundary and observe device output.
- VM oracle: run the recovered VM runtime against the same input.
- Semantic implementation: run the standard-library-like code against the same input.
The implementation is accepted only when all four agree over many generated vectors. A single match can be luck. A thousand random matches against the VM and semantic implementation provide much stronger confidence.
The Role Of AI In This Process
AI is useful as a naming, summarization, and comparison assistant. It can help turn trace groups into candidate names, compare a block skeleton against standard algorithm families, and point out missing verification steps.
AI should not be treated as the oracle. The oracle is still the VM runtime, the device result, and the tests. A suggested function name is useful only after trace behavior proves it.
Common Pitfalls
- Calling the input MD5 because the boundary contains MD5 hex.
- Calling the core SHA-256 because the rounds look SHA-like.
- Calling the final reducer CRC because the output is short.
- Trusting one trace without checking input variation.
- Keeping register names in the final semantic layer.
- Throwing away the VM runtime after the semantic code starts passing.
Each pitfall comes from naming too early. The cure is to keep the evidence chain visible.
Combining The Two Stages
The first article recovers the VM architecture: entry, rqtx.dat, context, instruction decoding, runner, and offline runtime. This article uses that runtime to recover semantic algorithm structure.
Together they form one chain:
entry proof -> VM image proof -> context proof -> runner proof -> runtime oracle -> trace aggregation -> semantic algorithm -> standard comparison -> verification vectorsThis is why the runtime matters even after semantic lifting. It remains the bridge between protected execution and readable code.
Final Notes
The most useful result is not a dramatic name for the algorithm. The useful result is a maintainable model: normalized input, profile-specific staging, SHA-2-like compression, non-standard tail mixing, rolling finalization, and tests that compare semantic output with VM output.
That model is precise enough to publish, precise enough to test, and flexible enough to survive version migration.
FAQ
Is RQTX standard SHA-256?
No. It has SHA-2-like structure, but the input staging, constants, tail behavior, and short output do not match standard SHA-256.
Is RQTX standard HMAC?
No. Some profile-conditioned mixing can look HMAC-like, but the recovered path does not follow the standard HMAC construction.
Is the 32-byte input the raw request body?
No. The observed input boundary is a normalized lowercase MD5 hex value derived from request-related material.
Why keep the VM runtime after semantic recovery?
The runtime remains the oracle for regression, trace, and version migration. Semantic code is easier to read, but the VM proves whether it is still correct.
How should readers verify the result?
Use the generated page vector, hook the normalized local input boundary, compare the device result, then compare the VM runtime and semantic implementation against the same vector.
Share Article
If this article helped you, please share it with others!
Comments
Stay anonymous and keep the discussion technical. Nickname and website are optional.