一般移动端的请求都会带一些加密字段,比如链家App的 Authorization
,这是由后端动态生成的,因此想要实现对App的数据爬取,需要破解请求中的加密字段。
获取链家APK
由于手上目前没有安卓机,故选择 Genymotion 来获取APK并调试。Genymotion 是一款安卓模拟器, 支持 Mac,Windows及 Linux等平台,虽是付费产品,不过在注册账号后可以安装Genymotion for personal use
,即个人免费版。
Genymotion 依赖 VirtualBox,在 VirtualBox 中通过设置共享文件夹,即可把下载的 APK 拷贝到主机,虚拟机中共享文件夹位置在/mnt/shared/共享文件夹名
。
反编译 APK
工具
- dex2jar:APK 解压后可以得到几个
.dex
文件,它是 classes 文件通过DEX编译后的文件格式,包含应用代码,dex2jar,顾名思义,可以将dex文件转换成jar格式的文件。
- JD-GUI:Java反编译器的GUI工具,导入jar文件可以看到反编译后的代码。
使用 dex2jar 将dex反编译成jar
解压APK得到dex文件
反编译dex文件
将解压出的dex文件拷贝到 dex2jar 文件夹下,执行命令得到jar文件。
1
2
3
4
|
chmod 764 *.sh
sh d2j-dex2jar.sh classes.dex
sh d2j-dex2jar.sh classes2.dex
sh d2j-dex2jar.sh classes3.dex
|
使用 JD-GUI 查看源码
将 jar 文件拖拽到 JD-GUI 中,即可看到反编译后的源码。
分析 Authorization
的生成逻辑
搜索关键字Authorization
定位到HeaderInterceptor
文件。
localObject1
的值赋给了Authorization
,接下来需要找到localObject1
的生成逻辑:
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
|
public Response intercept(Interceptor.Chain paramChain)
throws IOException
{
Object localObject1 = BaseSharedPreferences.a().d();
Request.Builder localBuilder = paramChain.request().newBuilder();
Object localObject2 = paramChain.request().url().uri().toString();
HashMap localHashMap = new HashMap();
if (!Tools.d((String)localObject1)) {
localBuilder.addHeader("Lianjia-Access-Token", (String)localObject1);
}
localObject1 = new HashMap();
((HashMap)localObject1).put("mac_id", DeviceUtil.s(APPConfigHelper.c()));
((HashMap)localObject1).put("lj_device_id_android", DeviceUtil.x(APPConfigHelper.c()));
((HashMap)localObject1).put("lj_android_id", DeviceUtil.y(APPConfigHelper.c()));
((HashMap)localObject1).put("lj_imei", DeviceUtil.z(APPConfigHelper.c()));
localBuilder.addHeader("extension", Tools.a((HashMap)localObject1));
boolean bool = "GET".equalsIgnoreCase(paramChain.request().method());
localObject1 = null;
if (bool)
{
/*这里对请求方法进行了判断,通过 b 函数生成了localObject1,接下来跳入到 b 函数中*/
localObject1 = HttpUtil.a().b((String)localObject2, localHashMap);
}
else if ("POST".equalsIgnoreCase(paramChain.request().method()))
{
······
}
localObject2 = localBuilder.addHeader("User-Agent", BaseParams.a().d()).addHeader("Lianjia-Channel", DeviceUtil.e(APPConfigHelper.c())).addHeader("Lianjia-Device-Id", DeviceUtil.k());
BaseParams.a();
((Request.Builder)localObject2).addHeader("Lianjia-Version", BaseParams.e()).addHeader("Lianjia-City-Id", CityConfigCacheHelper.a().f()).addHeader("Authorization", (String)localObject1).addHeader("Lianjia-Im-Version", APPConfigHelper.g());
return paramChain.proceed(localBuilder.build());
}
}
|
在 JD-GUI 中直接点击函数名即可跳转:
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
|
public String b(String paramString, Map<String, String> paramMap)
{
/*传入的paramString为url,这里通过一个函数对url进行了处理,代码就不贴了,函数的作用是将url的参数转为字典。*/
paramString = b(paramString);
Object localObject1 = new HashMap();
if (paramString != null) {
((HashMap)localObject1).putAll(paramString);
}
if (paramMap != null) {
((HashMap)localObject1).putAll(paramMap);
}
/*对参数排序*/
Object localObject2 = new ArrayList(((HashMap)localObject1).entrySet());
Collections.sort((List)localObject2, new HttpUtil.1(this));
paramString = null;
try
{
/*这里涉及到了AppSecret和AppId两个参数,不过之前在headers中并没有看到这两个参数,先在此处插眼标记。*/
localObject1 = JniClient.GetAppSecret(APPConfigHelper.c().getApplicationContext());
paramMap = JniClient.GetAppId(APPConfigHelper.c().getApplicationContext());
localObject1 = new StringBuilder((String)localObject1);
int i = 0;
while (i < ((List)localObject2).size())
{
/*遍历排序好的键值对,拼接成key1=value1key2=value2的格式,然后加到AppSecret字符串后边。*/
localObject3 = (Map.Entry)((List)localObject2).get(i);
StringBuilder localStringBuilder = new StringBuilder();
localStringBuilder.append((String)((Map.Entry)localObject3).getKey());
localStringBuilder.append("=");
localStringBuilder.append((String)((Map.Entry)localObject3).getValue());
((StringBuilder)localObject1).append(localStringBuilder.toString());
i += 1;
}
localObject2 = a;
Object localObject3 = new StringBuilder();
((StringBuilder)localObject3).append("sign origin=");
((StringBuilder)localObject3).append(localObject1);
LjLogUtil.a((String)localObject2, ((StringBuilder)localObject3).toString());
/*这里将上边的字符串传入了新的函数中,这个c函数的作用是SHA1加密。*/
localObject1 = DeviceUtil.c(((StringBuilder)localObject1).toString());
localObject2 = new StringBuilder();
/*AppId与加密后的字符串拼接。并生成Base64编码。*/
((StringBuilder)localObject2).append(paramMap);
((StringBuilder)localObject2).append(":");
((StringBuilder)localObject2).append((String)localObject1);
paramMap = Base64.encodeToString(((StringBuilder)localObject2).toString().getBytes(), 2);
try
{
paramString = a;
localObject1 = new StringBuilder();
((StringBuilder)localObject1).append("sign result=");
((StringBuilder)localObject1).append(paramMap);
LjLogUtil.a(paramString, ((StringBuilder)localObject1).toString());
return paramMap;
}
catch (Exception localException)
{
paramString = paramMap;
paramMap = localException;
}
paramMap.printStackTrace();
}
catch (Exception paramMap) {}
return paramString;
}
|
总结Authorization
的生成逻辑
- url中的params转字典
- 根据key排序
- localObject1 = AppSecret + (遍历键值对,“="号相连)
- localObject1 = 对localObject1使用SHA1加密
- localObject2 = AppId + “:” + localObject1
- Authorization = localObject2转Base64编码
获取AppSecret
和AppId
两个参数
分析了 Authorization 的生成逻辑后,只有AppSecret
和AppId
两个参数是未知的,通过动态调试 APK 获取两个参数的值。
根据 Authorization
的生成逻辑,Python 版本的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
AppSecret = "xxx"
AppId = "xxx"
def get_authorization(url_dict):
global AppSecret
global AppId
sorted_dict = {key: url_dict[key] for key in sorted(url_dict)}
localObject1 = ''.join([key + '=' + str(sorted_dict[key]) for key in sorted_dict.keys()])
localObject1 = AppSecret + localObject1
localObject1_sha1 = hashlib.sha1(localObject1.encode()).hexdigest()
authorization_source = AppId + ":" + localObject1_sha1
authorization = base64.b64encode(authorization_source.encode())
return authorization.decode()
def url2dict():
global url
params = re.search(r'.*\?(.*)', url).group(1)
params_list = params.split("&")
params_dict = {}
for item in params_list:
key = re.search(r'(.*)=(.*)', item).group(1)
value = re.search(r'(.*)=(.*)', item).group(2) if not item.endswith("=") else ""
params_dict[key] = value
return params_dict
|
其中,AppSecret 及 AppId 两个参数,需要通过动态调试 APK获取。