初识Frida--Android逆向之Java层hook

Posted 看雪学院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识Frida--Android逆向之Java层hook 相关的知识,希望对你有一定的参考价值。

今天继续一个新的示例,同样采用CTF作为例子,难度稍微加大了一点,如果对Frida基本的使用还不是很了解,建议先看看之前的文章

文章涉及到的知识点:

  • 怎么使用javascript实例化类并调用类方法

  • 怎么在"jscode"中增加自定义javascript方法

  • 怎么较为灵活的hook类方法



apk的安装与分析


示例下载:whyshouldIpay(点击左下角阅读原文下载)

 

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



流程分析


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

 

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

 

找到其中验证的主要代码 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);
  }


在MainActivity的onCreate方法中,看到了最终答案生成的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文件中生成秘钥,激活成功。



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()


运行看看结果:


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


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,以及端口转发,可以先看看


启动python脚本,在模拟器中直接点击PREMIUM CONTENT,即可看到执行结果。


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



0x02 调用showPremium获取答案


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

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

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


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

 

完整python代码:下载(点击阅读原文下载)



总结


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

  1. 任意类方法调用。

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



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

看雪ID:ghostmazeW    

bbs.pediy.com/user-811277



本文由看雪论坛 ghostmazeW 原创

转载请注明来自看雪社区


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



看雪 2018 安全开发者峰会,7月21日火热来袭!







戳原文,立即看!

以上是关于初识Frida--Android逆向之Java层hook 的主要内容,如果未能解决你的问题,请参考以下文章

Java逆向基础之初识Byteman

iOS逆向之初识汇编的基础理论

FreeMarker之根据模板生成Java代码

Mybaits之逆向工程

Django初识

Django之模型层应用和初识Ajax