本文的阅读对象是具有一定 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 入门安装教程具体可以看这里 和
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 了, 安全起见, 此时可以提示环境异常或退出.
来源: http://www.tuicool.com/articles/JVFRJfz