安卓应用程序的逆向工程

你常常在用 Burp 拦阻信息的时刻很渺茫么?你常常在阐发用加密的数据停止通讯的App,对付必要懂得它的数据而怀疑么?在本文,我将会分享许多办法来用于逆向阐发APK。咱们将会对目的APP采纳静态和静态的阐发办法。
我创立了一个简略的APP作为阐发目的,它的功效只是纯真地对咱们输入的数据停止验证,假如用户输入准确的话,将会在屏幕上表现“Congratulations“。
咱们先看一下这个应用的源代码以便于咱们一下子可以或许将它与反编译后的APK代码停止比拟。
package com.punsec.demo;
 
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Base64;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
 
import javax.crypto.SecretKey;
 
public class MainActivity extends AppCompatActivity {
 
    TextView result;
    EditText input;
    Button button;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        input = (EditText) findViewById(R.id.input);
        result = (TextView) findViewById(R.id.result);
        button = (Button) findViewById(R.id.ok);
 
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String a = input.getText().toString();
                String b = getString(R.string.a);
                try {
                    SecretKey secretKey = Util.a(new String(Base64.decode(getString(R.string.b), Base64.DEFAULT)));
                    byte e[] = Util.a(a, secretKey);
                    String er = Base64.encodeToString(e, Base64.DEFAULT).trim();
 
                    if(er.equals(b)) {
                        result.setText(getString(R.string.d));
                    }else {
                        result.setText(getString(R.string.e));
                    }
 
                } catch (Exception e) {
//                    Log.d("EXCEPTION:", e.getMessage());
                }
            }
        });
    }
}
这个应用应用下面的这个帮助类来履行一些重要的操纵:
package com.punsec.demo;
 
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
 
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
 
 
class Util {
    static SecretKey a(String secret)
            throws NoSuchAlgorithmException, InvalidKeySpecException

    {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(secret.getBytes());
        byte[] digest = md.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", (0xFF & b)));
        }
        return new SecretKeySpec(sb.toString().substring(0,16).getBytes(), "AES");
    }
 
    static byte[] a(String message, SecretKey secret)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidParameterSpecException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException
    {
        Cipher cipher = null;
        cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secret);
        return cipher.doFinal(message.getBytes("UTF-8"));
    }
 
    static String a(byte[] cipherText, SecretKey secret)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidParameterSpecException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException
    {
        Cipher cipher = null;
        cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secret);
        return new String(cipher.doFinal(cipherText), "UTF-8");
    }
}
你可以或许下载曾经编译好的APK From -> CrackMe
在咱们停止下一步的操纵以前,先枚举阐发所需的配景常识:
一个曾经root的安卓装备或许虚构机(固然并非一切的阐发办法都必要root权限,然则有一个root的装备是不错的)。
Frida
Python
Inspeckage
Xposed Framework
APKTool
APKStudio
ByteCodeViewer
Dex2Jar
JarSigner(Java JDK)
JD-JUI
Brain
咱们将会应用的三种阐发办法:
静态阐发和Hooking.
二进制文件Patch(byte code改动).
静态阐发和代码复制.
静态/运转时情况 阐发和函数Hooking:
咱们必要应用的阐发工具: Frida, dex2jar, JD-GUI.
用 Frida阐发:
究竟甚么是Frida ?

It's Greasemonkey for native apps, or, put in more technical terms, it’s a dynamic code instrumentation toolkit. It lets you inject snippets of JavaScript or your own library into native apps on Windows, macOS, Linux, iOS, Android, and QNX. Frida also provides you with some simple tools built on top of the Frida API. These can be used as-is, tweaked to your needs, or serve as examples of how to use the API.
用简略的术语来讲,它可以或许被用来Hook函数挪用,注入你自己的代码将来可以或许来改动应用自己的履行流程。咱们将会应用它来经由过程检测和来辨认分歧的变量。
为了可以或许装置Frida,咱们可以或许将手机开启USB调试以后用数据线衔接电脑,而且在电脑端运转
# check adb devices whether connected or not
adb devices
# push/copy the latest frida server to phone
adb push frida-server-10.4.0-android-arm /data/local/tmp/frida
# set permissions for frida, grant SU permissions if prompted
adb shell su -c "chmod 755 /data/local/tmp/frida"
# start frida server on android device
adb shell su -c "./data/local/tmp/frida &"
# install frida-python on your Windows/Mac/Linux device
pip install --user frida
运转了下面的敕令以后,咱们的Frida Server就曾经运转在了咱们的电脑上,让咱们来查验一下,关上终端,运转python:
Python 2.7.10 (default, Feb  7 2017, 00:08:15)
Type "help", "copyright", "credits" or "license" for more information.
>>> import frida
>>> frida.get_usb_device()
Device(id="802b7421", name="LG SCH-XXXX", type='tether')
为了便利以后的阐发,如今让咱们创立一个python剧本:
import frida, sys, time
 
