WeChat RQTX Part 2: From VM Runtime Trace to Semantic Algorithm Recovery

2689 words
13 minutes

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.

RQTX Challenge generating
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 ViewWhat It Answers
memory boundary traceWhere input is staged and output is written.
dynamic block traceWhich blocks form loops, rounds, and tails.
context slot diffWhich slots behave like state, block words, counters, or digest.
block-local instruction windowWhich 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 result

The 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_output

This 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 value

This 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:

SignalWhy It Matters
schedule expansionMatches the idea of deriving later words from earlier words.
eight-lane stateResembles a SHA-2 compression state shape.
rotate/shift/xor mixMatches SHA-2-style bit mixing.
repeated round loopSeparates schedule, compression, and tail behavior.
profile constantsExplains 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 oracle

A 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 ShapeRQTX Relationship
MD5Used as input normalization boundary; not the main VM core.
SHA-256 / SHA-2Strong structural resemblance in schedule and compression; not a standard digest implementation.
HMACProfile-conditioned tail influence resembles keyed mixing; not standard HMAC construction.
CRCShort 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:

  1. Page vector: edge-generated input/result pair.
  2. Device hook: replace the normalized input boundary and observe device output.
  3. VM oracle: run the recovered VM runtime against the same input.
  4. 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 vectors

This 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!

WeChat RQTX Part 2: From VM Runtime Trace to Semantic Algorithm Recovery
https://taskagent.one/en/posts/wechat-rqtx-vm-runtime-to-semantic-hash/
Author
TaskAgent Reverse Lab
Published at
2026-05-09
License
CC BY-NC-SA 4.0

Comments

Anonymous Discussion

Stay anonymous and keep the discussion technical. Nickname and website are optional.

Loading
Loading comments
0/1200
Profile Image of the Author
TaskAgent Reverse Lab
AI-agent-driven, expert-guided research on protected algorithms.
Research Boundaries
This site records reverse engineering, security validation, and engineering reviews in authorized environments only. It does not publish bypass, abuse, or unauthorized attack material.
Categories
Tags
Site Statistics
Posts
3
Categories
1
Tags
24
Total Words
44,956
Visits
Loading...
Running Days
0 days
Last Activity
0 days ago

Table of Contents