Background
I wrote a web version of Slowly using Vue.js last year, which provided some useful functions unsupported, even now, by the official app. It ran well till yesterday when I failed to sign in. At first, I suspected the server might have detected what I had done was illegal and blocked me from further use, since my app can extract accurate locations of friends, show content of a letter even before it has arrived, and send photos without acceptances from a receiver, which, to some extent, may violate their licences.
After some investigations, it turned out, luckily, that they just upgraded the sercurity level of servers to protect information of users. I spent several hours studing their strategies and modifying my code and eventually made the app sail out again.
This post elaborates on the problems I encountered during the process and how I finally solved them.
Charles didn’t help
I created the first version with help of Charles to intercept requests sent from the Slowly app running in my phone with Android 7 installed. In this way, I was able to see the content sealed with https and record all important APIs and hence build my own version. In order to figure out what went wrong I had to check first the requests sent by the latest version of Slowly. However, installation of certificates is forbidden on Android with platform version above 7, so I turned to VirtualXposed, which promises a way to hack https in Android phones running on an OS of any version without root. It didn’t work either.
Before digging into VirtualXposed I tried doing it on Windows instead. Although there were many problmes, like mismatch of architectures, missing of services from google play required by Slowly and my inability to separate a runnable apk from a bundle, I solved them patiently and finally intercepted requests in Charles.
It turned out that besides using a new API, the login request also brought an encrypted parameter of otp
, changing each time, which obviously was generated in client. My next task was to find out how to generate otp
.
otp:qxg6AGYQ4y5rDVaSaiRYjZ3WNwbalO
P.S. Charles did help. A lot!
Generate otp
There was little I could do withought reading the code. So I downloaded the latest apk, unpacked it, and found the source file index.android.bundle
directly under the folder assets
, which turned out to be a large compressed JavaScript file with the size of nearly 3.5 Mb. I copied it to another file named index.js
, opening it with VS Code. The messy code soon filled the screen and made my computer roar heavily.
I extracts a piece of code related to the new api /users/me/v2
, and it’s easy to find otp
is passed from the caller side.
__d(function(g, r, i, a, m, e, d) {
......
e.getMe = function(n) {
var P = n.otp,
y = void 0 === P ? null : P,
S = {
......
otp: y
};
return fetch(t.API_URL + '/users/me/v2?token=' + o, {
method: 'POST',
headers: t.headers,
body: JSON.stringify(S)
}).then(t.handleApiErrors).then(function(n) {
return n.json()
}).then(function(n) {
return n
}).catch(function(n) {
throw n
})
};
}, 962, [1, 924, 598]);
Code blocks referencing getMe
are all surrounded with a large piece of messed code, which makes it difficult to figure out what’s going on. Instead, I searched otp
and found the core logic to generate otp
:
__d(function(g, r, i, a, m, e, d) {
......
var t = r(d[0]);
var o = t(r(d[2]))
e.genOTP = function(t) {
var n = t.timestamp,
c = t.uid;
return new o.default(
'+DP;=SW`DGX&n|]OGoGkj/4XqPw?^Fclc2F-_V~D=rquG+L(kW_xzVR=slp+Yj;B',
30
).encode(parseInt(n), parseInt(c), Math.floor(1e5 + 9e5 * Math.random()))
};
......
}, 923, [1, 924, 925]);
genOTP
accepts an object containing two keys, one is timestam
, another one is uid
.
Before going further, the structure of a module needs to be explainded a little. Each module is defined in a factory function passed along with another two parameters——an integer and an array——as arguments to a function _d
. The second parameter, i.e. the integer, indicates the index of the module, and the third parameter is an array of index of module it depends on.
__d(function(g,r,i,a,m,e,d){
......
},923,[1,924,925]);
Each factory function accepts 7 parameters, the most important three ones are:
- r: require,
r(12)
means to import a module with index 12 - e: exports,
e.getMe = function
means to export a functiongetMe
from this module - d: array of dependencies,
r(d[1])
means to import a module whose index isd[1]
In this case, d
is [1, 924, 925]
, then t = r(d[0])
equals t = r(1)
and o = t(r(d[2]))
equals o = r(1)(r(925))
, hence the code above can be transformed into something below:
var otp = new (__r(1)(__r(925)).default)(
'+DP;=SW`DGX&n|]OGoGkj/4XqPw?^Fclc2F-_V~D=rquG+L(kW_xzVR=slp+Yj;B',
30
).encode(
parseInt(Date.now()),
parseInt(0),
Math.floor(1e5 + 9e5 * Math.random())
)
console.log(otp)
Put this piece of code at the bottom of index.js
and run node index.js
. After supressing some(a lot of) errors related to ReactNative, we will get an output, pretty much looking like the one capured in a previous request:
j4P89Am5g96GPkKwuYiZ5xE3wgbLYO
Now, let’s look at the detail of module 1:
__d(function(g, r, i, a, m, e, d) {
m.exports = function(n) {
return n && n.__esModule ? n : {
default: n
}
}
}, 1, []);
Since module 1 does nothing except returning the input function back, the invocation of module 1 can be eliminated. Next, we will see what module 925 looks like:
__d(function(g, r, i, a, m, e, d) {
!(function(t, s) {
if ("function" == typeof define && define.amd) define(["exports"], s);
else if (void 0 !== e) s(e);
else {
var h = {};
s(h), t.Hashids = h
}
})(this, function(t) {
......
var h = (function() {
......
})();
t.default = h
})
}, 925, [])
Module 925 is an immediate function which at last export function h
as default
property of e
, and it can be simplified to:
__d(function(g, r, i, a, m, e, d) {
var h = (function() {
// a large piece code
......
})();
e.default = h
}, 925, [])
What is still unkown to us is how to get timestamp
and uid
for genOTP
. After investigating the code again I sifted the most important logic related to process of sending request of getMe
:
__d(function(g, r, i, a, m, e, d) {
......
var p = r(d[7]),
E = r(d[10]),
......
function X() {
var t, n, s, o, x, _, S, y, h, R, T, b, O, w;
return c.default.wrap(function(c) {
for (;;) switch (c.prev = c.next) {
......
case 11:
return S = c.sent, c.prev = 12, c.next = 15, (0, u.call)(p.getTime);
case 15:
return y = c.sent, c.next = 18, (0, u.select)(E.getMyID);
case 18:
return h = c.sent, c.next = 21, (0, u.call)(p.genOTP, {
timestamp: parseInt(y.now),
uid: parseInt(h)
});
case 21:
return R = c.sent, c.next = 24, (0, u.call)(l.getMe, {
token: n,
location_code: s,
location: o,
device: S.device,
otp: R
});
......
}
}, C, null, [
[12, 45]
])
}
}, 969, [383, 1, 46, 136, 623, 948, 962, 923, 968, 952, 963, 633, 2, 634, 636, 738, 920, 353])
This code block shows that we can get uid
through E.getMyID
, where E
is imported from module 923 and get timestamp
by calling p.getTime
, where p
implies module 963, same one as genOTP
.
It’s time to uncover the real face of getTime
and getMyID
:
__d(function(g, r, i, a, m, e, d) {
......
e.getTime = function() {
return fetch(n.API_URL + '/timestamp', {
timeout: 2e3
}).then(n.handleApiErrors).then(function(t) {
return t.json()
}).then(function(t) {
return t
}).catch(function(t) {
throw t
})
}
}, 962, [1, 924, 598]);
__d(function(g, r, i, a, m, e, d) {
......
var _ = function(t) {
return t.me.id ? t.me.id : 0
};
e.getMyID = _;
}, 963, [1, 18, 46, 964, 356, 919, 965]);
Since we don’t have any information before signing in, thus uid
can always be set to 0
.
Combining all the code and optimizations metioned above together, we get to a final, feasible piece of code:
const Encryption = (function() {
......
})()
export default function getOtp() {
getTimestamp().then((time) => {
return new Encryption(
"+DP;=SW`DGX&n|]OGoGkj/4XqPw?^Fclc2F-_V~D=rquG+L(kW_xzVR=slp+Yj;B",
30
).encode(
parseInt(time),
parseInt(0),
Math.floor(1e5 + 9e5 * Math.random())
);
})
}