encrypted = None
 
def on_message(message, data):
    global encrypted
    try:
        if not encrypted:
          encrypted = message['payload']['encrypted']
          print('[+] Received str : ' + encrypted)

 

{C}          return
    except:
        pass  
  if message['type'] == 'send':
     print('[*] {0}'.format(message['payload']))
  elif message['type'] == 'error':
    if "'encrypted' undefined" in message['description']:
          print('[!] Encrypted value not updated yet, try to rotate the device')
    else:
          print('[!] ' + message['description'])
  else:
    print message
      
jscode = open('punsec.js').read()
print('[+] Running')
process_name = 'com.punsec.demo'
device = frida.get_usb_device()
try:
    pid = device.get_process(process_name).pid
    print('[+] Process found')
except frida.ProcessNotFoundError:
    print('[+] Starting process')
    pid = device.spawn([process_name])
    device.resume(pid)
    time.sleep(1)
process = device.attach(pid)
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
while True:
    time.sleep(1)
    if encrypted:
      script.post({'type':'encrypted','value':encrypted})
      break
sys.stdin.read()
让咱们逐步讲一些这些代码:
这个Encrypted变量最后是一个无范例的工具,它不久以后就会被剧原来将它更新为一个加密的值。这个 on_message 函数是一个回调函数可以或许被 frida 的 javascript 代码来应用,在javascript代码当中,咱们将注入到咱们法式的过程中往返调咱们的python代码。这个回调函数可以或许被经由过程在javascript代码中的send() 函数来履行。下一个变量是jscode, 它可以或许将咱们的js代码注入到法式的过程中。为了更便利咱们阅读,js代码被写到另一个文件中。Process_name变量是咱们的过程名字。咱们可以或许经由过程在adb shell中运转 "ps" 敕令 "pm list packages" 敕令获得咱们应用的过程名字。
这个 device 变量是来衔接咱们的USB装备(手机)的。Try except 用于处置非常(万一目的法式尚未在咱们的装备上运转的话,就会发生非常)。在晓得了运转法式的UID后,咱们可以或许挂接到目的法式上,而且在目的过程上注入jscode。经由过程应用js的 send() 函数,剧本就会开端注册咱们的回调函数。下面是 while 轮回,可以或许看到frida现实上是有何等的壮大,在这个轮回中,咱们检测能否encrypted变量它的范例曾经不是None了,假如它的范例发生了转变,剧本的post()函数将会发送一个信息将咱们的js代码注入到目的过程,而且信息将会被在js代码中的recv() 函数所处置。
在开端下一步的操纵以前,咱们必要对目的apk停止静态阐发。咱们起首要反编译apk而且将java bytecode转换为.java格局的代码来阅读。在这里,咱们应用的阐发工具是dex2jar。
$ ./d2j-dex2jar.sh CrackMe.apk
dex2jar CrackMe.apk -> ./CrackMe-dex2jar.jar
如今让咱们经由过程JD-GUI来阐发适才天生的CrackMe-dex2jar.jar文件
 

可以或许看到反编译后的代码与原始的java代码照样有很大的分歧的。咱们来阐发一下分歧的处所:起首可以或许很显著的看到资本 id由原来的R.x.x变换称为了数字格局的。
正如咱们下面看到的,MainActivity只包括一个 onCreate() 函数。咱们起首来看一下android应用的性命周期: 

可以或许看到: onCreate() 函数在app启动以后就运转。为了坚持应用的现实功效,咱们如今就在hook这个函数,来履行对原始函数的挪用,可以或许获得到今朝activity的上下文来获得一些字符串的值,就像下面这一行异样:

String str = MainActivity.this.getString(2131099669);
如今让咱们创立punsec.js文件,来获得这些值。
Java.perform(function () {
    var MainActivity = Java.use('com.punsec.demo.MainActivity');
    MainActivity.onCreate.implementation = function(a) {
        this.onCreate(a);
        send({"encrypted":this.getString(2131099669)});
    };
});
Java.perform() 是 frida 界说的,它的功效是:奉告frida server来运转曾经包装好的js代码。 Java.use() 是一个包装器为了可以或许静态的加载packages到咱们的目的过程中。为了下一步的必要,咱们将会应用send() 回调函数来发送数据到咱们的python法式中。如今运转着的python剧本给咱们前往了如许的信息:
$ python punsec.py
[+] Running
[+] Starting process
[+] Received str : vXrMphqS3bWfIGT811/V2Q==
要记着,要想onCreate() 函数触发,必需要履行回调函数,也便是在启动过程以后,必需要让它在后盾运转后再关上法式,请参考下面的Activity性命周期。
咱们也看到了代码中有几个挪用来履行 Base64.decode() 和经由过程数字id来获得string, 咱们能够也会必要这些值,以是让咱们来改动一下咱们的代码

 

{C}Java.perform(function () {
    var MainActivity = Java.use('com.punsec.demo.MainActivity');
    MainActivity.onCreate.implementation = function(a) {
        this.onCreate(a);
        send({'encrypted':this.getString(2131099669)});
    };
    var base64 = Java.use('android.util.Base64');
    base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {
        send('Base64 Encoded : ' + x);
        var buf = new Buffer(base64.decode(x, y));
        send('Base64 Decoded : ' + buf.toString());
        return base64.decode(x, y);
    }
 
});
再一次运转咱们的python法式将会获得下面的输入:
$ python punsec.py
[+] Running
[+] Process found
[*] Base64 Encoded : TXlTdXBlclNlY3JldEwzM3RQYXNzdzByZA==
[*] Base64 Decoded : MySuperSecretL33tPassw0rd
Hmm, 彷佛咱们曾经胜利了。不要发急,如今让咱们再来细心看一下咱们的反编译代码:
if (Base64.encodeToString(
Util.a(paramAnonymousView,
Util.a(new String(
Base64.decode(MainActivity.this.getString(2131099683), 0)
)
)
),
0).trim().equals(str))
在下面的代码中,有两次对 Util.a 函数的挪用然则都采纳的分歧的参数范例,咱们曾经hook了 Base64.decode() 函数,以是如今让咱们用下面的代码对 Util.a() 创立一个 hook :
Java.perform(function () {
    var MainActivity = Java.use('com.punsec.demo.MainActivity');
    MainActivity.onCreate.implementation = function(a) {
        this.onCreate(a);
        send({'encrypted':this.getString(2131099669)});
    };
    var base64 = Java.use('android.util.Base64');
    base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {
        send('Base64 Encoded : ' + x);
        var buf = new Buffer(base64.decode(x, y));
        send('Base64 Decoded : ' + buf.toString());
        return base64.decode(x, y);
    }
    var Util = Java.use('com.punsec.demo.Util');
    Util.a.implementation;
 
});
运转咱们的python代码,而后可以或许获得如下的输入:
$ python punsec.py
[+] Running
[+] Process found
[!] Error: a(): has more than one overload, use .overload() to choose from:
.overload('java.lang.String')
.overload('java.lang.String', 'javax.crypto.SecretKey')
.overload('[B', 'javax.crypto.SecretKey')
这彷佛呈现了一点差错。看起来是咱们的Util类中有函数重载(有雷同的办法称号然则领有分歧的参数)。为了降服这个成绩, frida供应给咱们额定的办法 overload(),经由过程这个办法,咱们可以或许显式地设置哪一个办法来 override/hook。咱们将会 hook Util.a(String, SecretKey)函数(由于它是一个卖力加密的函数)来为了停止下一步阐发:
 

然则咱们怎样能力辨认出这是一个加密函数的呢?起首可以或许看到这个函数的前往范例是byte,很明显意味着并无前往一个string范例,同时,当地暗码初始化为1来作为第一个参数通报:
 

