免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负
本文的阅读对象是具有一定Android开发经验的开发人员以及想学习安全技术的吃瓜群众,这里只上核心代码。
在【某通信工具】表情里,我特别喜欢“乖巧**”系列的表情,简明清新以及萌萌哒的设计风格,颇得年轻人的喜爱。
数月前在【某通信工具】“XX表情付费篇” 页面看中了“乖巧**6” 里的一个表情,发现要付费 1元才能使用。。。是的,就一块钱,但对于“抠门”的我来说,肯定不愿意掏的。所以就没买,直接关闭了付费窗口。
而前两天,在XXX技术群里,看见某大佬连发了好几个“乖巧**6” 的表情,我问他是不是买的表情。他说“NO”,这些是他使用frida提取出来的表情。我就比较好奇,frida是什么?以及他是如何提取这些表情的?经过几天研究,终于弄懂了这一切,特撰此文。
本人环境为windows,linux 类同。
1、安装python (内含pip)、frida。frida 是一个动态hook框架,支持hook java代码和native代码(so)。它主要提供了功能简单的python接口和功能丰富的js接口。官网地址:https://www.frida.re 。 python 和frida 入门安装教程具体可以看这里https://blog.csdn.net/tabactivity/article/details/88106511和 https://blog.csdn.net/tabactivity/article/details/88130653
2、准备一台root 的安卓手机,并安装好【某通信工具】App
1、我们要hook【某通信工具】付费表情,我们必须先知道 这个表情的ImageView的ID。
打开【某通信工具】-》我-》表情-》朋友表情 下面滚动的图片里选择 “XX表情付费篇”
然后选择 你喜欢的表情,并点击预览
2、启动 Android Device Monitor ,点击 Dump View Hierarchy for UI Automator ,鼠标放到表情处。如下图
我们就知道了用来表情的ImageView的Id是 :com.tencent.mm:id/bg6 ,也就是com.tencent.mm包里bg6。我们要做的就是把ImageView里的图片提取出来,可以通过hook ImageView 的onDraw(Canvas canavs)事件,将需要绘制到canvas内容,也同样绘制到我们用Bitmap创建的Canvas上,然后将Bitmap保存png文件。随着onDraw 一次一次调用,动图表情的多个png图片帧就保存下来了。最后,我们只需要将png图片合成 gif 就能在【某通信工具】里随意发了。
1、启动frida
2、编写frida脚本代码。wxface.py
import frida
import sys
import io
import os
import time
device = frida.get_usb_device()
pid = device.spawn(["com.tencent.mm"])
session = device.attach(pid)
src_tencent_mm = """
Java.perform(function(){
var ImageView = Java.use("android.widget.ImageView");
var Bitmap = Java.use("android.graphics.Bitmap");
var Bitmap_Config = Java.use("android.graphics.Bitmap$Config");
//var bufBitmap = Bitmap$new(394, 394, 5);
var bitmap_va = Bitmap_Config.ARGB_8888.value;
console.log("bitmap_va = " + bitmap_va);
var Canvas = Java.use("android.graphics.Canvas");
console.log("Canvas = " + Canvas);
var ByteArrayOutputStream = Java.use("java.io.ByteArrayOutputStream");
console.log("ByteArrayOutputStream = " + ByteArrayOutputStream);
var CompressFormat = Java.use("android.graphics.Bitmap$CompressFormat");
console.log("CompressFormat value= " + CompressFormat.PNG.value);
var FileOutputStream = Java.use("java.io.FileOutputStream");
var System = Java.use("java.lang.System");
var index = 0;
var startTime = 0;
var endTime = 0;
//创建存储表情帧的 目录
var File = Java.use("java.io.File");
File.$new("/sdcard/mmface").mkdirs();
ImageView.onDraw.implementation = function(canvas){
this.onDraw(canvas);
var viewId = this.getResources().getIdentifier("bg6", "id", "com.tencent.mm");
if(this.getId() != viewId){
return;
}
console.log("ImageView onDraw.....");
if(startTime == 0){
startTime = System.currentTimeMillis();
}else{
endTime = System.currentTimeMillis();
console.log("git更新间隔为:"+(endTime - startTime));
startTime = endTime;
}
console.log("gitd draw entry! " + canvas.getWidth() + ","+ canvas.getHeight());
//将ImageView 要绘制的内容 也绘制到我们创建的 Bitmap中
var bufBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), bitmap_va);
console.log("bufBitmap = " + bufBitmap);
var tempCanvas = Canvas.$new(bufBitmap);
console.log("tempCanvas = " + tempCanvas);
this.onDraw(tempCanvas);
var bos = ByteArrayOutputStream.$new();
console.log("bos = " + bos);
console.log("bos size= " + bos.size());
bufBitmap.compress(CompressFormat.PNG.value, 100, bos);
console.log("222 bos size= " + bos.size());
var bytesss = bos.toByteArray();
console.log("bytesss length = " + bytesss.length);
var fos = FileOutputStream.$new("/sdcard/mmface/" +index +".png");
fos.write(bytesss);
fos.flush();
fos.close();
console.log("保存成功! index=" +index );
index++;
}
});
"""
#message["payload"] message为map,取出key payload 的value
def on_message(message, data):
print(message)
#time.sleep(5)
script = session.create_script(src_tencent_mm)
#设置message 回调函数为 on_message。js 调用send 就会发到 on_message
#script.on("message", on_byte_message)
script.on("message", on_message)
script.load()
device.resume(pid)
sys.stdin.read()
3、打开CMD,执行:python wxface.py
然后【某通信工具】会自动重新启动, 待【某通信工具】启动后,点击我-》表情-》朋友表情 下面滚动的图片里选择 “XX表情付费篇”,选择你喜欢的表情包,接着点击一个你喜欢的表情 预览,此时 ,frida 会将 动图表情 的没一帧 都保存到 手机的/sdcard/mmface/里。确认表情循环显示完毕后,关闭表情预览 窗口。
4、执行:adb pull /sdcard/mmface/ mmface/
mmface的表情帧全部 拉取到 电脑上。我们可以手动 删除重复的 图片帧(推荐),也可以自动删除重复的图片帧(下面会讲)。最后 将这些图片帧合成gif
去重后
5、多张图片,合成gif 。可以使用网上任意一种工具。本文直接基于python合成。编写gif.py
from PIL import Image
import os
import sys
import zlib
import imageio
#处理透明gif
def create_gif_2(image_list, gif_name):
frames = []
im_tmp = Image.open(image_list[0])
mask = Image.new("RGBA", im_tmp.size, (255, 255, 255, 0))
for image_item in image_list:
im = Image.open(image_item)
frames.append(Image.alpha_composite(mask, im));
#img = Image.new("RGBA", im.size, (255, 255, 255, 0))
first = frames.pop(0);
first.save(gif_name, save_all=True, append_images=frames, loop=0, transparency=0, duration=100,disposal=2)
#处理非透明gif
def create_gif(image_list, gif_name):
frames = []
for image_item in image_list:
im = Image.open(image_item)
alpha = im.getchannel('A')
print("alpha = ", alpha)
# Convert the image into P mode but only use 255 colors in the palette out of 256
im = im.convert('RGBA').convert('P', palette=Image.ADAPTIVE, colors=255)
# Set all pixel values below 128 to 255 , and the rest to 0
mask = Image.eval(alpha, lambda a: 255 if a <=128 else 0)
# Paste the color of index 255 and use alpha as a mask
im.paste(0, mask)
# The transparency index is 255
im.info['transparency'] = 0
frames.append(im)
#frames.append( im = Image.open(path).imread(image_item, "PNG"))
frames[0].save(gif_name, save_all=True, append_images=frames, loop=0, duration=100 )
return
def crc32(filepath):
block_size = 1024 *1024
crc = 0
fd = open(filepath, 'rb')
while True:
buffer = fd.read(block_size)
if len(buffer) == 0:
fd.close()
if sys.version_info[0] < 3 and crc < 0:
crc += 2 ** 32
return crc
crc = zlib.crc32(buffer, crc)
def filter_png(dirpath):
image_crc = []
image_files = os.listdir(dirpath)
for filename in image_files:
path = os.path.join(dirpath, filename)
if os.path.isfile(path):
find = False
crc = crc32(path)
for crc_item in image_crc:
if crc_item == crc:
find = True
break
if find == False:
image_crc.append(crc)
else:
os.remove(path)
def main(argv):
image_list = []
image_names = os.listdir(argv[1])
image_names.sort(key= lambda x:int(x[:-4]))
if len(argv) > 3 and argv[3]=="-f":
filter_png(argv[1])
for filename in image_names:
path = os.path.join(argv[1], filename)
if os.path.isfile(path):
image_list.append(path)
create_gif_2(image_list, argv[2])
if __name__ == "__main__":
main(sys.argv)
上诉gif.py 可以传递2-3个参数。
第1个为 图片帧所在的目录
第2个为保存的文件名(如果包含目录,目录必须已经存在)
第3个为可选参数-f ,设置此参数,图片帧将根据crc32 自动去重。
执行gif.py:python gif.py C:\Users\Administrator\Desktop\mmface gif/saodong.gif
确保 “当前工作目录/gif” 目录存在,像我下面,工作目录就是 xxx/py/PyTest/src/com/test,然后在该目录创建一个gif目录。然后 就会在gif目录 生成 saodong.gif。
saodong.gif效果如下。保存到手机了,到【某通信工具】里 选择发送图片就行了。
当然,我们也可以做的更人性化一点。当检测到【某通信工具】表情预览窗口关闭,就自动adb pull /sdcard/mmface mmface/,然后调用gif.py将图片帧合成gif。之后清空手机/sdcard/mmface 和电脑上的mmface/,这样 点开一个收费表情 就自动转存到电脑上了。
自己的成果被别人窃取心里的滋味肯定不好受。那问题来了 —- 如何防住上诉破解呢?
最简单有效的方法是特征码检查。在APK运行时,我们可以读取/proc/self/maps得到当前进程的内存映射关系,检查映射里是否包含 “frida” 字符,如果有,我们就提示用户当前运行环境异常,并退出。
例如:在android中调用so,so里执行以下代码
char line[512];
FILE* fp;
fp = fopen("/proc/self/maps", "r");
if(!fp){
//打开proc/self/maps 失败
return -1;
}
while (fgets(line, 512, fp)) {
if (strstr(line, "frida")) {
//检测到了frida,执行退出操作
exit(0);
}
}
fclose(fp);
return 0;
在电脑上,我们也可以通过adb查看。以【某通信工具】为例子,我们看下【某通信工具】的进程是否包含frida:
不出意外,【某通信工具】的进程已经映射了frida 了,安全起见,此时可以提示环境异常或退出。
*本文原创作者:ab6326795,本文属于FreeBuf原创奖励计划,未经许可禁止转载