篇幅有限 完整内容及源码关注公众号:ReverseCode,发送 冲
vip破解 adb install -r -t fulao2.apk 通过jadx查询已经被混淆
hookEvent.js实现trace系统框架库android.view.View快速定位关键代码,trace所有的mOnClickListener,hook它们的onClick函数,实现点到哪里,定位到哪个类的功能。
前台运行fulao2.apk后,frida -UF -l hookEvent.js 启动hook
清晰度切换 点击切换高清标清按钮,触发了发现在q0时的com.ilulutv.fulao2.film.l$t类,根据获取的类名进入jadx中搜索t,实现bool判断,下面我们手动将内存中的q0改成true。
1 2 3 4 5 pyenv local 3.8.0 objection -g com.ilulutv.fulao2 explore -P ~/.objection/plugins plugin wallbreaker classdump --fullname com.ilulutv.fulao2.film.l\$t plugin wallbreaker objectsearch com.ilulutv.fulao2.film.l\$t plugin wallbreaker objectdump --fullname 0x26a2 获取到com.ilulutv.fulao2.film.l的对象实例
1 plugin wallbreaker objectdump --fullname 0x2406 拿到内存中的对象数据
通过内存漫游修改q0的False的默认值,frida -UF -l fulao2.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function hookq0(){ Java.perform(function(){ Java.choose("com.ilulutv.fulao2.film.l",{ onMatch:function(ins){ if(ins.e0.value){ ins.q0.value = true /* if(ins.e0.value.toString().indexOf("宝宝睡")>0){ console.log("e0 value is :", ins.e0.value); //ins.q0.value = Java.use("java.lang.Boolean").\$new("true"); //ins.q0.value = true } */ } },onComplete:function(){ console.log("search complete!") } }) }) } setImmediate(hookq0)
android hooking search classes Boolean 获取Boolean类全路径java.lang.Boolean
重新调用plugin wallbreaker objectdump –fullname 0x2406 查看q0的值
这样就实现了标清切换高清的功能,破解了vip的切换视频清晰度。这种基于本地代码判断容易破解,基于服务器判断就只能根据逻辑漏洞判断。可以通过setInterval实现不断在内存循环调用,将内存中所有实例的q0改成true。
线路切换 frida -UF -l hookEvent.js attach模式
frida -U -f com.ilulutv.fulao2 -l hookEvent.js –no-pause spawn模式一开始把所有View的OnClick类hook上,不用从内存中枚举
点击线路切换按钮,触发了com.ilulutv.fulao2.film.l$s和com.ilulutv.fulao2.film.l$m类方法
通过jadx查看这两个类方法
由于com.ilulutv.fulao2.film.l$s和之前的com.ilulutv.fulao2.film.l$t类似,都是以q0判断,不过没有生效,现在关注com.ilulutv.fulao2.film.l$m中的OnClick里的i方法
查看jadx的i方法
进入g()方法
通过hook androidx.fragment.app.Fragment.g方法,点击切换高清1的线路按钮,触发并返回了调用栈
登录抓包 frida -UF -l hookSocket.js -o login.txt 所有内容包括手机号全部加密,除了一些请求头,gzip协议头关键字是1f 8b ,包括视频 图片都是加密的
图片下载 1 2 android hooking search classes ImageView plugin wallbreaker objectsearch android.widget.ImageView
1 2 3 4 plugin wallbreaker classsearch bitmap android hooking search classes bitmap 将所有相关类保存到file.txt中,sed -i -e 's/^/android hooking watch class /' file.txt objection -g com.ilulutv.fulao2 explore -c file.txt 批量hook plugin wallbreaker objectsearch android.graphics.Bitmap
Java.choose属于内存的搜刮,将现有内存的Bigmap对象实例保存,基于hook的话可以将未来持续增长的setInterval定时保存一份内存中的图片setInterval(main,5*1000)
1 android hooking watch class_method android.graphics.BitmapFactory.decodeByteArray --dump-args --dump-backtrace --dump-return 通过批量hook拿到下拉触发的方法进行hook打印堆栈,glide是流式图片展示的框架
通过jadx搜索com.ilulutv.fulao2.other.helper.glide.b.a,decodeByteArray应该是解密开始了,返回b2应该就是明文。
开始hook Base64系统库,因为系统库不可能被混淆,下拉加载图片发现确实经过了android.util.Base64.encodeToString
1 android hooking watch class_method android.util.Base64.encodeToString --dump-args --dump-backtrace --dump-return
frida -UF -l fulao2.js -o /root/raw.txt 通过hook发现Base64.encodeToString得到的和SSLOutputStream得到的数据流一致
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 function hookImageByteCiphered(){ # 传输中的流 Java.perform(function(){ Java.use("android.util.Base64").encodeToString.overload('[B', 'int').implementation = function(bytearray,int){ var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("IMAGE DATA:bytearray,int=>",ByteString.of(bytearray).hex(),int) var result = this.encodeToString(bytearray,int) return result; } }) } function hook_SSLsocketandroid8(){ Java.perform(function(){ console.log("hook_SSLsocket") Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket\$SSLOutputStream").write.overload('[B', 'int', 'int').implementation = function(bytearry,int1,int2){ var result = this.write(bytearry,int1,int2); console.log("HTTPS write result,bytearry,int1,int2=>",result,bytearry,int1,int2) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("HTTPS bytearray contents=>", ByteString.of(bytearry).hex()) //console.log(jhexdump(bytearry,int1,int2)); console.log(jhexdump(bytearry)); return result; } Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket\$SSLInputStream").read.overload('[B', 'int', 'int').implementation = function(bytearry,int1,int2){ var result = this.read(bytearry,int1,int2); console.log("HTTPS read result,bytearry,int1,int2=>",result,bytearry,int1,int2) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("HTTPS bytearray contents=>", ByteString.of(bytearry).hex()) //console.log(jhexdump(bytearry,int1,int2)); //console.log(jhexdump(bytearry)); return result; } }) }
说明com.ilulutv.fulao2.other.i.b.a((ByteBuffer) obj)确实是https传输的流,也是加密前的流,ffd8ff 是png文件头,通过后面的代码实现解密。
1 2 android hooking search classes BitmapFactory android hooking watch class_method android.graphics.BitmapFactory.decodeByteArray --dump-args --dump-backtrace --dump-return 开始hook BitmapFactory.decodeByteArray(b2, 0, b2.length)
frida -UF -l fulao2.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hookImage(){ Java.perform(function(){ Java.use("android.graphics.BitmapFactory").decodeByteArray.overload('[B', 'int', 'int', 'android.graphics.BitmapFactory\$Options') .implementation = function(data, offset, length, opts){ var result = this.decodeByteArray(data, offset, length, opts); var ByteString = Java.use("com.android.okhttp.okio.ByteString"); //console.log("data, offset, length, opts=>",data, offset, length, opts) //console.log("IMAGE DATA:bytearray,int=>",ByteString.of(data).hex()) var path = "/sdcard/Download/tmp/"+guid()+".jpg" console.log("path=> ",path) var file = Java.use("java.io.File").\$new(path) var fos = Java.use("java.io.FileOutputStream").\$new(file); fos.write(data); fos.close(); fos.close(); return result; } }) }
python调用保存 fulao2.js 将解密后的字节数组发送给python,二进制写入图片
1 2 3 4 5 6 7 8 9 10 function hookImage(){ Java.perform(function(){ Java.use("android.graphics.BitmapFactory").decodeByteArray.overload('[B', 'int', 'int', 'android.graphics.BitmapFactory\$Options') .implementation = function(data, offset, length, opts){ var result = this.decodeByteArray(data, offset, length, opts); var ByteString = Java.use("com.android.okhttp.okio.ByteString"); send(data) return result; } }) }
调用fulao2.js发送到本机
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 import frida import json import time import uuid import base64 import re def my_message_handler(message, payload): print(message) print(payload) if message["type"] == "send": print(message["payload"]) #image = re.findall("(-?\d+)", message["payload"]) image = message["payload"] intArr = [] for m in image: ival = int(m) if ival < 0: ival += 256 intArr.append(ival) bs = bytes(intArr) fileName = str(uuid.uuid1()) + ".jpg" f = open(fileName,'wb') f.write(bs) f.close() device = frida.get_usb_device() target = device.get_frontmost_application() session = device.attach(target.pid) # 加载脚本 with open("fulao2.js") as f: script = session.create_script(f.read()) script.on("message" , my_message_handler) #调用错误处理 script.load() # 脚本会持续运行等待输入 input()
不能够以战术的勤奋,掩盖战略的懒惰。
大多数人努力的程度还谈不上拼天分。
脱机 二进制写入图片 1 2 objection -g com.ilulutv.fulao2 explore -P ~/.objection/plugins android hooking watch class_method android.graphics.BitmapFactory.decodeByteArray --dump-args --dump-backtrace --dump-return hook解密方法,下拉获取调用栈
jadx搜索com.ilulutv.fulao2.other.helper.glide.b.a,抓包抓到的二进制数据流是encodeToString之前的b.a返回的数据,可以将协议中内容直接解密,不需要app参与,可以直接hook收发包
1 android hooking watch class_method com.ilulutv.fulao2.other.i.b.a --dump-args --dump-backtrace --dump-return 下拉加载图片,关注Hooking com.ilulutv.fulao2.other.i.b.a(java.nio.ByteBuffer),可以看到其他协议的解密也通过这个方法
由于返回是[object Object],看不出结果还是通过hook实现吧。尽量不要用hookImageByteCiphered,因为其他类可能也用到了Base64
1 2 3 4 5 6 7 8 9 10 function hookImageByteCiphered() { Java.perform(function () { Java.use("android.util.Base64").encodeToString.overload('[B', 'int').implementation = function (bytearray, int) { var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log("IMAGE DATA:bytearray,int=>", ByteString.of(bytearray).hex(), int) var result = this.encodeToString(bytearray, int) return result; } }) }
通过hook ByteBuffer获取com.ilulutv.fulao2.other.i.b.a 的入参实现hook com.ilulutv.fulao2.other.i.b.a((ByteBuffer) obj)
1 2 3 4 5 6 7 8 9 10 11 12 13 function hookByteBuffer() { Java.perform(function () { Java.use("com.ilulutv.fulao2.other.i.b").a.overload('java.nio.ByteBuffer').implementation = function (bf) { var result = this.a(bf) // [b //var gson = Java.use('com.google.gson.Gson') //console.log("result is => ",result); send(result) //console.log( gson.$new().toJson(result)) return result; } }) }
frida -UF -l fulao2.js 下拉显示图片,将打印返回的字节数组的结果,通过python实现解密后结果用于脱机处理。
通过hook byte[] b2 = com.ilulutv.fulao2.other.i.b.b(decode, Base64.decode(bytes2, 0), encodeToString);
中的com.ilulutv.fulao2.other.i.b.b
android hooking list class_methods com.ilulutv.fulao2.other.i.b 获取需要hook的方法
android hooking watch class_method net.idik.lib.cipher.so.CipherClient.decodeImgKey –dump-args –dump-backtrace –dump-return 获取hook的返回
android hooking search classes base64 获取android.util.Base64方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function hookdecodeimgkey() { Java.perform(function () { var base64 = Java.use("android.util.Base64") Java.use("com.ilulutv.fulao2.other.i.b").b.overload('[B', '[B', 'java.lang.String').implementation = function (key, iv, image) { var result = this.b(key, iv, image); console.log("key", base64.encodeToString(key, 0)); console.log("iv", base64.encodeToString(iv, 0)); return result; } }) /* key svOEKGb5WD0ezmHE4FXCVQ== iv 4B7eYzHTevzHvgVZfWVNIg== */ }
frida -UF -l fulao2.js 下拉加载图片
查看加密方式com.ilulutv.fulao2.other.i.b.b
python实现解密 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pycrypto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def IMGdecrypt(bytearray): imgkey = base64.decodebytes( bytes("svOEKGb5WD0ezmHE4FXCVQ==", encoding='utf8')) imgiv = base64.decodebytes( bytes("4B7eYzHTevzHvgVZfWVNIg==", encoding='utf8')) cipher = AES.new(imgkey, AES.MODE_CBC, imgiv) # enStr += (len(enStr) % 4)*"=" # decryptByts = base64.urlsafe_b64decode(enStr) msg = cipher.decrypt(bytearray) def unpad(s): return s[0:-s[-1]] return unpad(msg) # 拿到数据后Base64解密 bs = IMGdecrypt(bs)
将比较耗性能的加解密计算放到电脑端处理,减少了手机端的资源损耗,实现脱机处理。抓包后直接使用以上算法解码。
查看BitmapFactory.decodeByteArray返回的类型
1 2 android hooking search classes Bitmap android hooking list class_methods android.graphics.Bitmap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function getObjClassName(obj) { if (!jclazz) { var jclazz = Java.use("java.lang.Class"); } if (!jobj) { var jobj = Java.use("java.lang.Object"); } return jclazz.getName.call(jobj.getClass.call(obj)); } function hookImage(){ Java.perform(function(){ Java.use("android.graphics.BitmapFactory").decodeByteArray.overload('[B', 'int', 'int', 'android.graphics.BitmapFactory$Options') .implementation = function(data, offset, length, opts){ var result = this.decodeByteArray(data, offset, length, opts); var ByteString = Java.use("com.android.okhttp.okio.ByteString"); var gson = Java.use('com.google.gson.Gson') console.log("result is =>",gson.$new().toJson(result)) // 打印BitmapFactory对象属性,说明BitmapFactory.decodeByteArray返回对象 console.log("className is =>",getObjClassName(result)) console.log('Object.getOwnPropertyNames()=>',Object.getOwnPropertyNames(result.$className)) return result; } }) }
安卓保存图片 1 2 android hooking search classes CompressFormat plugin wallbreaker classdump --fullname android.graphics.Bitmap$CompressFormat
frida -UF -l fulao2.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function hookImage(){ Java.perform(function(){ Java.use("android.graphics.BitmapFactory").decodeByteArray.overload('[B', 'int', 'int', 'android.graphics.BitmapFactory$Options') .implementation = function(data, offset, length, opts){ var result = this.decodeByteArray(data, offset, length, opts); var ByteString = Java.use("com.android.okhttp.okio.ByteString"); var gson = Java.use('com.google.gson.Gson') var path = "/sdcard/Download/tmp/" + guid() + ".jpg" console.log("path=> ", path) var file = Java.use("java.io.File").$new(path) var fos = Java.use("java.io.FileOutputStream").$new(file); result.compress(Java.use("android.graphics.Bitmap$CompressFormat").JPEG.value, 100, fos) console.log("success!") fos.flush(); fos.close(); return result; } }) }
多线程保存 创建线程com.onejane.runnable,android hooking search classes onejane
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 function hookImage() { Java.perform(function () { var Runnable = Java.use("java.lang.Runnable"); var saveImg = Java.registerClass({ name: "com.onejane.runnable", implements: [Runnable], fields: { bm: "android.graphics.Bitmap", }, methods: { $init: [{ returnType: "void", argumentTypes: ["android.graphics.Bitmap"], implementation: function (bitmap) { this.bm.value = bitmap; } }], run: function () { var path = "/sdcard/Download/tmp/" + guid() + ".jpg" console.log("path=> ", path) var file = Java.use("java.io.File").$new(path) var fos = Java.use("java.io.FileOutputStream").$new(file); this.bm.value.compress(Java.use("android.graphics.Bitmap$CompressFormat").JPEG.value, 100, fos) console.log("success!") fos.flush(); fos.close(); } } }); Java.use("android.graphics.BitmapFactory").decodeByteArray.overload('[B', 'int', 'int', 'android.graphics.BitmapFactory$Options').implementation = function (data, offset, length, opts) { var result = this.decodeByteArray(data, offset, length, opts); var ByteString = Java.use("com.android.okhttp.okio.ByteString"); //var gson = Java.use('com.google.gson.Gson') //send(data) //send(gson.$new().toJson(data)) //console.log("data, offset, length, opts=>",data, offset, length, opts) //console.log("IMAGE DATA:bytearray,int=>",ByteString.of(data).hex()) /* var path = "/sdcard/Download/tmp/"+guid()+".jpg" console.log("path=> ",path) var file = Java.use("java.io.File").$new(path) var fos = Java.use("java.io.FileOutputStream").$new(file); fos.write(data); fos.flush(); fos.close(); */ /*var gson = Java.use('com.google.gson.Gson') console.log("result is =>",gson.$new().toJson(result)) # 打印BitmapFactory对象属性,说明BitmapFactory.decodeByteArray返回 console.log("className is =>",getObjClassName(result)) console.log('Object.getOwnPropertyNames()=>',Object.getOwnPropertyNames(result.$className))*/ /* var path = "/sdcard/Download/tmp/" + guid() + ".jpg" console.log("path=> ", path) var file = Java.use("java.io.File").$new(path) var fos = Java.use("java.io.FileOutputStream").$new(file); result.compress(Java.use("android.graphics.Bitmap$CompressFormat").JPEG.value, 100, fos) console.log("success!") fos.flush(); fos.close(); */ var runnable = saveImg.$new(result); runnable.run() return result; } }) }
so分析 CipherClient类中所有的返回都是CipherCore.get
而CipherCore又加载了cipher-lib的so库
1 2 3 4 objection -g com.ilulutv.fulao2 explore -P ~/.objection/plugins memory list modules 搜索cipher-lib ls -alt /data/app/com.ilulutv.fulao2-6tvMrrptF1h1A4NvQbV85A==/lib/arm/ memory list exports libcipher-lib.so 查看该so中有哪些导出函数
其中的getString对应了private static native String getString(String str);
1 cp libcipher-lib.so /sdcard/Download/ 取出so后丢到IDA中分析
通过jnitrace trace下所有native的执行流。
1 2 3 4 ./data/local/tmp/fs1428arm64 pyenv local 3.8.5 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple jnitrace jnitrace -m attach -l libcipher-lib.so com.ilulutv.fulao2 下拉加载图片 查看trace的调用栈,默认是spawn
so层再次调用java层方法AESEncryptor
1 android hooking watch class net.idik.lib.cipher.so.encrypt.AESEncryptor 下拉图片加载,虽然到native进行转化,但是啥也没干,重新从java层调用加解密
由于每次hook时app总是崩掉,objection在app启动时直接执行hook方法
1 objection -g com.ilulutv.fulao2 explore -P ~/.objection/plugins -s "android hooking watch class_method net.idik.lib.cipher.so.encrypt.AESEncryptor.decrypt --dump-args --dump-backtrace --dump-return" 堆栈说明确实从native层到了java层