某通信工具收费表情安全性研究
免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负
本文的阅读对象是具有一定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 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原创奖励计划,未经许可禁止转载