如今,让咱们来改动咱们的js代码为了可以或许正当地hook这个函数:
Java.perform(function () {
    var MainActivity = Java.use('com.punsec.demo.MainActivity');
    MainActivity.onCreate.implementation = function(a) {
        this.onCreate(a);
        send({'encrypted':this.getString(2131099669)});
    };
    var base64 = Java.use('android.util.Base64');
    base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {
        send('Base64 Encoded : ' + x);
        var buf = new Buffer(base64.decode(x, y));
        send('Base64 Decoded : ' + buf.toString());
        return base64.decode(x, y);
    }
    var Util = Java.use('com.punsec.demo.Util');
    Util.a.overload('java.lang.String', 'javax.crypto.SecretKey').implementation = function(x, y) {

 

{C}        send('UserInput : ' + x);
        return this.a(x,y);
    }
 
});
再次运转咱们的python法式,察看输入有哪些转变:
$ python punsec.py
[+] Running
[+] Process found
[*] Base64 Encoded : TXlTdXBlclNlY3JldEwzM3RQYXNzdzByZA==
[*] Base64 Decoded : MySuperSecretL33tPassw0rd
[*] UserInput : wrongSecretTest
极好的,咱们如今可以或许拦阻咱们的输入了。如今咱们可以或许发明 Util 类还有一个函数 Util.a(byte, SecretKey) 不停没有在app中应用,经由过程阐发可以或许看到这是一个解密函数。以是如今咱们该若何做呢? 加密函数曾经接管到了密钥,以是咱们可以或许在解密函数中应用,然则咱们还必要第一个参数。第一个参数是一个 base64 解密的string 变量。以是让咱们来改动咱们的代码,为了可以或许在咱们的 js中收到这个参数,而且过掉这个解密函数,如许的话,咱们就可以解密终极的Key来完成此次挑衅。如今末了一次改动咱们的js代码:
Java.perform(function () {
    var MainActivity = Java.use('com.punsec.demo.MainActivity');
    MainActivity.onCreate.implementation = function(a) {
        this.onCreate(a);
        send({'encrypted':this.getString(2131099669)});
    };
    var base64 = Java.use('android.util.Base64');
    base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {
        // send('Base64 Encoded : ' + x);
        // var buf = new Buffer(base64.decode(x, y));
        // send('Base64 Decoded : ' + buf.toString());
        return base64.decode(x, y);
    }
    var Util = Java.use('com.punsec.demo.Util');
    Util.a.overload('java.lang.String', 'javax.crypto.SecretKey').implementation = function(x, y) {
        recv('encrypted', function onMessage(payload) {
            encrypted = payload['value'];
        });
        cipher = base64.decode(encrypted, 0); // call the above base64 method
        secret = this.a(cipher, y); // call decrypt method
        send('Decrypted : ' + secret)
        return this.a(secret,y);
    }
 
});
咱们把一个 recv() 挪用放在了函数中以便于可以或许收到咱们写的python法式中曾经存储的加密string。如今解密这个曾经被加密过的base64密钥而且和密钥一路发送到解密函数中。如今让咱们再一次运转咱们的python法式:
$ python punsec.py
[+] Running
[+] Process found
[!] Encrypted value not updated yet, try to rotate the device
[+] Received str : vXrMphqS3bWfIGT811/V2Q==
[*] Decrypted : knb*AS234bnm*0
woah, 咱们获得了key。这也会笼罩掉任何的用户输入并将其调换为解密的string, 以是如今每个用户输入都是起作用的: 

如今咱们不仅用现实的secret笼罩了用户输入,而且还笼罩了现实的secret phrase为了经由过程这个挑衅。
假使咱们的apk应用中没有解密函数,咱们该怎么办呢? 不用担忧,咱们能奇妙的将js代码拔出到package中来履行解密操纵而且用必要的参数笼罩这个办法,或许咱们还可以或许用下面的python代码来解密:
import frida, sys, time, md5
from Crypto.Cipher import AES
 
encrypted = None
secretKey = None
 
 
def decrypt(encrypted, key):
  key = md5.new(key).hexdigest()[:16]
  cipher = AES.new(key)
  decrypted = cipher.decrypt(encrypted.decode('base64'))[:14]
 
  for i in range(1,len(encrypted.decode('base64'))/16):
    cipher = AES.new(key, AES, encodedEncrypted.decode('base64')[(i-1)*16:i*16])
    decrypted += cipher.decrypt(encodedEncrypted.decode('base64')[i*16:])[:16]
 
  return decrypted.strip()
 
def on_message(message, data):
    global encrypted, secretKey
    try:
      if not encrypted:
        encrypted = message['payload']['encrypted']
      if not secretKey:
        secretKey = message['payload']['secretKey']
    except:
      pass
    if message['type'] == 'send':
      print('[*] {0}'.format(message['payload']))
    elif message['type'] == 'error':

 

