首页
论坛
课程
招聘
[原创]初识Frida--Android逆向之Java层hook (二)
2018-6-5 17:40 15261

[原创]初识Frida--Android逆向之Java层hook (二)

2018-6-5 17:40
15261

初识Frida--Android逆向之Java层hook (二)

目录

 

今天继续一个新的示例,同样采用CTF作为例子,难度稍微加大了一点,如果对Frida基本的使用还不是很了解,建议先看看之前的文章初识Frida--Android逆向之Java层hook (一)
博客同步:访问

 

文章涉及到的知识点:

  • 怎么使用javascript实例化类并调用类方法
  • 怎么在"jscode"中增加自定义javascript方法
  • 怎么较为灵活的hook类方法

apk的安装与分析

示例下载:whyshouldIpay

 

下载apk后安装,一样还是先来看看是什么功能,这是一个比较简单的验证程序,简单的使用后,了解到PREMIUM CONETNT内容需要输入License验证后才能查看。那估计PREMIUM CONETNT按钮中的内容应该就是答案了吧。

流程分析

使用jadx将apk反编译出来,分析,在AndroidManifest.xml中找到了启动的Activity是LauncherActivity

 

 

找到其中验证的主要代码verifyClick,分析如下:

public void verifyClick(View v) {
    //第一个验证,将输入的Licese通过网络验证,但这个肯定是通不过的,这是一个可能需要绕过的点。
        try {
            InputStream in = new URL("http://broken.license.server.com/query?license=" + ((EditText) findViewById(R.id.text_license)).getText().toString()).openConnection().getInputStream();
            StringBuilder responseBuilder = new StringBuilder();
            byte[] b = new byte[0];
            while (in.read(b) > 0) {
                responseBuilder.append(b);
            }
            String response = responseBuilder.toString();
     //网络验证需要服务器返回 "LICENSEKEYOK",才能进行下一步
            if (response.equals("LICENSEKEYOK")) {
      //当网络验证成功后,生成激活秘钥,并写入到preferences文件中
                String activatedKey = new String(MainActivity.xor(getMac().getBytes(), response.getBytes()));
                Editor editor = getApplicationContext().getSharedPreferences("preferences", 0).edit();
                editor.putString("KEY", activatedKey);
                editor.commit();
      //这样便成功激活
                new Builder(this).setTitle((CharSequence) "Activation successful").setMessage((CharSequence) "Activation successful").setIcon(17301543).show();
                return;
            }
            new Builder(this).setTitle((CharSequence) "Invalid license!").setMessage((CharSequence) "Invalid license!").setIcon(17301543).show();
        } catch (Exception e) {
            new Builder(this).setTitle((CharSequence) "Error occured").setMessage((CharSequence) "Server unreachable").setNeutralButton((CharSequence) "OK", null).setIcon(17301543).show();
        }
    }

verifyClick中可以知道生成激活秘钥的算法是MainActivity.xor

 String activatedKey = new String(MainActivity.xor(getMac().getBytes(), response.getBytes()));

来到MainActivity中,查看该方法,看上去笔算起来还是比较麻烦。

  public static byte[] xor(byte[] val, byte[] key) {
        byte[] o = new byte[val.length];
        for (int i = 0; i < val.length; i++) {
            o[i] = (byte) (val[i] ^ key[i % key.length]);
        }
        return o;
    }

接下来当程序被激活成功后,点击PREMIUM CONETNT按钮,会调用MainActivity中的方法,可以看到它将MAC,以及生成的Key发送到了MainActivity中。

 public void showPremium(View view) {
        Intent i = new Intent(this, MainActivity.class);
        i.putExtra("MAC", getMac());
        i.putExtra("KEY", getKey());
        startActivity(i);
    }

MainActivityonCreate方法中,看到了最终答案生成的native方法stringFromJNI(key, mac)

protected void onCreate(Bundle savedInstanceState) {
 //获取Intent传递过来的值
        String key = getIntent().getStringExtra("KEY");
        String mac = getIntent().getStringExtra("MAC");
        if (key == "" || mac == "") {
            key = "";
            mac = "";
        }
        super.onCreate(savedInstanceState);
        setContentView((int) R.layout.activity_main);
     //调用native函数,算出答案
        ((TextView) findViewById(R.id.sample_text)).setText(stringFromJNI(key, mac));
    }

好,现在源代码分析基本上能够理清楚了,大概的过程就是这样。

  1. 输入License,进行验证
  2. 通过网络验证获取返回值“LICENSEKEYOK”后,然后调用MainActivity.xor在本地preferences文件中生成秘钥,激活成功。
  3. 本地获取MAC地址及秘钥Key传入MainActivity得出答案。

hook点分析

