X (former Twitter) Login flow reconstruction. Episode 2 — UI Metrics
This is my second episode about the login X / Twitter flow and all the needed generation required in order to create a requests based script. If you missed…

This is my second episode about the login X / Twitter flow and all the needed generation required in order to create a requests based script. If you missed the previous episode, be sure to check it out
Check this episode GitHub repo with full study and its Node/Go/Python implementations
Today goal is to correctly understand and solve the ui_metrics challenge loaded from the script https://twitter.com/i/js_inst?c_name=ui_metrics

Understanding it
During an “username check” request ( on the endpoint https://api.x.com/1.1/onboarding/task.json?flow_name=login ) you'll get as response subtask_id LoginJSInstrumentationSubtask

This mean that the twitter sdk is going to load the js script from https://twitter.com/i/js_inst?c_name=ui_metrics
A dynamic JS script that will return a long dynamic JS obfuscated script that looks like

By taking a look into this mess, we can already identify some key informations:
- Variables used 6x hex chars (SHA256 like approach)
- A return object with rf (computed values) and s (a static signature/token)
- The script creates an hidden input fields named ui_metrics with the JSON result
The main calculation function uses some bitwise operations on 4 numeric variables and then return the final result. But before proceeding we need to understand 2 “complex” obfuscation patterns.
1. Identifying the obfuscation patterns
Prototype XOR
A pretty clean pattern that uses Javascript prototype chain in order to perform XOR operations in an “obfuscated” way
a6119e2f973a1d...4ca6d3d91310d0927 = function(POgAM, oFHVk, TvMAz) {
function UkRmJ(yBLdW) {
this.TWByd = function() {
return this.mUxbW ^ yBLdW;
}
}
var vjVer = {
mUxbW: TvMAz
};
var aVDMH = new UkRmJ(POgAM);
aVDMH.mUxbW = oFHVk;
UkRmJ.prototype = vjVer;
return aVDMH.TWByd() | (new UkRmJ(oFHVk)).TWByd();
}(arg1, arg2, arg3);
Let’s break it down:
- A constructor UkRmJ is created that stores a XOR method
- An object vjVer is created with mUxbW set to the third argument
- An instance is created, then its mUxbW is overwritten with the second argument
- The prototype is changed AFTER creating the first instance
- First call: this.mUxbW comes from instance property (arg2), XORed with arg1
- Second call: this.mUxbW comes from prototype (arg3), XORed with arg2
That can be easly converted into
arg2 ^ arg1 | arg3 ^ arg2
DOM Tree Calculation
Now we get creative. Using an actual DOM a calculation is made
a4952d9090df64c4e...1e4c7a23d4f827 = function(Bcgrg, UZGVA, iKpwN) {
var aRkCd = document.createElement('div');
aRkCd.setAttribute('style', 'display:none;');
document.getElementsByTagName('body')[0].appendChild(aRkCd);
function QvFOY(jIbtN, YvKjZ) {
for (var i = 0; i < 8; i++) {
var RFTrj = document.createElement('div');
jIbtN.appendChild(RFTrj);
RFTrj.innerText = YvKjZ;
if ((YvKjZ & 1) == 0)
jIbtN = RFTrj;
YvKjZ = YvKjZ >> 1;
}
return jIbtN;
}
function bUZeJ(RFTrj, aRkCd, YvKjZ) {
if (!RFTrj || RFTrj == aRkCd)
return YvKjZ % 256;
while (RFTrj.children.length > 0)
RFTrj.removeChild(RFTrj.lastElementChild);
return bUZeJ(RFTrj.parentNode, aRkCd, YvKjZ + parseInt(RFTrj.innerText));
}
var YvKjZ = bUZeJ(QvFOY(QvFOY(QvFOY(aRkCd, Bcgrg), UZGVA), iKpwN), aRkCd, 0);
aRkCd.parentNode.removeChild(aRkCd);
return YvKjZ;
}(val1, val2, val3);
The logic of it is:
- Creates an hidden div and appends to the body
- QvFOY builds a tree structure based on the binary representation of the input value - for each of the 8 bits, it creates a child div and conditionally changes the "current parent" based on whether the bit is 0
- This is called three times in a chain to build nested tree structures for all three values
- bUZeJ recursively traverses up the tree, summing up innerText values and returning the result mod 256
- The hidden div is cleaned up after calculation
Basically a computation “hidden” as a DOM manipulation, probably made in order to block headless browsers.
Date XOR
There’s also a third, simpler pattern. It looks like this
value = value ^ new Date(value * 10000000000).getUTCDate();
This one just XORs the variable with the UTC day of the month derived from the value itself. Pretty straight forward, and it means the output is time-independent since the timestamp is derived from the value, not the current date.
2. The Deobfuscation
Now that we understand what the obfuscation patterns actually do, we can skip the browser entirely and reimplement the logic in any language. Let’s proceed.
The approach is simple: parse the script line by line using regex, detect which pattern we’re dealing with, and execute it natively.
For the Prototype XOR we already know the formula, so whenever we detect a function IIFE containing "prototype", we extract the three arguments and compute the result directly.
For the DOM Tree we detect it by looking for "createElement" in the function body. Then instead of actually building DOM nodes, we simulate the tree with a simple array
def dom_tree_calc(val1, val2, val3):
nodes = [{'innerText': 0, 'parentIndex': -1}]
def build_tree(parent_idx, value):
current_parent_idx = parent_idx
for _ in range(8):
new_node_idx = len(nodes)
nodes.append({'innerText': value, 'parentIndex': current_parent_idx})
if (value & 1) == 0:
current_parent_idx = new_node_idx
value = value >> 1
return current_parent_idx
def traverse_and_sum(node_idx, root_idx, total):
if node_idx == -1 or node_idx == root_idx:
return js_mod(total, 256)
node = nodes[node_idx]
return traverse_and_sum(node['parentIndex'], root_idx, total + node['innerText'])
d1 = build_tree(0, val1)
d2 = build_tree(d1, val2)
d3 = build_tree(d2, val3)
return traverse_and_sum(d3, 0, 0)
Each node is just a dict with innerText and parentIndex. build_tree walks the 8 bits of the value and either nests deeper (even bit) or stays at the same level (odd bit). traverse_and_sum walks back up summing the values.
The annoying Javascript logic
There is one critical thing worth mentioning. Notice the js_mod call instead of a plain % 256. In Python, the modulo operator always returns a non-negative result for a positive divisor
# Python
-208 % 256 # returns 48
While in javascript, the sign of the dividend is preserved
// JavaScript
-208 % 256 // returns -208
The fix is a small helper
def js_mod(a, b):
if a >= 0:
return a % b
return -((-a) % b) if (-a) % b != 0 else 0
3. The solver
Putting it all together, the solver works in 4 steps:
- Extract the 4 initial variable values using a regex on var declarations
- Extract the static s string from the return statement
- Walk the function body line by line, detecting each operation type
- Return the final variable values as the rf object, paired with the s string
So, by understanding the operation type via this table
| Pattern Detected | Action |
|---------------------------|-------------------------------------------|
| IIFE with `createElement` | `dom_tree_calc(arg1, arg2, arg3)` |
| IIFE with `prototype` | `(arg2 ^ arg1) \| (arg3 ^ arg2)` |
| Assignment with `new Date`| `value ^ getUTCDate(value * 10000000000)` |
| Simple assignment | Eval bitwise exp (`^`, `&`, `\|`, `~`) |
We can straight-forward solve this challenge with just regex and arithmetic. By having at the end an output dict like
{
"rf": {
"a426be92eaca1e3378ae3....c72d05ac7757212fc": 22,
"b068e1e02c6e975194343...bcd335f40257cab": 207,
"da6a57a0e5a772c8f414e...4f082ec3ef525f25d23d1c2": -195,
"ae09b38c692fe2ba158f6...8cc7413f58b5fdf886b2ca5c": 20
},
"s": "eJKvRt3Dg_63RqBBrX_w9EfBvNuG8b4O..."
}
That you’ll need to send back to twitter
What’s next?
We have terminated the ui_metrics challenge, be sure to check the full repo on Github where you can find the solvers for both Python NodeJS and Golang. Be sure to star it.
Next step is proceeding with integrating everything into the complete login flow alongside the Castle Token generation. Be sure to subscribe to my newsletter in order not to miss it, a full login script request based will be posted.
Connect with me
If you enjoyed this article, follow me on GitHub and on Medium to receive notifications whenever I post or open-source something, or buy me a coffee to keep me up.
Skip the reverse-engineering.
Takion returns fresh cookies, headers, and tokens for every major antibot wall. One POST, no browser, first call within the hour.