某游戏盒App非标准算法的协议逆向

admin 2023年7月15日01:48:03评论14 views字数 10367阅读34分33秒阅读模式
VOL 155

14

2023-7

今天距2024年170天

这是鸣谦安全第155次推文

点击上方蓝字“鸣谦安全“关注我,周二,周五,每晚 19:00准时推送。

微信公众号后台回复“资源”领取学习资料,回复“QQ群”一起进群学习闲聊。

本文4127字,阅读约需10分钟

本文对某游戏盒 App 进行协议分析,从抓包发现是乱码后,然后 hook 系统关键方法进行快速定位,最后再结合动静态分析,还原出登录中用的非标准算法,最后实现协议复现。

配置抓包环境

抓包环境使用的是手机端 Postern 加电脑端 Charles。

电脑端的 IP 地址是 192.168.0.104 ,SOCKS Proxy 的端口配置为 8889。

手机端配置代理规则对应电脑端的设置。

抓到的乱码

再确认抓包环境无误,并且抓包开启的情况下。

进入 App 的登录界面,输入手机号 15026818188 密码 123456789。

发现抓到的包的 url 是 /xxxxxxxx/user/login ,见名知意也知道登录包绝对是抓到了。

但是提交的内容却是乱码的。

将 Contents 的内容由 Raw 转为 Hex 显示。

可知,发送的内容其实就是上面的一系列 16 进制字符,是抓包工具 Charles 硬转成字符串形式,才造成的乱码。

hook系统方法实现关键方法定位

抓到的内容是乱码的,证明一定存在加密,因为如果不加密,App 直接传输原字符串就好了。

如果采用 hook Java 加密库的方式,抓包的内容又是乱码的,又无法在 hook 的结果中进行比对。

所以就只能 hook 在加密过程中,大概率一定会调用的系统方法。

例如 哈希算法会调用 getBytes 方法。对称加密和非对称加密也会调用 getBytes 方法。

所以直接用 objection hook 一下 java.lang.String.getBytes 的所有重载方法,并打印参数、调用栈、返回值。

1android hooking watch class_method java.lang.String.getBytes --dump-args --dump-backtrace --dump-return

当点击登录的时候,发现成功触发了 hook 。

1xxx.xxxx.xxxxxxxxx on (vivo: 9) [usb] # (agent) [337799] Called java.lang.String.getBytes()
 2(agent) [337799] Backtrace:
 3        java.lang.String.getBytes(Native Method)
 4        xxx.xxxx.xxxxxxxxx.util.Encrypt.encodeStr(Encrypt.java:92)
 5        xxx.xxxx.xxxxxxxxx.util.Encrypt.encode(Encrypt.java:23)
 6        xxx.xxxx.xxxxxxxxx.network.GetDataImpl.doRequest(GetDataImpl.java:237)
 7        xxx.xxxx.xxxxxxxxx.network.GetDataImpl.requestLoginUrl(GetDataImpl.java:1017)
 8        xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:100)
 9        xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:96)
10        android.os.AsyncTask$2.call(AsyncTask.java:333)
11        java.util.concurrent.FutureTask.run(FutureTask.java:266)
12        android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:245)
13        java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
14        java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
15        java.lang.Thread.run(Thread.java:764)
16
17(agent) [337799] Called java.lang.String.getBytes(java.nio.charset.Charset)
18(agent) [337799] Backtrace:
19        java.lang.String.getBytes(Native Method)
20        java.lang.String.getBytes(String.java:978)
21        java.lang.String.getBytes(Native Method)
22        xxx.xxxx.xxxxxxxxx.util.Encrypt.encodeStr(Encrypt.java:92)
23        xxx.xxxx.xxxxxxxxx.util.Encrypt.encode(Encrypt.java:23)
24        xxx.xxxx.xxxxxxxxx.network.GetDataImpl.doRequest(GetDataImpl.java:237)
25        xxx.xxxx.xxxxxxxxx.network.GetDataImpl.requestLoginUrl(GetDataImpl.java:1017)
26        xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:100)
27        xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:96)
28        android.os.AsyncTask$2.call(AsyncTask.java:333)
29        java.util.concurrent.FutureTask.run(FutureTask.java:266)
30        android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:245)
31        java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
32        java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
33        java.lang.Thread.run(Thread.java:764)
34
35(agent) [337799] Arguments java.lang.String.getBytes(UTF-8)
36(agent) [337799] Return Value: [object Object]
37(agent) [337799] Return Value: [object Object]
38(agent) [337799] Called java.lang.String.getBytes()
39(agent) [337799] Backtrace:
40        java.lang.String.getBytes(Native Method)
41        xxx.xxxx.xxxxxxxxx.GetDataImpl.doRequest(GetDataImpl.java:238)
42        xxx.xxxx.xxxxxxxxx.network.GetDataImpl.requestLoginUrl(GetDataImpl.java:1017)
43        xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:100)
44        xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:96)
45        android.os.AsyncTask$2.call(AsyncTask.java:333)
46        java.util.concurrent.FutureTask.run(FutureTask.java:266)
47        android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:245)
48        java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
49        java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
50        java.lang.Thread.run(Thread.java:764)