接下来重点就是要寻找hook点,经过刚才解题流程的分析,得出hook思路如下:

  1. 获取getMac()函数的返回值,与“LICENSEKEYOK"字符串进行xor运算得出秘钥Key.
  2. hook getKey方法,让它不从preferences文件读取Key,而是我们自己构造。
  3. hook verifyClick,让它调用showPremium方法

JavaScript代码构造与执行

0x00 hook getMac()

先来一个简单的示例,看看getMac()方法返回的的是什么,采用的方法是hook showPremium,这样就能通过点击PREMIUM CONETNT按钮直接得到getMac()的返回值。

 

JavaScript代码如下:

js_code = '''
    Java.perform(function(){
        var hook_Activity = Java.use('de.fraunhofer.sit.premiumapp.LauncherActivity');
        //hook showPremium从而方便直接点击按钮得出Mac值
        hook_Activity.showPremium.implementation = function(v){
            //因为showPremium,getMac()均在LauncherActivity类中,所有直接通过this就能直接调用getMac()方法
            var Key = this.getKey();
            var Mac = this.getMac();
            send(Key);
            send(Mac);

        }
    });
'''

完整python代码如下:

import frida,sys


def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)


js_code = '''
    Java.perform(function(){
        var hook_Activity = Java.use('de.fraunhofer.sit.premiumapp.LauncherActivity');
        hook_Activity.showPremium.implementation = function(v){
            var Key = this.getKey();
            var Mac = this.getMac();
            send(Key);
            send(Mac);

        }
    });
'''


session = frida.get_usb_device().attach("de.fraunhofer.sit.premiumapp")
script = session.create_script(js_code)
script.on('message',on_message)
script.load()
sys.stdin.read()

运行看看结果:

0x01 计算秘钥Key

接下来开始真正第一步的hook,将mac值与“LICENSEKEYOK"通过MainActivity.xor获取秘钥Key。那就直接hook getKey方法吧,这样可以自己来构造秘钥Key。
仔细分析,会发现在这一步中可能会遇到下面的问题:

  1. 怎么调用xor方法。
  2. java是强类型语言,javascript是弱类型语言,怎么将javascript参数进行类型转换并传递到java语言中。

怎么将javascript参数进行类型转换并传递到java语言中?其实方法很简单,既然java是强类型语言,那就根据它要求的类型传递对应参数即可,看看它参数的类型。

public static byte[] xor(byte[] val, byte[] key) {
        byte[] o = new byte[val.length];
        for (int i = 0; i < val.length; i++) {
            o[i] = (byte) (val[i] ^ key[i % key.length]);
        }
        return o;
    }

那么,在javascript代码中,先准备一个将字符串类型转换为byte[]类型的方法stringToBytes,再通过实例化MainActivity类的方式调用xor(),然后还需要一个将byte[]回转为String的方法,因为秘钥key是Sting类型的。

js_code = '''
     //字符串转换byte[]的方法
     stringToBytes = function(str) {  
        var ch, st, re = []; 
        for (var i = 0; i < str.length; i++ ) { 
            ch = str.charCodeAt(i);  
            st = [];                 

           do {  
                st.push( ch & 0xFF );  
                ch = ch >> 8;          
            }    
            while ( ch );  
            re = re.concat( st.reverse() ); 
        }  
        return re;  
    } 
    //将byte[]转成String的方法
     function byteToString(arr) {  
        if(typeof arr === 'string') {  
            return arr;  
        }  
        var str = '',  
            _arr = arr;  
        for(var i = 0; i < _arr.length; i++) {  
            var one = _arr[i].toString(2),  
                v = one.match(/^1+?(?=0)/);  
            if(v && one.length == 8) {  
                var bytesLength = v[0].length;  
                var store = _arr[i].toString(2).slice(7 - bytesLength);  
                for(var st = 1; st < bytesLength; st++) {  
                    store += _arr[st + i].toString(2).slice(2);  
                }  
                str += String.fromCharCode(parseInt(store, 2));  
                i += bytesLength - 1;  
            } else {  
                str += String.fromCharCode(_arr[i]);  
            }  
        }  
        return str;  
    }
    //hook 代码
    Java.perform(function(){
        var hook_Activity = Java.use('de.fraunhofer.sit.premiumapp.LauncherActivity');
        var MainActivity = Java.use('de.fraunhofer.sit.premiumapp.MainActivity')
        var LicenseStr = "LICENSEKEYOK";
        //hook getKey()方法,直接构造密码,而不从preferences读取
        hook_Activity.getKey.implementation = function(){
            //获取Mac
            var Mac = this.getMac();
            //实例化MainActivity
            var instance = MainActivity.$new();
            //类型转换
            var MacByte =stringToBytes(Mac);
            var LicenseByte = stringToBytes(LicenseStr);

            send("MacByte:"+MacByte)
            send("LicenseByte:"+LicenseByte)
            //调用实例化对象的xor方法
            xorResult = instance.xor(MacByte,LicenseByte);
            send(xorResult);
            //类型回转
            var Key = byteToString(xorResult)
            send(Key);
            return Key;
        }
        hook_Activity.verifyClick.implementation = function(view){
            this.showPremium(view);
        }
    });
'''

