Reversing an Android App's Sign Algorithm: Inside libsgmain.so
About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.
This research is published for educational and defensive purposes. The target sample is anonymized throughout.
Reversing an Android App’s Sign Algorithm: Inside libsgmain.so
At the start of 2020 I was stuck at home, so I pulled out an APK I’d come across and took it apart. I’m no expert — just having fun.
The app’s protocol is plaintext, with a single sign field for verification.
1. The protocol
Long-connection heartbeat:

Response:
{"cod":"0","type":"5","msg":"success","username":"ii12qqaa","vip":"regular member","time":"8888888888","jifen":"0","login":"106","userid":"4232206e2a","token":"9f1ff9900178b5d0fe3db2a37d078ac5201c02380ebb0ba71a3bbf71bb2a8b2c0e18907e0383e8341dc00ee45fbc6923be0313f558197894"}
Login and registration:

Type 1 = login, 2 = register. username and password are the credentials; imei is the device serial; t is the timestamp; sign is the encrypted verification.
Response:
{"cod":"0","type":"2","msg":"registered","username":"yyyyyy1234qq","vip":"regular member","time":"8888888888","jifen":"0","login":"0","userid":"06ef899b13","token":"9f1ff9900178b5d0fe3db2a77951decf2913053b5cbb0ba71a3bbf71bb2a8b2c0e18907e0383e8341dc00ee45fbc6923be0313f558197894"}
Search. Name is the search term:

Tapping a list item sends urn:btih:, a timestamp, and sign to the server:

Response:
{"msg":"0","list":[{"name":"1.rm","data":"83a371d890c1178e8ecc8c060f17d2acf4a0eff707955035be407f7ce707d8ed49d5689a4abbffb8","size":""}]}
Tapping again:

Response:
{"url":"http://[redacted]/video.php?host=[redacted]&code=87d1e08d092c1d05527947754d542095aade3db0cf4619df17e81d10b6e414c4fe101e3622bbcc09f45404c17b601e05d2861bfb4799b740e087e2d94ab886f7&data=83a371d890c1178e8ecc8c060f17d2acf4a0eff707955035be407f7ce707d8ed49d5689a4abbffb81d30aadc7fb85e082658c7","cookie":"b4d29c58","msg":"0"}
That’s the video source. Time to find the sign algorithm.
2. The APK source
In com.Soku.jxActivity’s inner class madaper, onBindViewHolder sends the hash and data to get the video address:

sign is the return value of com.Soku.Sign.getsign, passed hash+time (BuildConfig.VERSION_NAME is "6.3"):

Then it calls suritySignature.sign(), a class in ibaba.wireless.security.jaq.SecuritySignature (SecureSignatureDefine.OPEN_KEY_SIGN_INPUT is "INPUT"):

Next is an interface, IsecureSignatureComponent, implemented in lib/armeabi/d1libsgmain.so — which isn’t actually a .so but a JAR.
I used DDMS to draw the function trace and get the full call chain:
com.alibaba.wireless.security.a.h.a.sign()
-> com.taobao.wireless.security.adapter.a.b.a() // checks the passed-in map type
-> com.taobao.wireless.security.adapter.a.b.a() // overload; converts Unicode bytes to UTF-8
-> com.taobao.wireless.security.adapter.JNICLibrary.doCommandNative(int i, Object... objArr)
Finally it hits the native function. The passed arguments are 10401, new Object{ hash+time+"6.3", "3be780bc-8cf2-446c-9e03-de3a8fd9d0a7", Integer.valueOf(3), "0335" }.
3. Analyzing libsgmain.so
Inside the JAR, lib/armeabi/libsgmain-5.4.56.so. Crudely zero out all the section-header offsets and sizes, then open it in IDA. The .so has heavy control-flow obfuscation — see this writeup for details.
A lot of functions can’t be decompiled. The pattern: at function entry and exit, the code reads a value in memory and compares them; if unequal it jumps to another address. That value isn’t used in the actual computation, so patching it out doesn’t affect analysis and lets IDA happily F5.

Execution flow. First the .so looks up Boolean, String, Integer and makes global references to the methods. Then at 0xb9b0 it decrypts and loads com/taobao/wireless/security/adapter/common/HttpUtil.
The decrypt function is sub_903F0: arg1 is the data to decrypt, arg2 the return buffer, arg3 the size. It first allocates 56 bytes (malloc_56), then 11 bytes, and puts the 11-byte pointer into the start of malloc_56. The 11 bytes hold a 10-byte RC4 key; malloc_56[1] is 0, malloc_56[2] is 11, and the rest are function pointers:

The key is DcO/lcK+h? m3c*q@ (seems to be the same across versions):