发现调用的重载只有两个

1java.lang.String.getBytes()
2java.lang.String.getBytes(java.nio.charset.Charset)

objection hook 后得到的返回值由于是 byte 数组类型,objection 不支持显示除 String 的其他类型,因此显示的是 [object Object] 。

所以需要手动编写 hook 代码,重新进行 hook,并将 byte 数组类型转换为 String。

hook 代码为:

 1setImmediate(function () {
 2    function showStacks() {
 3        console.log(
 4            Java.use("android.util.Log")
 5                .getStackTraceString(
 6                    Java.use("java.lang.Throwable").$new()
 7                )
 8        );
 9    }
10
11    Java.perform(function () {
12        let targetClass = 'java.lang.String';
13        let gclass = Java.use(targetClass);
14        gclass.getBytes.overload('java.nio.charset.Charset').implementation = function (c) {
15            let i = this.getBytes(c)
16            let result = gclass.$new(i)
17            console.log("result =>", result)
18            showStacks()
19            return i;
20        }
21
22        gclass.getBytes.overload().implementation = function () {
23            let i = this.getBytes()
24            let result = gclass.$new(i)
25            console.log("result =>", result)
26            showStacks()
27            return i;
28        }
29    })
30})

hook 到的结果如下,可以看到通过 hook 系统方法,一下就定位到了关键方法并拿到了重要数据。

 1result => {"appid":"2","username":"15026818188","password":"123456789","cpsId":"tg001lxx","imei":"ffffffff-e36a-74b4-ffff-ffffae7732a7"}
 2java.lang.Throwable
 3        at java.lang.String.getBytes(Native Method)
 4        at xxx.xxxx.xxxxxxxxx.util.Encrypt.encodeStr(Encrypt.java:92)
 5        at xxx.xxxx.xxxxxxxxx.util.Encrypt.encode(Encrypt.java:23)
 6        at xxx.xxxx.xxxxxxxxx.network.GetDataImpl.doRequest(GetDataImpl.java:237)
 7        at xxx.xxxx.xxxxxxxxx.network.GetDataImpl.requestLoginUrl(GetDataImpl.java:1017)
 8        at xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:100)
 9        at xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:96)
10        at android.os.AsyncTask$2.call(AsyncTask.java:333)
11        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
12        at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:245)
13        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
14        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
15        at java.lang.Thread.run(Thread.java:764)
16
17result => x149_170_124_155_151_125_120_167_146_124_170_152_172_206_174_207_179_171_179_155_206_158_195_231_209_221_183_230_205_199_190_172_192_226_190_171_142_134_140_118_148_138_140_124_150_158_178_128_150_183_198_185_180_153_153_206_184_137_187_206_188_199_123_138_127_154_123_171_129_159_131_103_134_141_186_149_112_110_180_170_210_209_181_173_181_214_208_182_189_186_203_181_188_170_190_228_201_230_198_190_130_186_165_140_173_122_144_177_192_179_172_163_126_186_176_163_154_136_156_193_175_195_177_197_179_199_132_142_138_158_126_138_137_175_132_164_125_173_175_220_181_205_179_169_152_213_195_215_197_217_185_197_201_221_126_124_205_225_207_222_209_204_220_173_142_188_141_172_147_191_145_129_86_84y
18java.lang.Throwable
19        at java.lang.String.getBytes(Native Method)
20        at xxx.xxxx.xxxxxxxxx.network.GetDataImpl.doRequest(GetDataImpl.java:238)
21        at xxx.xxxx.xxxxxxxxx.network.GetDataImpl.requestLoginUrl(GetDataImpl.java:1017)
22        at xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:100)
23        at xxx.xxxx.xxxxxxxxx.ui.LoginActivity$1.doInBackground(LoginActivity.java:96)
24        at android.os.AsyncTask$2.call(AsyncTask.java:333)
25        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
26        at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:245)
27        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
28        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
29        at java.lang.Thread.run(Thread.java:764)

jadx静态分析

定位到关键方法之后就可以进行静态分析了。

根据打印的调用栈首先找到 xxx.xxxx.xxxxxxxxx.network.GetDataImpl.requestLoginUrl 方法。

requestLoginUrl 方法做的内容就是将数据放到 jSONObject 中。然后调用 doRequest 方法。

继续跟踪方法,查看 doRequest 方法。

doRequest 方法做的是构造一个 post 请求方式,将 str2 的内容经过 Encrypt.encode 方法加密,然后 getBytes() ,进行传输到服务器。

继续跟踪方法,查看 encode 方法。

encode 方法里面就是做的一系列加密,然后返回一个字符串。

encode 方法里面又调用了 encodeStr 方法。