接下来,执行看看,能不能获取秘钥Key。
不知道怎么启动模拟器中的frida-server,以及端口转发,可以先看看初识Frida--Android逆向之Java层hook (一)
启动python脚本,在模拟器中直接点击PREMIUM CONTENT,即可看到执行结果。

0x02 调用showPremium获取答案

前面2个步骤,可以说是已经完成90%了,接下来只需要在hook一个能够触发showPremium方法的即可。方法就随意了,这里采用hook verifyClick的方式,这样点击app上的VERIFY按钮,触发verifyClick方法去调用showPremium,进而获得最终答案。

 hook_Activity.verifyClick.implementation = function(view){
            this.showPremium(view);
        }

启动脚本,点击app上的VERIFY按钮看看执行结果:

 

完整python代码:下载

总结

通过上面的例子,可以学习在java层怎么使用frida实现:

  1. 任意类方法调用。
  2. 任意类方法重实现。
    以及学会怎么构造和使用自定义javascript方法。
    当然这还仅仅只是一个开始.....

[培训] 优秀毕业生寄语:恭喜id咸鱼炒白菜拿到远超3W月薪的offer,《安卓高级研修班》火热招生!!!

最后于 2018-6-6 10:54 被ghostmazeW编辑 ,原因: 修复图片
收藏
点赞5
打赏
分享
最新回复 (25)
雪    币: 289
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zhuangbx 活跃值 2018-6-5 19:43
2
0
很好很详细,谢谢分享
雪    币: 234
活跃值: 活跃值 (380)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
安卓qq 活跃值 2018-6-6 11:11
3
0
好牛逼的帖子,不过不会js
雪    币: 713
活跃值: 活跃值 (325)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
ghostmazeW 活跃值 2 2018-6-6 11:24
4
0
zhuangbx 很好很详细,谢谢分享
谢谢
雪    币: 713
活跃值: 活跃值 (325)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
ghostmazeW 活跃值 2 2018-6-6 11:24
5
0
安卓qq 好牛逼的帖子,不过不会js[em_5]
没事,抽点时间简单学习js一下就OK了
雪    币: 234
活跃值: 活跃值 (380)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
安卓qq 活跃值 2018-6-7 23:15
6
0
不错,大佬还有后续么
雪    币: 713
活跃值: 活跃值 (325)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
ghostmazeW 活跃值 2 2018-6-8 13:21
7
0
安卓qq 不错,大佬还有后续么[em_6]
当然有啊!工作太忙了,我一定会抽时间给大家分享的
雪    币: 915
活跃值: 活跃值 (313)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
La0s 活跃值 2018-6-11 22:39
8
0
感谢楼主分享,未来还会出Native层的吗
雪    币: 1255
活跃值: 活跃值 (21)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
jackyspy 活跃值 1 2018-6-12 18:36
9
0
兄弟对DexClassLoader动态加载的class有什么好的hook方法吗?
frida作者只是简单讲了一下思路  https://github.com/frida/frida/issues/150#issuecomment-238328067
另一个提问者说搞定了,但没分享方案  https://github.com/frida/frida-java/issues/65
雪    币: 713
活跃值: 活跃值 (325)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
ghostmazeW 活跃值 2 2018-6-28 14:46
10
0
wx_La0s 感谢楼主分享,未来还会出Native层的吗
会的
雪    币: 2224
活跃值: 活跃值 (314)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
MaYil 活跃值 2018-7-2 18:34
11
0
感谢分享, 期待native的
最后于 2018-7-2 18:35 被MaYil编辑 ,原因:
雪    币: 713
活跃值: 活跃值 (325)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
ghostmazeW 活跃值 2 2018-7-3 16:16
12
0
jackyspy 兄弟对DexClassLoader动态加载的class有什么好的hook方法吗? frida作者只是简单讲了一下思路 https://github.com/frida/frida/issues/15 ...
第三篇文章,我会详细给大家演示
雪    币: 1
活跃值: 活跃值 (28)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shutu 活跃值 2018-7-27 15:40
13
0
牛逼  期待多出一点教程
雪    币: 222
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
stukd 活跃值 2018-8-3 17:37
14
1
楼主帖子写的仔细,很不错,能学到很多东西。感觉我这种小白还是从正向和其他简单逆向教程慢慢学起好了。
雪    币: 618
活跃值: 活跃值 (38)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
kimoh 活跃值 2018-11-16 13:52
15
6

