违法应用fulao2取证分析

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

vip破解

adb install -r -t fulao2.apk 通过jadx查询已经被混淆

image-20210419212413059

hookEvent.js实现trace系统框架库android.view.View快速定位关键代码,trace所有的mOnClickListener,hook它们的onClick函数,实现点到哪里,定位到哪个类的功能。

前台运行fulao2.apk后,frida -UF -l hookEvent.js 启动hook

image-20210419215916951

清晰度切换

点击切换高清标清按钮,触发了发现在q0时的com.ilulutv.fulao2.film.l$t类,根据获取的类名进入jadx中搜索t,实现bool判断,下面我们手动将内存中的q0改成true。

image-20210419214658427

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的对象实例

image-20210419214133698

1
plugin wallbreaker objectdump --fullname 0x2406  拿到内存中的对象数据

image-20210419214241612

通过内存漫游修改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的值

image-20210419215307695

这样就实现了标清切换高清的功能,破解了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类方法

image-20210419220749728

通过jadx查看这两个类方法

image-20210419221125869

image-20210419221042903

由于com.ilulutv.fulao2.film.l$s和之前的com.ilulutv.fulao2.film.l$t类似,都是以q0判断,不过没有生效,现在关注com.ilulutv.fulao2.film.l$m中的OnClick里的i方法

image-20210419221328804

查看jadx的i方法

image-20210419221417228

进入g()方法

image-20210419221924238

通过hook androidx.fragment.app.Fragment.g方法,点击切换高清1的线路按钮,触发并返回了调用栈

image-20210419222111488

登录抓包

frida -UF -l hookSocket.js -o login.txt 所有内容包括手机号全部加密,除了一些请求头,gzip协议头关键字是1f 8b ,包括视频 图片都是加密的

image-20210419231251449

图片下载

1
2
android hooking search classes ImageView
plugin wallbreaker objectsearch android.widget.ImageView

image-20210419232424458

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

image-20210419232701953

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是流式图片展示的框架

image-20210419233511403

通过jadx搜索com.ilulutv.fulao2.other.helper.glide.b.a,decodeByteArray应该是解密开始了,返回b2应该就是明文。

image-20210419233832484

开始hook Base64系统库,因为系统库不可能被混淆,下拉加载图片发现确实经过了android.util.Base64.encodeToString

1
android hooking watch class_method android.util.Base64.encodeToString --dump-args --dump-backtrace --dump-return

image-20210419234203880

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)

image-20210419235827791

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;
}
})
}

image-20210420000856134

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收发包

image-20210420102356732

image-20210421195745321

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),可以看到其他协议的解密也通过这个方法

image-20210420194546464

由于返回是[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==
*/
}

image-20210421202105675image-20210421202624855

frida -UF -l fulao2.js 下拉加载图片

image-20210421202954986

查看加密方式com.ilulutv.fulao2.other.i.b.b

image-20210421203135967

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)

将比较耗性能的加解密计算放到电脑端处理,减少了手机端的资源损耗,实现脱机处理。抓包后直接使用以上算法解码。

image-20210421205216372

查看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

image-20210421211014635

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;
}
})
}

image-20210421211443291

多线程保存

创建线程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

image-20210421213211293

而CipherCore又加载了cipher-lib的so库

image-20210421213239499

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中有哪些导出函数

image-20210421213508844

其中的getString对应了private static native String getString(String str);

image-20210421213823826

1
cp libcipher-lib.so /sdcard/Download/  取出so后丢到IDA中分析

image-20210421215733176

通过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

image-20210421220940662

so层再次调用java层方法AESEncryptor

1
android hooking watch class net.idik.lib.cipher.so.encrypt.AESEncryptor 下拉图片加载,虽然到native进行转化,但是啥也没干,重新从java层调用加解密

image-20210421221236514

由于每次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层

image-20210421221625985

文章作者: J
文章链接: http://onejane.github.io/2021/04/15/违法应用fulao2取证分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