而 encodeStr 方法里面看似只做了一个 base64 的编码,但调用的不是 Java 官方库的,而是 apache 下的一个库。

具体和 Java 官方库的 base64 编码是否有区别,接下来再验证。

objection hook

通过上面的分析可以得出代码逻辑,在和 Hook 得出的调用栈对比。

hook 到的结果是下面这段 json 。

1result => {"appid":"2","username":"15026818188","password":"123456789","cpsId":"tg001lxx","imei":"ffffffff-e36a-74b4-ffff-ffffae7732a7"}

这段 json 是经过 byte 数组转换为字符串的,所以 encodeStr 方法的 str,也就是调用 getBytes() 的就是上面这段 json。

下面这段结果应该就是 encode 方法加密后的结果。

1result => x149_170_124_155_151_125_120_167_146_124_170_152_172_206_174_207_179_171_179_155_206_158_195_231_209_221_183_230_205_199_190_172_192_226_190_171_142_134_140_118_148_138_140_124_150_158_178_128_150_183_198_185_180_153_153_206_184_137_187_206_188_199_123_138_127_154_123_171_129_159_131_103_134_141_186_149_112_110_180_170_210_209_181_173_181_214_208_182_189_186_203_181_188_170_190_228_201_230_198_190_130_186_165_140_173_122_144_177_192_179_172_163_126_186_176_163_154_136_156_193_175_195_177_197_179_199_132_142_138_158_126_138_137_175_132_164_125_173_175_220_181_205_179_169_152_213_195_215_197_217_185_197_201_221_126_124_205_225_207_222_209_204_220_173_142_188_141_172_147_191_145_129_86_84y

为了印证上面的推理,再次利用 objection hook。

这次 hook 两个方法。

1android hooking watch class_method xxx.xxxx.xxxxxxxxx.util.Encrypt.encode --dump-args --dump-backtrace --dump-return
2android hooking watch class_method xxx.xxxx.xxxxxxxxx.util.Encrypt.encodeStr --dump-args --dump-backtrace --dump-return
hook 到的结果正如推理的一致。

base64是真的吗

接下来就证明 encodeStr 方法中的 base64 编码和 Java 官方库的 base64 编码是否有区别。

因为上面已经有 objection hook 到的结果,所以直接将字符串丢入到任意在线 base64 编码的网址,看是否和 objection hook 到输出的结果是否一致。

比对后证明是一致的,但又是不一致的,App 中的 Base64 计算方法和常规的计算方法是一样的,但是多了换行符。

非标准算法的还原

接下来只剩对 encode 方法的还原了。

如今的反编译工具还是比较强的,Java 层的算法还原,只需要扣反编译后的代码放入到 Java 中运行即可,如果碰到复杂的,还原 Smali 也可解决。

还原后的 Java 代码如下,这里要注意回车,经过测试后发现此 App base64 编码后使用 rn 来换行的。

 1public class CustomAlgorithm {
 2    public static void main(String[] args) {
 3        char[] k = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '*', '!'};
 4
 5        String encodeStr = "eyJhcHBpZCI6IjIiLCJ1c2VybmFtZSI6IjE1MDI2ODE4MTg4IiwicGFzc3dvcmQiOiIxMjM0NTY3rn" +
 6                "ODkiLCJjcHNJZCI6InRnMDAxbHh4IiwiaW1laSI6ImZmZmZmZmZmLWUzNmEtNzRiNC1mZmZmLWZmrn" +
 7                "ZmZhZTc3MzJhNyJ9rn";
 8
 9        StringBuilder stringBuffer = new StringBuilder();
10
11        char[] charArray = encodeStr.toCharArray();
12        stringBuffer.append("x");
13        for (int i = 0; i < charArray.length; i++) {
14            char c = charArray[i];
15            char[] cArr = k;
16            stringBuffer.append(c + cArr[i % cArr.length]);
17            stringBuffer.append("_");
18        }
19
20        stringBuffer.deleteCharAt(stringBuffer.length() - 1);
21        stringBuffer.append("y");
22
23        String result = stringBuffer.toString();
24        System.out.println(result);
25    }
26}

协议分析到此结束,最后只要简单构建一个 Http 请求,就可以完成脱机登录了。

总结语

在逆向过程中,有时候会碰到自定义的算法和抓包显示乱码的情况,让逆向无从下手。

这时候就要对系统库和日常的开发有一定的认知,因为再复杂的程序,也不可能不调用系统库,只要调用了,就一定能通过 hook 拿到调用栈进而定位到关键方法处。

另外如过碰到非标准算法的还原问题,如果是 Java 层,可以采用扣代码来完成,过于复杂的程序,也可以采用 SmaIi 手动还原代码的方式来完成。

以上
That‘s all
更多系列文章
敬请期待

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年7月15日01:48:03
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   某游戏盒App非标准算法的协议逆向http://cn-sec.com/archives/1877648.html

发表评论

匿名网友 填写信息