差点就花钱找人了……
过程记录
周末的时候心血来潮想获取一个 app 的数据,就试着看能不能逆一下。先是抓了个包,发现请求和响应都不是明文:
典型的 Base64,但解码发现是数据加密之后再 Base64 的,因此需要寻找加密算法。这个 app 没有加壳,直接拖到 JADX 里,先拿到 Java 层的源代码。跟了一下发现所有的请求和响应都有统一的加解密管理,业务层看到的是这样:
public EncryptJsonCallback<***Response> mCallBack = new EncryptJsonCallback<***Response>() { @Override public void onError(int i, String str) { ***Presenter.this.callLoadAll***(-11); } @Override public void onStart(Request<***Response, ? extends Request> request) { super.onStart(request); ***Presenter.this.callLoadAll***(-10); } @Override public void onSuccess(Response<***Response> response) { if (response == null || response.body() == null || response.body().code != 0) { ***Presenter.this.callLoadAll***(-11); } else { ***Presenter.this.parse***(response.body().data, response.body()); } } };
在这里的 onSuccess()
方法中,参数已经是解密过的了,继续往下找,发现了解密响应的部分:
@Override public T convertResponse(Response response) throws Throwable { // ... if (new JSONObject(string).optInt("isSign") == 1) { EncryptResponse encryptResponse = (EncryptResponse) GsonUtils.fromJson(string, (Class<Object>) EncryptResponse.class); IEncTools encTools = EncToolsFactory.getEncTools(OkGo.getContext()); JSONObject jSONObject = new JSONObject(); if (TextUtils.isEmpty(encryptResponse.data)) { jSONObject.put("data", (Object) null); } else { encryptResponse.data = encryptResponse.data.replaceAll("\n", ""); if (TextUtils.isEmpty(this.encryptKey)) { return null; } encryptResponse.data = encTools.decode(encryptResponse.data, getServerFlag(), this.encryptKey); if (TextUtils.isEmpty(encryptResponse.data)) { return null; } jSONObject.put("data", new JSONObject(encryptResponse.data)); } jSONObject.put("code", encryptResponse.code); // ... } // ... }
这里大致的逻辑是从加密的响应中解密出来 data
放到一个新构造的 JSON 对象里。需要关注的是 encTools.decode(encryptResponse.data, getServerFlag(), this.encryptKey)
这一句。
getSeverFlag()
对于 Release 版恒为 1。encryptKey
经过尝试是一个固定的值,接下来只要逆出来加密算法就好了。跳到 IEncTools
的实现类,这其实也是个 Wrapper:
public class EncToolsImpl implements IEncTools { public static EncToolsImpl mEncToolsImpl; public static EncToolsNative mEncToolsNative; public EncToolsImpl(Context context) { } public static EncToolsImpl getInstance(Context context) { if (mEncToolsImpl == null && context != null) { synchronized (EncToolsImpl.class) { if (mEncToolsImpl == null) { mEncToolsNative = new EncToolsNative(); mEncToolsImpl = new EncToolsImpl(context); } } } return mEncToolsImpl; } @Override public String decode(String str, int i, String str2) { if (str == null || str2 == null) { return new String("error input parameter"); } return (str.length() == 0 || str2.length() == 0) ? new String("error input parameter") : mEncToolsNative.decode(str, i, str2); } @Override public String encode(String str, int i, String str2) { if (str == null || str2 == null) { return new String("error input parameter"); } return (str.length() == 0 || str2.length() == 0) ? new String("error input parameter") : mEncToolsNative.encode(str, i, str2); } @Override public String sign(String str, String str2, int i, String str3, String str4) { if (str == null || str2 == null || str3 == null) { return new String("error input parameter"); } return (str.length() == 0 || str2.length() == 0 || str3.length() == 0) ? new String("error input parameter") : mEncToolsNative.sign(str, str2, i, str3, str4); } }
从命名上来看 encode
是加密,decode
是解密,sign
是签名。看到 Native 字眼的时候有一种很不好的感觉,点进去果然是 JNI:
public class EncToolsNative { public EncToolsNative() { System.loadLibrary("enc"); } public native String decode(String str, int i, String str2); public native String encode(String str, int i, String str2); public native String sign(String str, String str2, int i, String str3, String str4); }
先用 Frida 看看入参到底都是什么:
这里的 flag
就是前面提到的 getServerFlag()
的返回值。str
是密文,str2
是密钥。接下来就是大头,Native 层的算法逆向了。拉出来 IDA,先看看导出表:
三个 Java
开头的就是 JNI 的导出函数。这里注意到有很多 AES 相关的函数,以及 MD5 函数。结合之前的抓包,请求里的签名应该是对某种数据进行 MD5 之后的结果。加解密算法应该就是 AES 了,但至于是 CBC、ECB 还是 CTR,需要跟踪看一下。反编译一下 decode
看看:
__int64 __fastcall Java_***_EncToolsNative_decode( __int64 a1, __int64 a2, __int64 a3, unsigned int a4, __int64 a5) { // ... }
熟悉 JNI 的都知道,这个参数是不对的。用 JNI Helper 恢复一下参数:
jstring __fastcall Java_***_EncToolsNative_decode( JNIEnv *env, jobject this, jstring a1, jint a2, jstring a3) { // ... }
先看一下控制流:
很乱,显然经过了 OLLVM 的混淆。我对于 Native 逆向其实没什么经验,只能硬着头皮先看看,找到了一个真正负责解密的 decode
函数,跟进去:
void __usercall decode(unsigned int a1@<W1>, __int64 a2@<X2>, _QWORD *a3@<X8>) { // ... v50 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); base64_decode(); std::string::basic_string(v42, a2); getSecKey(v42, a1); if ( (v42[0] & 1) != 0 ) operator delete(ptr); if ( a1 ) std::string::basic_string<decltype(nullptr)>((int)&v39, "20190314********"); else std::string::basic_string<decltype(nullptr)>((int)&v39, "03920392********"); if ( (v44 & 1) != 0 ) v6 = v46; else v6 = v45; if ( (v39 & 1) != 0 ) v7 = v41; else v7 = v40; AES_init_ctx_iv(v49, v6, v7); AES_CBC_decrypt_buffer(v49, v47, (unsigned int)((_DWORD)v48 - (_DWORD)v47)); // ... }
嗯,很标准的从 Base64 字符串解密的过程。先进行 Base64 解码得到原始密文,然后进行 AES CBC 解密。众所周知 AES CBC 的参量有数据、密钥和 IV。简单搜索了一下,看起来这里的 AES 像是用的 TinyAES in C。参考源码还原出来这些函数的签名:
void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv); void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv); void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
如此一来,上面的 v6
就是密钥,v7
就是 IV,v47
是密文。值得一提的是,a1
是之前一直传进来的 Flag,这里会根据它的值决定用 20190314********
还是 03920392********
。之前说过这个是 Release 标志位,所以可以默认这里构造了一个字符串 20190314********
。
这里最简单的办法是挂调试器断点看寄存器,我比较懒嫌麻烦,于是直接上 Frida。需要注意的是,Frida 在 Hook Native 方法的时候传入的方法名不是在 IDA 里看到的导出符号表,而是在反编译之后的伪代码里看到名称。
来抓一下 IV,写一段 Frida:
Interceptor.attach(aesInitCtxIvPtr, { onEnter: function (args) { console.log("*******_Z15AES_init_ctx_ivP7AES_ctxPKhS2_"); console.log("args[0]=" + args[0]); console.log("key=" + args[1].readCString()); // console.log(hexdump(args[1])) console.log("iv=" + args[2].readCString()) // console.log(hexdump(args[2])) }, onLeave: function (retval) { } });
默认情况下打出来的是指针地址,对于 C 字符串,需要用 readCString()
来读出实际的内容。当然也可以使用 hexdump
来查看从这个地址起始的内存数据:
*******_Z15AES_init_ctx_ivP7AES_ctxPKhS2_ args[0]=0x783871b1b8 key=522A8D50******** 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 783871b189 35 32 32 41 38 44 35 30 xx xx xx xx xx xx xx xx 522A8D50******** 783871b199 00 00 00 00 00 00 00 80 8e d5 8d 77 00 00 b4 30 ...........w...0 783871b1a9 de d5 8d 77 00 00 b4 80 0e d6 8d 77 00 00 b4 48 ...w.......w...H 783871b1b9 ae 91 5f 79 9b 02 00 e0 b4 71 38 78 00 00 00 00 .._y.....q8x.... 783871b1c9 70 00 00 00 00 00 00 00 c0 71 38 78 00 00 00 6e p........q8x...n 783871b1d9 40 00 00 00 00 00 00 27 00 00 00 00 00 00 00 c8 @......'........ 783871b1e9 95 43 5f 77 00 00 00 50 6a 00 00 00 00 00 00 08 .C_w...Pj....... 783871b1f9 94 43 5f 77 00 00 00 c0 b3 71 38 78 00 00 00 00 .C_w.....q8x.... 783871b209 c0 71 38 78 00 00 00 20 00 00 00 00 00 00 00 00 .q8x... ........ 783871b219 03 b5 8d 77 00 00 b4 18 00 00 00 00 00 00 00 e0 ...w............ 783871b229 02 b5 8d 77 00 00 b4 0d 02 60 74 79 00 00 00 e1 ...w.....`ty.... 783871b239 bb b2 bb 79 1e ce 40 90 b2 71 38 78 00 00 00 e0 ...y..@..q8x.... 783871b249 65 91 5f 79 49 7f 00 00 c0 71 38 78 00 00 00 18 e._yI....q8x.... 783871b259 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 c0 ....... ........ 783871b269 0b fc 5b 78 00 00 b4 00 03 b5 8d 77 00 00 b4 e1 ..[x.......w.... 783871b279 bb b2 bb 79 1e ce 40 c0 b3 71 38 78 00 00 00 20 ...y..@..q8x... iv=20190314******** 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 783871b159 32 30 31 39 30 33 31 34 xx xx xx xx xx xx xx xx 20190314******** 783871b169 00 00 00 00 00 00 00 21 00 00 00 00 00 00 00 18 .......!........ 783871b179 00 00 00 00 00 00 00 20 03 b5 8d 77 00 00 b4 20 ....... ...w... 783871b189 35 32 32 41 38 44 35 30 xx xx xx xx xx xx xx xx 522A8D50******** 783871b199 00 00 00 00 00 00 00 80 8e d5 8d 77 00 00 b4 30 ...........w...0 783871b1a9 de d5 8d 77 00 00 b4 80 0e d6 8d 77 00 00 b4 48 ...w.......w...H 783871b1b9 ae 91 5f 79 9b 02 00 e0 b4 71 38 78 00 00 00 00 .._y.....q8x.... 783871b1c9 70 00 00 00 00 00 00 00 c0 71 38 78 00 00 00 6e p........q8x...n 783871b1d9 40 00 00 00 00 00 00 27 00 00 00 00 00 00 00 c8 @......'........ 783871b1e9 95 43 5f 77 00 00 00 50 6a 00 00 00 00 00 00 08 .C_w...Pj....... 783871b1f9 94 43 5f 77 00 00 00 c0 b3 71 38 78 00 00 00 00 .C_w.....q8x.... 783871b209 c0 71 38 78 00 00 00 20 00 00 00 00 00 00 00 00 .q8x... ........ 783871b219 03 b5 8d 77 00 00 b4 18 00 00 00 00 00 00 00 e0 ...w............ 783871b229 02 b5 8d 77 00 00 b4 0d 02 60 74 79 00 00 00 e1 ...w.....`ty.... 783871b239 bb b2 bb 79 1e ce 40 90 b2 71 38 78 00 00 00 e0 ...y..@..q8x.... 783871b249 65 91 5f 79 49 7f 00 00 c0 71 38 78 00 00 00 18 e._yI....q8x....
这样可以确定密钥和 IV 都是字符串,再来打一下内容:
*******_Z15AES_init_ctx_ivP7AES_ctxPKhS2_ args[0]=0x780f3fddd8 key=CRG************* iv=20190314******** *******_Z15AES_init_ctx_ivP7AES_ctxPKhS2_ args[0]=0x780f3fe018 key=522A8D50******** iv=20190314******** *******_Z15AES_init_ctx_ivP7AES_ctxPKhS2_ args[0]=0x780f3fd978 key=CRG************* iv=20190314******** *******_Z15AES_init_ctx_ivP7AES_ctxPKhS2_ args[0]=0x77aa3cff58 key=CRG************* iv=20190314******** *******_Z15AES_init_ctx_ivP7AES_ctxPKhS2_ args[0]=0x77aa3d01b8 key=522A8D50******** iv=20190314********
IV 很眼熟,就是之前在 Native decode
函数里看到的根据 Release 标志位里取出来的。但密钥跟之前从 Java 层传过来的东西不太一样,猜测应该是通过 getSecKey()
算出来的。因为我只是为了拿加解密算法和 AES 参量,暂时就先没有去分析这部分了。
注意到这里有两个不同的密钥,哪一个是要用的呢?都试试就好了。这里推荐一个很好用的在线工具 CyberChef,快速分析各种加解密和编解码算法。尝试下来发现 522A8D50********
是在我要的接口里用到的密钥:
到这里逆向响应就算完成了。尝试了一下,能正常取得响应的最小请求是这样:
{ "timeStamp": "30A78E464DF4BE9DD58CF93E41832296", "sign": "7DEA936BCD70A1688017400521A8FCA6", "isSign":2, "params": "soRHDQqFLOi4njTO69WlnP***", "cguid": "3O+koZ5vtyEVgycRLcynBie***" }
params
就是正常的请求 JSON 加密之后的结果。这里还需要搞清楚的几个东西是 timeStamp
、cguid
和 sign
三个参数。因为在 Native 层用不到后两个参数,我们先回到 Java 层。在发起请求的时候会统一进行加密:
private Response<T> encRequest() throws Throwable { String json; EncyptConvert encyptConvert = (EncyptConvert) this.request.getConverter(); EncryptPostRequest encryptPostRequest = (EncryptPostRequest) this.request; EncryptRequest encryptRequest = new EncryptRequest(); boolean hasEncrypt = encryptPostRequest.hasEncrypt(); encryptRequest.timeStamp = getUUID(); if (hasEncrypt) { encryptRequest.encrypt = 2; } else { encryptRequest.encrypt = 0; } Object upObject = encryptPostRequest.getUpObject(); String keySync = EncryptKeyManager.getInstance().getKeySync(); // ... if (upObject == null) { if (!hasEncrypt) { encryptRequest.paramsObj = null; } } else if (upObject instanceof ****BaseRequestModel) { // ... if (hasEncrypt) { json = GsonUtils.toJson(valueByKey); if (hasEncrypt) { if (TextUtils.isEmpty(json)) { encryptRequest.params = ""; } else { try { encryptRequest.params = EncToolsFactory.getEncTools(OkGo.getContext()).encode(json, getServerFlag(), keySync); // ... } catch (Exception e) { // ... } } } if (encryptPostRequest.hasCGuid()) { String cGuidSync = CGuidManager.getInstance().getCGuidSync(); // ... try { encryptRequest.sign = EncToolsFactory.getEncTools(OkGo.getContext()).sign(encryptRequest.timeStamp, keySync, getServerFlag(), encryptRequest.cguid, encryptRequest.params == null ? "" : encryptRequest.params); // ... } catch (Exception e2) { // ... } } / ... } encryptRequest.paramsObj = valueByKey; } else if (hasEncrypt) { // ... } else { encryptRequest.paramsObj = upObject; } // ... } private String getUUID() { String replace = UUID.randomUUID().toString().replace("-", ""); return MD5Util.encrypt_string(replace + System.nanoTime()); }
timeStamp
的计算有点好笑,就是一个去掉 -
的 UUID 拼上当前时间然后进行 MD5,经过测试用任意字符串进行 MD5 都可以,只要保证流程中所有用到 timeStamp
值的地方保持一致即可。
cguid
是一个设备相关的东西,在生成之后似乎会传递给服务端,因此在重放请求的时候这里保持使用真实值不变。
密钥的来源是 EncryptKeyManager
,里面会有一个请求去获取。
sign
的计算跟 timeStamp
、密钥、cguid
和加密后的请求参数 params
有关,再次回到 Native 层,用 Frida 捞一下:
*******_Z4signNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES5_iS5_S5_ arg0=0x780e4cbbf0 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 780e4cbbf0 31 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 1....... ....... 780e4cbc00 30 19 91 f1 75 00 00 b4 24 ac fe d7 78 83 45 00 0...u...$...x.E. 780e4cbc10 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ................ 780e4cbc20 98 f2 97 24 36 39 00 00 51 00 00 00 00 00 00 00 ...$69..Q....... 780e4cbc30 40 00 00 00 00 00 00 00 a0 61 48 bb 78 00 00 b4 @........aH.x... 780e4cbc40 51 00 00 00 00 00 00 00 4c 00 00 00 00 00 00 00 Q.......L....... 780e4cbc50 00 48 48 bb 78 00 00 b4 21 00 00 00 00 00 00 00 .HH.x...!....... 780e4cbc60 18 00 00 00 00 00 00 00 40 da eb 0e 78 00 00 b4 ........@...x... 780e4cbc70 31 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 1....... ....... 780e4cbc80 60 19 91 f1 75 00 00 b4 6c a4 5a 6a 2c 4e af 25 `...u...l.Zj,N.% 780e4cbc90 00 bd 4c 0e 78 00 00 00 10 af 44 cc 78 00 00 00 ..L.x.....D.x... 780e4cbca0 48 be 4c 0e 78 00 00 00 10 be 4c 0e 78 00 00 00 H.L.x.....L.x... 780e4cbcb0 00 09 e0 d7 78 00 00 00 74 06 00 00 00 00 00 00 ....x...t....... 780e4cbcc0 72 75 71 52 78 00 00 00 c8 e8 01 23 78 00 00 b4 ruqRx......#x... 780e4cbcd0 00 00 00 00 00 00 00 00 00 e8 01 23 78 00 00 b4 ...........#x... 780e4cbce0 48 be 4c 0e 78 00 00 00 34 74 e2 d7 78 00 00 00 H.L.x...4t..x... arg1=0x780e4cbbd0 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 780e4cbbd0 21 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 !............... 780e4cbbe0 a0 79 a2 00 78 00 00 b4 6c a4 5a 6a 2c 4e af 25 .y..x...l.Zj,N.% 780e4cbbf0 31 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 1....... ....... 780e4cbc00 30 19 91 f1 75 00 00 b4 24 ac fe d7 78 83 45 00 0...u...$...x.E. 780e4cbc10 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ................ 780e4cbc20 98 f2 97 24 36 39 00 00 51 00 00 00 00 00 00 00 ...$69..Q....... 780e4cbc30 40 00 00 00 00 00 00 00 a0 61 48 bb 78 00 00 b4 @........aH.x... 780e4cbc40 51 00 00 00 00 00 00 00 4c 00 00 00 00 00 00 00 Q.......L....... 780e4cbc50 00 48 48 bb 78 00 00 b4 21 00 00 00 00 00 00 00 .HH.x...!....... 780e4cbc60 18 00 00 00 00 00 00 00 40 da eb 0e 78 00 00 b4 ........@...x... 780e4cbc70 31 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 1....... ....... 780e4cbc80 60 19 91 f1 75 00 00 b4 6c a4 5a 6a 2c 4e af 25 `...u...l.Zj,N.% 780e4cbc90 00 bd 4c 0e 78 00 00 00 10 af 44 cc 78 00 00 00 ..L.x.....D.x... 780e4cbca0 48 be 4c 0e 78 00 00 00 10 be 4c 0e 78 00 00 00 H.L.x.....L.x... 780e4cbcb0 00 09 e0 d7 78 00 00 00 74 06 00 00 00 00 00 00 ....x...t....... 780e4cbcc0 72 75 71 52 78 00 00 00 c8 e8 01 23 78 00 00 b4 ruqRx......#x... arg2=0x1
很奇怪的是这里 Dump 出来的内存都不是想要的东西,而且参数数量也不对。那既然知道签名是 MD5 出来的,直接 Hook 最终 MD5 的地方,看看明文是什么:
*******_Z3md5PKvm arg0 0xb400006c50a471a0 BA0D8BEC59FF4DAA747603179EB63DCDCRG***PRODQwe*******3O+koZ5vtyEVgycRLcynBie***ZSxI1NTbvRcs2Z9nyQb9W5H1Kc*** return:0xb4000075f2b23cc0
arg0
里这些就是待签名的数据,拆一下:
BA0D8BEC59FF4DAA747603179EB63DCD CRG**** PRODQwe******* 3O+koZ5vtyEVgycRLcynBie*** ZSxI1NTbvRcs2Z9nyQb9W5H1Kc***
第一部分是 timeStamp
,第二部分是 CRG****
常量,第三部分是 PRODQwe*******
常量,第四部分是 cguid
,第五部分是 params
。因此算法可以描述为:
sign = md5(timeStamp + "CRG****PRODQwe*******" + cguid + params)
根据抓包的请求中的参数进行计算,验证通过。到这里想要的东西就算是逆向完了。
想法
说实话整个逆向过程还是显得相当小白,很多地方都懒得去看,能逆出来纯粹是因为信息很多。但其实这样也挺高效,控制流分析不出来就尝试看看数据流。作为第一次完整的逆向体验,当真正逆出来的那一瞬间整个人还是非常兴奋的。中间卡了半天以至于都再去想找人帮忙,但一听说要收四位数的价钱,想了想还是自己做了。