一般移动端的请求都会带一些加密字段,比如链家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获取。