Speed up home broadband with Tianyi Home Cloud, and decompile Tianyi Home Cloud APK to obtain the Signature algorithm
August 3, 2019, this method is no longer working as the interface has been closed by China Telecom
This article is a record of my tinkering process. Recently, I discovered the Tianyi Home Cloud app launched by China Telecom on my iPhone, which has a speed-up feature that can boost the broadband speed to 500Mbps downstream and 50Mbps upstream. I wondered if it could be used to continuously accelerate other applications. I found that it was possible, but the methods mentioned online only mentioned two parameters, Signature and SessionKey. However, in my tests, I found that the Date parameter was also required, and it seemed that the Signature parameter was calculated based on the Date.(English version Translated by GPT-3.5, 返回中文)
Screenshots (original broadband speed 200Mbps / 30Mbps)
Download speed
Note: Download speed measured by downloading a game produced by Snail Games.
Upload speed
Note: The upload speed is the result of downloading resources from a server in another city (China Telecom’s broadband provides a public IP address, so I exposed my personal server’s resources to the public network). The download speed represents the upload speed of my home server’s broadband. As the resources are private, I have blurred out any personal content (including IP address, download link, specific size, etc.) in the screenshot.
Reference
Speed up China Telecom broadband using Tianyi Home Cloud
Preparation
- Prepare Charles (a network traffic analyzer tool) Download Charles Web Debugging Proxy HTTP Monitor / HTTP Proxy / HTTPS
- Dex2jar, an Android decompilation tool Download dex2jar SourceForge
- Jd-GUI Download Jd-GUI benow.ca Download Jd-GUI Github
- Tianyi Home Cloud Android version Download Tianyi Home Cloud Android 189.cn (Invalid link) Download Tianyi Home Cloud Android 7.3.0 (Invalid link)
Following the original article
First, following the steps in the original article, I went through the process (starting from before StartQos) without further explanation in this article.
From Charles, I obtained these parameters (I noticed that the request returned a 400 code, but the speed had actually increased, so I assumed that 400 was a normal code)
I saw the SessionKey and Signature mentioned in the article and immediately used Postman to simulate the request. I got a response like this (even though the body is empty, I didn’t fill in the required parameters such as prodCode and clientType for now)
I found that the response was not what I expected, and it seemed that the Date parameter was also required. After adding the Date parameter, I got the expected result.
However, when I changed the Date, I got a response like this:
1
2
3
4
5
<error>
<code>InvalidArgument</code>
<message>sessionsignature is not match</message>
</error>From this, it can be inferred that Tianyi Home Cloud has been upgraded and Date has been added for verification.
Further exploration
I thought, since Date verification has been added, the server is likely to have a time check. However, I could not decompile the iOS version (assembly language etc.), so I used the dex2jar tool on the Android platform to decompile it and study how Date and Signature are calculated.
I downloaded the Tianyi Home Cloud Android version, specifically version 7.3.0 (the latest version as of February 9, 2019). I also prepared dex2jar. Decompilation can be done following this guide Android APK Decompilation Practice, or you can search for how to use dex2jar on your own. This article will not go into further explanation.
Decompilation process may show errors, but don’t worry. At this point, I obtained three files:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18D:\********>cd D:\********\dex2jar-2.0
D:\********\dex2jar-2.0>d2j-dex2jar.bat C:\********\classes.dex
dex2jar C:\********\classes.dex -> .\classes-dex2jar.jar
Detail Error Information in File .\classes-error.zip
Please report this file to http://code.google.com/p/dex2jar/issues/entry if possible.
D:\********\dex2jar-2.0>d2j-dex2jar.bat C:\********\classes2.dex
dex2jar C:\********\classes2.dex -> .\classes2-dex2jar.jar
Detail Error Information in File .\classes2-error.zip
Please report this file to http://code.google.com/p/dex2jar/issues/entry if possible.
D:\********\dex2jar-2.0>d2j-dex2jar.bat C:\********\classes3.dex
dex2jar C:\********\classes3.dex -> .\classes3-dex2jar.jar
Detail Error Information in File .\classes3-error.zip
Please report this file to http://code.google.com/p/dex2jar/issues/entry if possible.
D:\********\dex2jar-2.0>Use jd-gui to extract the source code from these files. Unpack the source code to facilitate full-text search using tools such as Sublime (I find Jd-GUI search to be cumbersome and unnecessary in this step).
Since the called interface is startQos.action, this is a keyword, so I performed a global search for
startQos
to get some clues. The search results reveal something suspicious in the third section, the StartQosRequest.java file inside the classes2.jar (Wow, found it so quickly, it exceeded my expectations…)Using Jd-GUI, I found the following code. From this code, it can be seen that it calls the send method, with the request address as the parameter. However, before making the request, it calls the addSessionHeaders method, which is the target we are looking for (they didn’t even bother to encrypt it properly!). Without hesitation, let’s dive straight into the method!
Inside the addSessionHeaders method, it calls the
HelperUtil.addSessionHeader(this.mHttpRequest, paramSession, paramString)
method, so let’s go deeper. In the next method, we can find our target.
This line is especially obvious:1
getSignatrue(paramString, str2, paramSession.getSessionSecret(), paramHttpRequestBase.getMethod(), str1)
It seems that this is the algorithm for calculating the Signature. Upon entering it, we find the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public static String getSignatrue(String paramString1, String paramString2, String paramString3, String paramString4, String paramString5)
{
StringBuilder localStringBuilder1 = new StringBuilder();
StringBuilder localStringBuilder2 = new StringBuilder();
localStringBuilder2.append(FamilyConfig.SessionKey);
localStringBuilder2.append("=");
localStringBuilder1.append(localStringBuilder2.toString());
localStringBuilder1.append(paramString2);
localStringBuilder1.append("&Operate=");
localStringBuilder1.append(paramString4);
if (paramString1.startsWith("/")) {
localStringBuilder1.append("&RequestURI=");
} else {
localStringBuilder1.append("&RequestURI=/");
}
localStringBuilder1.append(paramString1);
localStringBuilder1.append("&Date=");
localStringBuilder1.append(paramString5);
DLog.v("httpSignature", localStringBuilder1.toString());
return CodecUtil.hmacsha1(localStringBuilder1.toString(), paramString3);
}When we go deeper into the CodecUtil.hmacsha1 method, we find the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static String hmacsha1(String paramString1, String paramString2)
{
try
{
try
{
Mac localMac = Mac.getInstance("HmacSHA1");
localMac.init(new SecretKeySpec(paramString2.getBytes(), "HmacSHA1"));
paramString1 = localMac.doFinal(paramString1.getBytes());
}
catch (InvalidKeyException paramString1)
{
paramString1.printStackTrace();
}
}
catch (NoSuchAlgorithmException paramString1)
{
for (;;) {}
}
paramString1 = null;
return ByteFormat.toHex(paramString1);
}Analysis: Based on the above code, the Sign algorithm requires the following parameters:
The first paramString is a fixed value:
family/qos/startQos.action
The second paramString is the sessionKey. It calls
paramSession.getSessionKey()
The third paramString is the sessionSecret, a new parameter
The fourth paramString is the request method, which is fixed as
POST
because the request is a POST requestThe fifth paramString is the Date. Finally, the hmacsha1 method is called to encrypt using the HmacSHA1 algorithm, producing the Signature
The syncServerTime method uses
SystemClock.elapsedRealtime()
, which is an Android method used to get the time since the system was booted, represented as the elapsed time between the system startup and the current time.FamilyConfig.pre_elapsed_time
can be inferred as the time of the last startup. However, how does the server know when my device was started up? I will simply use fixed values for these two parameters.Copy these codes, reorganize them slightly, and the final reorganized code is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66private static final String SESSION_KEY= "SessionKey";
private static final String ACCESS_URL = "family/qos/startQos.action";
private static String syncServerDate() {
SimpleDateFormat localSimpleDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
Date localObject1 = new Date();
localSimpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
String str = localSimpleDateFormat.format((Date)localObject1);
long l1 = 16000; // 原 SystemClock.elapsedRealtime() 系统启动时间, 随便填
long l2 = 12500; // 原 FamilyConfig.pre_elapsed_time, 上次系统启动时间, 随便填
Date localObject2 = new Date(localObject1.getTime() + (l1 - l2));
if (localObject2 != null) {
try {
localSimpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
return localSimpleDateFormat.format((Date)localObject2);
} catch (Exception localException2) {
localException2.printStackTrace();
}
}
return str;
}
public static String getSignatrue(String accessUrl, String sessionKey, String sessionSecret, String requestMethod, String syncServerDate)
{
StringBuilder localStringBuilder1 = new StringBuilder();
StringBuilder localStringBuilder2 = new StringBuilder();
localStringBuilder2.append(SESSION_KEY);
localStringBuilder2.append("=");
localStringBuilder1.append(localStringBuilder2.toString());
localStringBuilder1.append(sessionKey);
localStringBuilder1.append("&Operate=");
localStringBuilder1.append(requestMethod);
if (accessUrl.startsWith("/")) {
localStringBuilder1.append("&RequestURI=");
} else {
localStringBuilder1.append("&RequestURI=/");
}
localStringBuilder1.append(accessUrl);
localStringBuilder1.append("&Date=");
localStringBuilder1.append(syncServerDate);
return hmacsha1(localStringBuilder1.toString(), sessionSecret);
}
public static String hmacsha1(String paramString1, String paramString2) {
try {
Mac localMac = Mac.getInstance("HmacSHA1");
localMac.init(new SecretKeySpec(paramString2.getBytes(), "HmacSHA1"));
return toHex(localMac.doFinal(paramString1.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String toHex(byte[] paramArrayOfByte) {
if ((paramArrayOfByte != null) && (paramArrayOfByte.length != 0)) {
StringBuilder localStringBuilder = new StringBuilder();
int i = 0;
while (i < paramArrayOfByte.length) {
localStringBuilder.append(HEX[(paramArrayOfByte[i] >> 4 & 0xF)]);
localStringBuilder.append(HEX[(paramArrayOfByte[i] & 0xF)]);
i += 1;
}
return localStringBuilder.toString();
}
return "";
}Now we know the Sign algorithm, but a new question arises: How do we obtain the Secret?
Attempting to capture the relevant information for Secret
Guess: Generally, a secret token is used for communication between the app and the server, most likely as a token for authentication. As it is such an important piece of information, China Telecom probably wouldn’t transmit it over HTTP, so it’s likely using HTTPS (as confirmed by the guess). Charles supports capturing HTTPS traffic, and some configuration is required.
Configuring Charles for HTTPS capture
For Android devices, refer to this link. The following instructions are for iOS. First, go back to Charles and select the “Help” tab, then choose “SSL Proxying” > “Install Charles Root Certificate on a Mobile Device or Remote Browser”
I already set up the SSL capture, so I can capture HTTPS requests in the screenshot.
After clicking, the following screen appears, instructing us to configure Charles in the device with the address “192.168.1.100:xxxx” (This also tells us that the phone and the computer running Charles need to be on the same local network). Then, use the phone’s browser to visit
chls.pro/ssl
and download and install the certificate. There is also a special note: “For iOS 10 and above, you must go to Settings -> General -> About -> Certificate Trust Settings and enable complete trust for the Charles certificate.”Follow the instructions. The basic steps are as follows: “If you have an Apple Watch, you may see the prompt in the second image. Choose ‘iPhone’ and don’t forget to go to the settings to trust the certificate.”
Once completed, go to the Charles menu bar, choose “Proxy” > “SSL Proxying Settings…”, and open the settings. Set
*
as the host and port 443 to capture all SSL requests on the 443 port (note: SSL is not necessarily on the 443 port, it can also be on port 8443).
Capturing the relevant information for Secret again
Run Tianyi Home Cloud again and noticed that the packets were captured. After some searching, I pinpointed the following (to cut to the chase, this is it).
After testing, I found that this was the sessionKey we needed. I stored its value in a variable, calculated the Signature, sent the request, and obtained the same result as what the app sent.
I also tried to figure out how it obtained the sessionKey and sessionSecret, but I gave up halfway… It became too much of a hassle…
March 30, 2019, update: It should be noted that sessionKey obtained in this article is still valid…
Don’t forget to close the certificate trust for Charles in a timely manner. There are certain risks associated with trusting a self-signed root certificate.
Attempting Automation
Now, you can write a Java program and use Crontab to schedule it to run regularly, or perform other operations to maintain the speed boost (it seems that sending the acceleration request every 10 minutes is sufficient, so the code also includes a 10-minute thread sleep).
The code is as follows:
1 | package com.ruterfu; |