感谢作者分享,写的很详细,针对byte[]转string,string转byte[] 传参这个问题上,这里有更简单快捷的实现方式,便于不擅于js的童鞋参考一下。

import frida,sys

def on_message(message,data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

jscode = """
Java.perform(function(){
    function stringToBytes(str){
    var javaString = Java.use('java.lang.String');
    var bytes = [];
    bytes = javaString.$new(str).getBytes();
    return bytes;
    }

    function bytesToString(bytes){
    var javaString = Java.use('java.lang.String');
    return javaString.$new(bytes);
    }    

    var LauncherActivity = Java.use('de.fraunhofer.sit.premiumapp.LauncherActivity');
    var activeKey_str="";
    LauncherActivity.getKey.implementation = function(v){
    return activeKey_str;
    }
    LauncherActivity.showPremium.implementation = function(v){
    send("Hook start...");
    var key  = this.getKey();
    var mac = this.getMac();
    send("key:"+key);
    send("mac:"+mac);
    var MainActivity = Java.use('de.fraunhofer.sit.premiumapp.MainActivity');
    var activeKey = MainActivity.xor(stringToBytes(mac),stringToBytes("LICENSEKEYOK"));
    activeKey_str = bytesToString(activeKey);    
    send("activeKey:" + activeKey_str);
    this.showPremium(v);
    }
}
);
"""

dev = frida.get_usb_device()
session = dev.attach('de.fraunhofer.sit.premiumapp')
script = session.create_script(jscode)
script.on('message',on_message)
script.load()
sys.stdin.read()
雪    币: 713
活跃值: 活跃值 (325)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
ghostmazeW 活跃值 2 2018-11-16 14:16
16
1
kimoh 感谢作者分享,写的很详细,针对byte[]转string,string转byte[] 传参这个问题上,这里有更简单快捷的实现方式,便于不擅于js的童鞋参考一下。 ```python import ...
赞!!!很好啊!
雪    币: 1022
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
moonlife 活跃值 2018-11-16 15:38
17
1
很赞 学习ing
雪    币: 4
活跃值: 活跃值 (324)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Harpe 活跃值 2018-11-22 23:13
18
0
学习了,非常感谢,请问什么时候出点native的呢
最后于 2018-11-22 23:13 被Harpe编辑 ,原因:
雪    币: 279
活跃值: 活跃值 (85)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
罗小墨 活跃值 2019-10-29 08:41
19
0
学习啦
雪    币: 279
活跃值: 活跃值 (85)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
罗小墨 活跃值 2019-10-29 08:42
20
0
希望楼主以后继续写出这一类的文章
雪    币: 1117
活跃值: 活跃值 (834)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
KerryS 活跃值 2019-11-27 04:31
21
0
感谢楼主,学到很多,以下是一些疑惑:
1、str.charCodeAt(i)得到的是unicode编码,所以stringToBytes得到的是unicode编码串,而 byteToString(arr)函数看样子像是把utf-8编码串转成字符串,因而两个函数其实是不能互逆的,即把一个字符串先用stringToBytes再byteToString(arr),得到的不再是原来的字符串。
2、var store = _arr[i].toString(2).slice(7 - bytesLength); 中bytesLength应该是utf-8编码中第一个字节1的个数,所以感觉slice里面好像应该是7 - bytesLength才对。
编码知识参考了这个 http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
最后于 2019-11-27 04:34 被KerryS编辑 ,原因:
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
嘛哩嘛哩轰 活跃值 2020-3-5 19:57
22
0
给大佬点赞
雪    币: 222
活跃值: 活跃值 (10)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
舵手 活跃值 3 2020-3-7 01:37
23
0
看到了why:)
在得到密钥后,也可以直接 hook LauncherActivity 类的verifyClick方法
    Java.perform(function(){
                var hook_Activity = Java.use('de.fraunhofer.sit.premiumapp.LauncherActivity');
               hook_Activity.verifyClick.implementation = function(){
                       var editor = this.getApplicationContext().getSharedPreferences("preferences", 0).edit();
                        editor.putString("KEY", "|{yu~iu{iq|yyu~");
                       editor.commit();
               }
    });
最后于 2020-3-7 02:00 被舵手编辑 ,原因:
雪    币: 3045
活跃值: 活跃值 (378)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shuichon 活跃值 2020-3-29 11:23
24
0
很详细,很赞。支持楼主。
雪    币: 225
活跃值: 活跃值 (121)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Allen_lea 活跃值 2020-4-22 17:00
25
0
很详细,谢谢楼主
游客
登录 | 注册 方可回帖
返回