{C}      if 'ReferenceError' in message['description']:
        print('[!] Rotate the device')
      else:
        print('[!] ' + message['description'])
    else:
      print message
      
jscode = open('punsec.js').read()
 
print('[+] Running')
 
process_name = 'com.punsec.demo'
device = frida.get_usb_device()
 
try:
    pid = device.get_process(process_name).pid
    print('[+] Process found')
except frida.ProcessNotFoundError:
    print('[+] Starting process')
    pid = device.spawn([process_name])
    device.resume(pid)
    time.sleep(1)
 
process = device.attach(pid)
 
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
while True:
    time.sleep(0.2)
    if encrypted and secretKey:
      script.post({'type':'encrypted','value':decrypt(encrypted, secretKey)})
      break
sys.stdin.read()
咱们更新后的js代码:
Java.perform(function () {
    var MainActivity = Java.use('com.punsec.demo.MainActivity');
    MainActivity.onCreate.implementation = function(a) {
        this.onCreate(a);
        send({'encrypted':this.getString(2131099669)});
    };
    var base64 = Java.use('android.util.Base64');
    base64.decode.overload('java.lang.String', 'int').implementation = function(x, y) {
        var buf = new Buffer(base64.decode(x, y));
        send({'secretKey': buf.toString()});
        return base64.decode(x, y);
    }
    var Util = Java.use('com.punsec.demo.Util');
    Util.a.overload('java.lang.String', 'javax.crypto.SecretKey').implementation = function(x, y) {
        recv('encrypted', function onMessage(payload) {
            secret = payload['value'];
        });
        send('Decrypted : ' + secret)
        return this.a(secret,y);
    }
 
});
如今运转咱们的python法式:
$ python punsec.py
[+] Running
[+] Process found
[*] {u'secretKey': u'MySuperSecretL33tPassw0rd'}
[!] Rotate the device
[*] {u'encrypted': u'vXrMphqS3bWfIGT811/V2Q=='}
[*] {u'secretKey': u'MySuperSecretL33tPassw0rd'}
[*] Decrypted : knb*AS234bnm*0
用 Inspeckage 来阐发
咱们将会应用到Inspeckage, Xposed Framework 和 ApkStudio/ByteCodeViewer.
Inspeckage – Android Package Inspector

Inspeckage is a tool developed to offer dynamic analysis of Android applications. By applying hooks to functions of the Android API, Inspeckage will help you understand what an Android application is doing at runtime.
Inspeckage可以或许让你来用简略的web接口停止阐发。Inspeckage必要你装置Inspeckage Xposed module而且在 Xpose 框架中激活它。在你的android装备上启动Inspeckage App而且抉择咱们的目的应用而且在Inspeckage Webserver中阅读。

 
 

关上主动革新开关,点击在webserver上的设置按钮而且封闭一些Actvity检测就像下面这张图异样,末了点击 start App 而且革新页面。
 

一旦咱们的App在手机上运转,就在App上输入测试的数据并点击ok按钮,而后察看Inspeckage webserver上的关照(留意要开启主动革新):
 

 

 


这两张截图都表现出了咱们应用了frida办法。用 Inspeckage阐发是相称简略的,你可以或许检测app履行的文件体系Activities, SQL行列步队操纵,在这面前应用的是和咱们应用frida办法雷同的观点: 在加密,文件体系,hash等操纵函数上停止hook,然则在这里,咱们可以或许履行函数hook吗? 固然了,正如你在末了一个标签上看到的,它供应了一个hook选项。然则随之而来的成绩是:它不像frida那样,Inseckage没有供应对重载的办法的笼罩,如今点击hook标签而且创立一个hook来验证咱们的想 法: 

以是如今为了可以或许创立一个有用的hook,咱们将会应用 ByteCodeViewer 或许 APKStudio 来改动apk中的 bytecode(字节码)。下面这是咱们对字节码的patch:
 

(留意:当关上apk的时刻,撤消抉择"Decode Resource",不然你将会碰到下面这些成绩)
ERROR: 9-patch image C:UserslabuserDesktopCrackMeresdrawable-mdpi-v4abc_list_divider_mtrl_alpha.9.png?www.2cto.com malformed.
       Must have one-pixel frame that is either transparent or white.
