freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

某通信工具收费表情安全性研究
2019-10-03 13:00:30
所属地 广东省

免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负

本文的阅读对象是具有一定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/88106511https://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

555546.png

然后【某通信工具】会自动重新启动, 待【某通信工具】启动后,点击我-》表情-》朋友表情 下面滚动的图片里选择 “XX表情付费篇”,选择你喜欢的表情包,接着点击一个你喜欢的表情 预览,此时 ,frida 会将 动图表情 的没一帧 都保存到 手机的/sdcard/mmface/里。确认表情循环显示完毕后,关闭表情预览 窗口。

4、执行:adb pull /sdcard/mmface/ mmface/

mmface的表情帧全部 拉取到 电脑上。我们可以手动 删除重复的 图片帧(推荐),也可以自动删除重复的图片帧(下面会讲)。最后 将这些图片帧合成gif

q1.png

去重后

q2.png

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。

q5.png

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原创奖励计划,未经许可禁止转载

# Android开发 # Frida # 表情包
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者