x猫免费小说安卓逆向分析 难度(♥)
1. 查壳
我这边使用的是ApkTool, 拖进去显示未加壳。
2. 抓包分析
POST https://xiaoshuo.xxxx.com/api/v1/login/index HTTP/1.1
net-env: 1
channel: qm-cbcpa056_wm
is-white: 0
platform: android
app-version: 50500
reg:
application-id: com.xxxx.reader
AUTHORIZATION:
qm-params: cLGZ4CG-uloLp3U1paHWHzpzpzpzpzpztqRTgeKQNlN2N5UzpzpztqpzpzpT4hG2Nhk-A3HjHSRUmqF5A5HwgI9wgI9wgaMMNeoTth0lgIHQpzpzp5Uzpzpz4TKMpqFnAIg5taG-pCp14lfQmqF5A5HLgIHwgh0LNh9nge4wgI0rNe9eNz4lpI0E4zFENyfepzpzNhsxgIfrNzFwgIkTgIox4egegIx5Nq4Lghgwg5HjHzUx4LHWH-oIATgEATFeA-fwA-FrAT0MH5w5OE2etCp2O5HWHTK7g3rwH5w5u_GUOEk2paU1paHWH-kUhRkokTfEBzn_k-N4OIpeczfYpExjNToWuS27uEuYhzpA3hGHm-rEgMNym_-rR-unhTxnRhHL3LQ53C2QpqnQ3ynHNhurtMJSN-QipTsiAyjLBEoAc205taG1BqR1HTZ5gefLNhgngIKrNh0rghOrH5w5BqJ-pqw5A5GFmCx2BaHjHSuj45U1BqR1HTZ5gefLNhgngIKrNh0rghOrH5w5uln5tCR1paHWHTgUgTfegh9rAIfnAI0EAaHjHzNjmqR7uaU1paHWHzN24lp54qk-gqHEglfM4qg5taG5Ozo7paHWHzuDBlujp3HjHzJxmqF5A5H56F==
sign: b27fb3051c9ee919bb7143a8f856a31f
QM-it: 1606239365
QM-ii: 1901732340
no-permiss: 0
User-Agent: webviewversion/50500
Content-Type: application/x-www-form-urlencoded
Content-Length: 124
Host: xiaoshuo.wtzw.com
Connection: Keep-Alive
Accept-Encoding: gzip
cancell_check=1&encrypt_phone=ghgUNIFENeKrNhf=&gender=2&open_push=1&type=1&verify=1111&sign=3a7c6107895fe47d8cfb7647d21a8c86
发现如果我们想要登录, 至少需要解决 qm-params
,encrypt_phone
,sign
由于没有加固, 那么我们从提交数据开始.
3. sign
jadx反编译, 一些魅族华为的包都不用看, 就看可以的。 可以定位到
f.f.e.b.c.a.c这个class.
if (this.f32692a.a(url.host())) {
HashMap<String, String> b2 = this.f32692a.b();
TreeMap treeMap = new TreeMap();
if (b2 != null) {
for (String str : b2.keySet()) {
String str2 = b2.get(str);
if (str2 != null) {
newBuilder.addHeader(str, str2);
treeMap.put(str, str2);
}
}
}
String y = f.f.e.b.d.a.m().y();
if (!TextUtils.isEmpty(y)) {
newBuilder.addHeader("qm-params", y);
treeMap.put("qm-params", y);
}
if (!treeMap.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (String str3 : treeMap.keySet()) {
sb.append(str3);
sb.append("=");
sb.append((String) treeMap.get(str3));
}
newBuilder.addHeader("sign", Encryption.sign(sb.toString()));
}
try {
if (!(this.f32693b == null || (m = this.f32693b.m()) == null || m.length <= 0)) {
for (String str4 : m) {
String string = this.f32693b.getString(str4, "");
if (!TextUtils.isEmpty(str4) && !TextUtils.isEmpty(string)) {
newBuilder.addHeader(TextUtil.appendStrings("QM-", str4), string);
}
}
}
newBuilder.addHeader("no-permiss", a());
} catch (Exception unused) {
}
向下跟踪
package com.qimao.qmsdk.tools.encryption;
import com.km.encryption.api.Security;
import java.io.UnsupportedEncodingException;
import org.geometerplus.fbreader.book.Encoding;
public class Encryption {
public static void init() {
}
public static String sign(String str) {
try {
return Security.sign(str.getBytes(Encoding.UTF8_NATIVE));
} catch (UnsupportedEncodingException unused) {
return "";
}
}
}
最后跟到
package com.km.encryption.api;
import android.content.Context;
import com.km.encryption.generator.KeyGenerator;
public class Security {
public static void a(Context context, String str) {
KeyGenerator.assetManager = context.getAssets();
KeyGenerator.key = str;
KeyGenerator.context = context;
}
public static native byte[] decode(String str);
public static native String decrypt(String str, String str2);
public static native String encrypt(String str, String str2);
public static native void init();
public static native String sign(byte[] bArr);
public static native SecurityEntity token(String str);
}
然后使用objection来hook一下。
android hooking watch class_method com.km.encryption.api.Security.sign --dum
p-args --dump-return --dump-backtrace
发现的确是这边,但是objection不能显示byte[]类型,因此需要自己写frida代码
function byteToString(arr) {
if(typeof arr === 'string') {
return arr;
}
var str = '',
_arr = arr;
for(var i = 0; i < _arr.length; i++) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if(v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for(var st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
str += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
str += String.fromCharCode(_arr[i]);
}
}
return str;
}
function startHook() {
Java.perform(function() {
var Security = Java.use('com.km.encryption.api.Security'); //要hook的类名完整路径
Security.sign.implementation = function(x) { // 重写要hook的方法Sign,当有多个重名函数时需要重载,function括号为函数的参数个数
console.log("---> Security.Sign Start")
console.log("---> Arg[0]:" + byteToString(x) );
console.log("---> Result:" + this.sign(x));
console.log("---> Security.Sign end")
return this.sign(x)
};
})
}
setImmediate(startHook)
于是乎执行命令
frida -U -f com.kmxs.reader -l demo01.js --no-pause
---> Security.Sign Start
---> Arg[0]:cancell_check=1encrypt_phone=ghgUNIFENeKrAIK=gender=2open_push=1type=1verify=1111
---> Result:1b75f39c2c27315b38ef5926a4beaac3
---> Security.Sign end
---> Security.Sign Start
---> Arg[0]:cancell_check=1encrypt_phone=ghgUNIFENeKrAIK=gender=2open_push=1type=1verify=1111
---> Result:1b75f39c2c27315b38ef5926a4beaac3
---> Security.Sign end
---> Security.Sign Start
---> Arg[0]:cancell_check=1encrypt_phone=ghgUNIFENeKrAIK=gender=2open_push=1type=1verify=1111
---> Result:1b75f39c2c27315b38ef5926a4beaac3
---> Security.Sign end
此时,我们已经用frida hook的方式, 可以拿到提交参数和返回结果.此时就可以衍生出方案1:Frida -rpc
1. Frida-rpc
参考肉丝姐姐的代码
原文地址:https://www.anquanke.com/post/id/195215
可以把我们的js文件改成
function byteToString(arr) {
if(typeof arr === 'string') {
return arr;
}
var str = '',
_arr = arr;
for(var i = 0; i < _arr.length; i++) {
var one = _arr[i].toString(2),
v = one.match(/^1+?(?=0)/);
if(v && one.length == 8) {
var bytesLength = v[0].length;
var store = _arr[i].toString(2).slice(7 - bytesLength);
for(var st = 1; st < bytesLength; st++) {
store += _arr[st + i].toString(2).slice(2);
}
str += String.fromCharCode(parseInt(store, 2));
i += bytesLength - 1;
} else {
str += String.fromCharCode(_arr[i]);
}
}
return str;
}
function stringToByte(str) {
var bytes = new Array();
var len, c;
len = str.length;
for(var i = 0; i < len; i++) {
c = str.charCodeAt(i);
if(c >= 0x010000 && c <= 0x10FFFF) {
bytes.push(((c >> 18) & 0x07) | 0xF0);
bytes.push(((c >> 12) & 0x3F) | 0x80);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if(c >= 0x000800 && c <= 0x00FFFF) {
bytes.push(((c >> 12) & 0x0F) | 0xE0);
bytes.push(((c >> 6) & 0x3F) | 0x80);
bytes.push((c & 0x3F) | 0x80);
} else if(c >= 0x000080 && c <= 0x0007FF) {
bytes.push(((c >> 6) & 0x1F) | 0xC0);
bytes.push((c & 0x3F) | 0x80);
} else {
bytes.push(c & 0xFF);
}
}
return bytes;
}
function getsign(args){
var result = ''
Java.perform(function(){
var Security = Java.use('com.km.encryption.api.Security');
result = Security.sign(stringToByte(args));
console.log("rpc getSign:" + result);
})
return result;
}
rpc.exports = {
sign : getsign
}
//setImmediate(startHook)
创建一个python文件
import frida
def on_message(message, data):
if message['type'] == 'send':
print(message['payload'])
elif message['type'] == 'error':
print(message['stack'])
source = ""
with open('demo01.js',encoding= 'utf-8') as f:
source = f.read()
print(source)
session = frida.get_usb_device().attach('com.kmxs.reader')
script = session.create_script(source)
script.on('message', on_message)
script.load()
print(script.exports.sign('cancell_check=1encrypt_phone=ghgUNIFENeKrAIK=gender=2open_push=1type=1verify=1111'))
session.detach()
测试没问题以后, 我们配合flask 编写一个api接口
from flask import Flask,jsonify,request
import frida
app = Flask(__name__)
tasks = {
'taskId':1,
'encryptData' : u'xiaopang',
'sign':'sign'
}
def on_message(message, data):
if message['type'] == 'send':
print(message['payload'])
elif message['type'] == 'error':
print(message['stack'])
source = ""
with open('demo01.js',encoding= 'utf-8') as f:
source = f.read()
print(source)
session = frida.get_usb_device().attach('com.kmxs.reader')
script = session.create_script(source)
script.on('message', on_message)
script.load()
@app.route('/getSign',methods=['POST'])
def index():
if request.method == 'POST':
encryptData = request.form.get('encryptData')
data = tasks
data['sign'] = script.exports.sign(encryptData)
data['encryptData'] = encryptData
return jsonify({'data':tasks})
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True)
此时其他的其实就和这个sign差不多了, 找到位置,rpc就好了!能用就好!