Then it allocates another malloc_56, whose first field holds the address of the data to decrypt. A switch in 0x75DDA calls sub_7C700 — the RC4 decrypt. RC4_Initialization sets up the XOR box, RC4_Decrypt decrypts:

sub_7CA82 is RC4_Decrypt:

After several rounds of decrypting and loading other classes, at offset 0xB986 it decrypts and loads com.taobao.wireless.security.adapter.JNICLibrary:

After JNICLibrary loads it doesn’t immediately call RegisterNatives; it keeps decrypting and loading more classes, creating globals. Finally at 0xbdce it registers doCommandNative, with the function at 0xbc38:

doCommandNative’s return value v6 comes from loc_9CD0:

The return value v9 comes from an off function, obtained via sub_9630 decryption. doCommandNative is passed 1, 4, 1, 1, m_off, 0, and the resolved address is 0x29d0d:

m_off + 16 is the objarray passed from Java, m_off + 12 is the env (this method doesn’t end at sub_29F9C, it just jumps there):
void __fastcall 29E6C(int m_off, _DWORD *a2, int a3, int a4, int a5, int a6)
{
v21 = m_off;
v22 = *(_DWORD *)off_ABBA8; // get the value here; fetched again at function end
*a2 = 99;
v23 = *(_DWORD *)(m_off + 16);
(*(void (**)(void))(**(_DWORD **)(m_off + 12) + 692))(); // JNI::GetObjectArrayElement
v24 = *(_DWORD *)(v21 + 12);
sub_9A568(); // process arg[0] (hash+time+6.3)
v25 = *(_DWORD *)(v21 + 16);
sub_9C6E0(*(JNIEnv **)(v21 + 12)); // process arg[1] key
sub_9C7AC(*(_DWORD *)(v21 + 12), *(_DWORD *)(v21 + 16), 2); // process arg[2], call intValue
v26 = *(_DWORD *)(v21 + 16);
sub_9C6E0(*(JNIEnv **)(v21 + 12)); // process arg[3]
sub_29F9C();
}
sub_9A568 mallocs a block of size ArrayLength * 4:

There’s a small bug here:
v8 = ((int (__fastcall *)(JNIEnv *, int, int))(*v4)->GetObjectArrayElement)(v4, v12, v7) == 0;
v9 = 0;
if ( !v8 )
sub_9A138();
*(_DWORD *)(v13 + 4 * v7++) = v9;
if (!v8)? v8 is the object array — on success it just gets thrown away?? So later, loading a value from this malloc gives LDR R0, 0x0000000? This bugged me for days; I thought some later function must be touching this malloc.
The assembly:

BLX R3 is the GetObjectArrayElement call. BEQ branches on equal, so the decompiled C should really be if (v8):

sub_9A138 calls the Java method String.getBytes on the object array just obtained, then reads its ArrayLength and ByteArrayElements. It allocates a 56-byte block (malloc_56) with the same structure as the class-name decrypt, copies ByteArrayElements into malloc_arr, and returns:

sub_9C6E0 (the key handler) is similar: it gets the key’s object array, calls GetStringUTFChars, allocates malloc_32 and malloc_128, stores the malloc_128 pointer in malloc_32[0], the size in malloc_32[2], plus function pointers, then appends the UTF-8 key after malloc_32:


sub_9C7AC calls the Java intValue method to get the return value. There’s another decrypt function, sub_903F0 — running it directly returns 0. The Java-layer arguments are now fully processed.
Next it processes sub_9A568’s return value again, allocating malloc_8:
malloc_8 = (_DWORD *)malloc_0(8);
if ( !malloc_8 )
{
sub_A1308(0, v22, v23, v24, 0, v22);
__asm { LDCL p15, c15, [R8], {0xFF} }
}
*malloc_8 = 0;
malloc_8[1] = 0;
v23 = 4 * number;
*malloc_8 = **(_DWORD **)(hashobj + 4 * number);
malloc_8[1] = *(_DWORD *)(*(_DWORD *)(hashobj + 4 * number) + 4);
*(_DWORD *)(malloc_off + 4 * number) = malloc_8;
malloc_off is the ArrayLength * 4 block allocated by the decrypt function (the ArrayLength in sub_9A568):

Then it calls sub_46708 with the malloc_off pointer and a pointer holding 0x63. sub_46708 calls 0x9db0 three times, discarding all return values — no idea what it does:

0x9db0 is the function that decrypts doCommandNative’s processed arguments. At 0x46BD2 there’s a switch with r1 = 0x4977c:

0x4977c holds a big switch loop — IDA couldn’t “Specify switch idiom” for some reason, so I had to squint through it:

switch_2:

I haven’t found where this data comes from, but re-packaging the APK or changing the key both fail to return a valid sign — so this data is probably the APK’s signature. These four switch layers are a maze:

key_2 isn’t derived from the key by encryption or anything — I suspect it’s tied to the APK’s signature verification.
The final sign algorithm is in case 0x17 of the 0x75dda method, at offset 0x76ff6. Both arguments are malloc_56 structs: the first holds key2, the second holds hash+time+"6.3" (or just time+"6.3" for heartbeat):
int __fastcall Decryptkey(int key2_malloc, int hash)
{
hash_ = hash;
v48 = *off_ABBA8;
v3 = 0;
if ( key2_malloc )
{
if ( hash )
{
key2_size = *(key2_malloc + 4);
if ( key2_size )
{
if ( *(hash_ + 4) )
{
if ( key2_size < 65 )
v5 = (*(key2_malloc + 40))(); // create a malloc_56 struct and copy key2 over
else
sub_78530();
v7 = v5;
m_key2 = 0;
if ( v5 )
{
v9 = *(v5 + 4) == 0;
v10 = 0;
v11 = 0;
v3 = 0;
if ( !v9 )
{
v44 = mirror_key2_1;
key2 = v7;
v45 = v7;
aeabi_memclr4(mirror_key2_1, 64);
aeabi_memclr4(mirror_key2_2, 64);
memcpy8(mirror_key2_1, *key2, key2[1]);
memcpy8(mirror_key2_2, *key2, key2[1]);
i = 0;
do
{
mirror_key2_1[-i] ^= 0x36u;
mirror_key2_2[-i] ^= 0x5Cu;
--i;
}
while ( i != -64 );
sub_97DA4(); // allocate malloc_56_100
m_key2 = malloc_56_100;
(*(malloc_56_100 + 12))(); // copy mirror_key2_1
(*(m_key2 + 48))(m_key2, hash_); // append hash to mirror_key2_1
sub_78530(); // not worth reading into this one
v16 = v15;
v11 = 0;
if ( v15 )
{
v3 = 0;
v7 = v45;
if ( *(v15 + 4) )
{
sub_98594(); // allocate malloc_56 with 0x40 of data, copy mirror_key2_2 over
v18 = v17;
(*(v17 + 48))(); // append hash to mirror_key2_2 in the new malloc_56
sub_78530(); // run it again; the return value is key_3
v11 = v18;
v7 = v45;
v3 = v19;
}
}
else
{
v3 = 0;
v7 = v45;
}
v10 = v16;
}
}
else
{
v10 = 0;
v11 = 0;
v3 = 0;
}
v20 = v10;
free_malloc56(v7, v7, v11, v6, v43, v44, v45, v11);
free_malloc56(m_key2, v21, v22, v23, v24, v25, v26, v27);
free_malloc56(v20, v28, v29, v30, v31, v32, v33, v34);
free_malloc56(v41, v35, v36, v37, v38, v39, v40, v41);
}
}
}
}
result = *off_ABBA8 - v48;
if ( *off_ABBA8 == v48 )
result = v3;
return result;
}
I wanted to reproduce the algorithm, but I got lost tracing it. By then the lockdown was winding down, so I moved on to job hunting.
Anyway, I’ve released the original .so; the signature function’s location is described above for anyone interested.
This function returns a 20-byte (0x14) block — call it key_3 — which then goes through:
do
{
_byte = *(*key_3 - i); // fetch one byte of key_3
v7 = (_byte >> 4) + 87; // loop: get a byte from key_3
if ( _byte >> 5 <= 4 ) // if byte < 0x80: logical-right-shift 4 then OR 0x30
// otherwise: logical-right-shift 4 then add 87 (0x57)
v7 = (_byte >> 4) | 0x30;
offset = 2 * i;
*(*sign - 2 * i) = v7; // put the value into sign (result); i is negative
v9 = _byte & 0xF; // byte & 0xF; if < 0xA OR 0x30, else add 0x5C
// 0x30-0x39 are 0-9, 0x61-0x7A are lowercase letters
if ( v9 < 0xA )
{
v11 = v9 | 0x30;
sign_off = *sign - offset;
}
else
{
sign_off = *sign - offset;
v11 = v9 + 87;
}
*(sign_off + 1) = v11; // put into sign
--i;
key_3 = key_3_;
}
while ( -var_0x14 != i );
That expands key_3 to 0x28 bytes — the final sign.
As for the token: I tried packet-capturing and changing the login protocol’s imei, and re-registering accounts — even switching hotspots returned the same value. I suspect it’s derived from a lower-level device id. That’s beyond me for now.
Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).
Get in touch