ERROR: Failure processing png?www.2cto.com image C:UserslabuserDesktopCrackMeresdrawable-mdpi-v4abc_list_divider_mtrl_alpha.9.png?www.2cto.com
ERROR: 9-patch image C:UserslabuserDesktopCrackMeresdrawable-hdpi-v4abc_list_divider_mtrl_alpha.9.png?www.2cto.com malformed.
       Must have one-pixel frame that is either transparent or white.
ERROR: Failure processing png?www.2cto.com image C:UserslabuserDesktopCrackMeresdrawable-hdpi-v4abc_list_divider_mtrl_alpha.9.png?www.2cto.com
ERROR: 9-patch image C:UserslabuserDesktopCrackMeresdrawable-xhdpi-v4abc_list_divider_mtrl_alpha.9.png?www.2cto.com malformed.
       Must have one-pixel frame that is either transparent or white.
ERROR: Failure processing png?www.2cto.com image C:UserslabuserDesktopCrackMeresdrawable-xhdpi-v4abc_list_divider_mtrl_alpha.9.png?www.2cto.com
ERROR: 9-patch image C:UserslabuserDesktopCrackMeresdrawable-xxhdpi-v4abc_list_divider_mtrl_alpha.9.png?www.2cto.com malformed.
       Must have one-pixel frame that is either transparent or white.
ERROR: Failure processing png?www.2cto.com image C:UserslabuserDesktopCrackMeresdrawable-xxhdpi-v4abc_list_divider_mtrl_alpha.9.png?www.2cto.com
在下面那副截图中,可以或许看到第168行,咱们经由过程辨认第168行的参数范例和前往值,胜利的辨认出了这便是加密函数,在第197行,这个被赋值为1的变量也是咱们以前看到的。咱们曾经把这个函数的名字改成为了b ,而且解密函数称号改成c。如今为了包管咱们的app可以或许失常运转,咱们必要在MainActivity的字节码上做出雷同的更新:
 

如今咱们的义务曾经完成为了,可以或许创立一个keystore来对咱们的apk停止署名。
C:Program FilesJavajdk1.8.0_144bin>keytool -genkey -v -keystore C:userslabuserDesktopmy.keystore -alias alias_na
me -keyalg RSA -keysize 2048 -validity 10000
Enter keystore password:
Re-enter new password:
What is your first and last name?
  [Unknown]:
What is the name of your organizational unit?
  [Unknown]:
What is the name of your organization?
  [Unknown]:
What is the name of your City or Locality?
  [Unknown]:
What is the name of your State or Province?
  [Unknown]:
What is the two-letter country code for this unit?
  [Unknown]:
Is CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown correct?
  [no]:  yes
Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days
        for: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
Enter key password for
        (RETURN if same as keystore password):
[Storing C:userslabuserDesktopmy.keystore]
C:Program FilesJavajdk1.8.0_144bin>jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore C:userslabuserDesktopmy.keystore C:userslabuserDesktopCrackMe.apk alias_name
将曾经署名的apk装置到装备上。重启Inspeckage,开端hook来验证能否咱们的改动曾经起作用了。
 

极好地,咱们的改动是完美的,如今咱们可以或许对目的函数Util.b() 下hook。抉择这个函数而且点击 Add hook 按钮。如今让咱们点击ok按钮而且察看Inspeckage Server的关照。

 

 

咱们可以或许看到Inspeckage曾经胜利地从曾经hook的函数中截取到数据而且供应给咱们了函数的参数和前往值。如今点击 WordStr 按钮而且设置装备摆设如下的选项。
 

在这里咱们将第一个参数通报给了咱们的加密函数,这个函数领有咱们曾经用frida辨认进去的机密值。不管甚么时刻停止输入测试(大小写敏感),Hook都邑调换数据而且通报咱们供应的值,而后将Congratulations再一次表现在咱们的屏幕上。
 

二进制补钉(字节码改动)
在这个办法中,咱们将会应用ApkStudio和Jarsigner。 咱们将会经由过程改动反编译的Apk,以后从新编译它来改动法式的逻辑。启动 ApkStudio而且再次加载文件( 记着要撤消抉择"Decode Resources"复选框),以后在MainActivity$1.smali中定位到法式代码中停止比拟的地位
 

咱们可以或许在第113行看到法式会比拟两个分歧的值来履行检测,假如比拟失败了,会表现"Umm, Try Again"。然则假如法式总是将两个雷同的值停止比拟会怎样呢?在这类情况下,法式将会跳过else前提间接前往true。以是如今让咱们将代码改动后从新编译并对咱们的Apk停止署名,而后做测试。
 

