目录
- 初识Frida--Android逆向之Java层hook (二)
- apk的安装与分析
- 流程分析
- hook点分析
- JavaScript代码构造与执行
- 0x00 hook getMac()
- 0x01 计算秘钥Key
- 0x02 调用showPremium获取答案
- 总结
今天继续一个新的示例,同样采用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
,分析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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
。
1 |
String activatedKey = new String(MainActivity.xor(getMac().getBytes(), response.getBytes())); |
来到MainActivity
中,查看该方法,看上去笔算起来还是比较麻烦。
1 2 3 4 5 6 7 |
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
中。
1 2 3 4 5 6 |
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)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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)); } |
好,现在源代码分析基本上能够理清楚了,大概的过程就是这样。
- 输入License,进行验证
- 通过网络验证获取返回值“LICENSEKEYOK”后,然后调用
MainActivity.xor
在本地preferences
文件中生成秘钥,激活成功。
- 本地获取MAC地址及秘钥Key传入
MainActivity
得出答案。
hook点分析
接下来重点就是要寻找hook点,经过刚才解题流程的分析,得出hook思路如下:
- 获取getMac()函数的返回值,与“LICENSEKEYOK"字符串进行xor运算得出秘钥Key.
- hook getKey方法,让它不从
preferences
文件读取Key,而是我们自己构造。
- hook
verifyClick
,让它调用showPremium
方法
JavaScript代码构造与执行
0x00 hook getMac()
先来一个简单的示例,看看getMac()方法返回的的是什么,采用的方法是hook showPremium
,这样就能通过点击PREMIUM CONETNT
按钮直接得到getMac()
的返回值。
JavaScript代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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。
仔细分析,会发现在这一步中可能会遇到下面的问题:
- 怎么调用xor方法。
- java是强类型语言,javascript是弱类型语言,怎么将javascript参数进行类型转换并传递到java语言中。
怎么将javascript参数进行类型转换并传递到java语言中?其实方法很简单,既然java是强类型语言,那就根据它要求的类型传递对应参数即可,看看它参数的类型。
1 2 3 4 5 6 7 |
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类型的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
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,进而获得最终答案。
1 2 3 |
hook_Activity.verifyClick.implementation = function(view){ this.showPremium(view); } |
启动脚本,点击app上的VERIFY
按钮看看执行结果:
完整python代码:下载
总结
通过上面的例子,可以学习在java层怎么使用frida实现:
- 任意类方法调用。
- 任意类方法重实现。
以及学会怎么构造和使用自定义javascript方法。
当然这还仅仅只是一个开始.....
https://bbs.pediy.com/thread-227233.htm