再一次运转应用验证能否法式能否经由过程了原来的法式逻辑。
静态阐发和代码复制
在这个办法中,咱们将会应用Android Studio/IntelliJ 和 ByteCodeViewer来停止静态代码阐发。
Static analysis
Also called static code analysis, is a method of computer program debugging that is done by examining the code without executing the program. The process provides an understanding of the code structure, and can help to ensure that the code adheres to industry standards.
启动 ByteCodeViewer(BCV) 而且期待它来装置依附项。一旦装置好了以后,咱们将可以或许间接在它外面关上apk文件。在BCV中,点击File->Add 而且抉择 CrackMe.apk,而后让它完成加载这个文件。点击View->Pane1->Procyon->java和View->Pane2->Smali/Dex->Samli/Dex 。你的界面将会看起来和下面的异样
 

在第9行,咱们可以或许看到"final String string2 = this.this$0.getString(2131099669);"。在以后运动上下文的getString()办法,可以或许应用"this","MainActivity.this "或许"getApplicationContext() " 经由过程一个整数值来获得资本值。这些数字id的索引在R类中被创立,以是咱们将会在R$string.class 中探求资本id,BCV可以或许将内容辨认为xml 文件格局。
 

咱们可以或许看到这个整数值被分配给a,如今咱们不得纰谬a在strings.xml中做一个查找,你可以或许在BCV中经由过程睁开CrackMe.apk->Decoded Resources->res->values->strings.xml 。
 

有时刻BCV关上文件会呈现出二进制情势而不是xml格局,对付这类情况,咱们可以或许点击File->Save As Zip ,而后解压zip而且在编辑器中关上strings.xml。
 

极好的,咱们曾经找到了这个字符串。咱们将会用这个办法规复一切的字符串而且保留它们。
2131099669 -> a -> vXrMphqS3bWfIGT811/V2Q==
2131099683 -> b -> TXlTdXBlclNlY3JldEwzM3RQYXNzdzByZA==
2131099685 -> d -> Congratulations
2131099686 -> e -> Umm, Try again
咱们将会应用IntelliJ来写咱们的代码来试图完成逆向原始函数的功效,经由过程从BCV反编译后的文件中复制代码。 当一切的代码让在一块的时刻,它将会看起来像下面的代码
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;
class Decrypt {
 
    public static void main(String args[]) {

 

        String a = "vXrMphqS3bWfIGT811/V2Q==";
        String b = "TXlTdXBlclNlY3JldEwzM3RQYXNzdzByZA==";
        String new_b = new String(Base64.getDecoder().decode(b));
 
        byte[] array = Base64.getDecoder().decode(a);
        String decoded = decrypt(array, getKey(new_b));
 
        System.out.println("Decoded : " + decoded);
    }
 
    private static String decrypt(byte[] array, SecretKey secretKey) {
        String decoded = null;
        try {
 
            Cipher instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
            instance.init(2, secretKey);
            decoded = new String(instance.doFinal(array), "UTF-8");
        }catch (Exception e) {
            // do something
        }
        return decoded;
    }
    
    private static SecretKey getKey(String s) {
        SecretKeySpec secretKeySpec = null;
        try {
            MessageDigest instance = MessageDigest.getInstance("MD5");
            instance.update(s.getBytes());
            byte[] digest = instance.digest();
            StringBuilder sb = new StringBuilder();
            for (int length = digest.length, i = 0; i
                sb.append(String.format("%02x", digest[i] & 0xFF));
            }
            secretKeySpec = new SecretKeySpec(sb.toString().substring(0, 16).getBytes(), "AES");
        } catch (Exception e) {
            // do something
        }
        return secretKeySpec;
    }
}
将文件命名为Decrypt.java 并保留文件。咱们必要编译这个文件,而后运转它来检测咱们的代码能否起作用了。
// create new file
$ nano Decrypt.java
// compile file
$ javac Decrypt.java
// run file
$ java Decrypt
Decoded : knb*AS234bnm*0
咱们可以或许在python代码中做异样的工作,就像先前frida那样,然则有时刻复制代码是更简略的,由于只必要做很小的改动就可以够或许使它运转。
咱们曾经描写了所提到的一切工具和办法,如今是时刻喝杯咖啡了。