安卓逆向——Frida的进阶用法
1484321987948696 发表于 贵州 移动安全 2066浏览 · 2024-08-23 00:14

八. .js脚本 Hook native函数
1.拿到附件,安装到模拟器中运行一下,可以看到可以让我们输入字符或者数字,然后点击提交,报错,没了。

2.用jadx静态分析吧,

package com.ad2001.frida0x8;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.ad2001.frida0x8.databinding.ActivityMainBinding;

/* loaded from: classes4.dex */
public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    Button btn;
    EditText edt;

    public native int cmpstr(String str);

    static {
        System.loadLibrary("frida0x8");
    }

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
        this.binding = inflate;
        setContentView(inflate.getRoot());
        this.edt = (EditText) findViewById(R.id.editTextText);
        Button button = (Button) findViewById(R.id.button);
        this.btn = button;
        button.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.frida0x8.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View v) {
                String ip = MainActivity.this.edt.getText().toString();
                int res = MainActivity.this.cmpstr(ip);
                if (res == 1) {
                    Toast.makeText(MainActivity.this, "YEY YOU GOT THE FLAG " + ip, 1).show();
                } else {
                    Toast.makeText(MainActivity.this, "TRY AGAIN", 1).show();
                }
            }
        });
    }
}

分析代码逻辑,我们可以知道,只有当我们输入的内容是正确的flag时,cmpstr方法的返回值是1时,就会输出表示正确的提示词,然后再输出flag。但是我们可以看到该方法是native层的,

那么我们就去native层查看该函数的逻辑。我们将附件中的apk文件的后缀,apk改成zip,然后解压缩,然后我们在路径lib->x86_64->libfrida0x8.so,打开libfrida0x8.so文件,用ida64分析,然后找到,

就可以看到伪代码,

可以看到就是aGsjebObujwfMbo字符串每一个元素都减一,然后将结果给到Password,最后进行比较,如果与我们输入的字符串进行比较,一样就返回1,否则返回0。由于password 参数只在 native 库中调用的 strcmp 函数中被作为参数传递,那么这里我们就可以通过 Frida 脚本 Hook strcmp 函数来实现。

我们需要编写一个js脚本Hook 应用程序加载的 frida0x8 native 库中 cmpstr 函数使用的 strcmp 函数,并获取函数中使用的参数 password 的值。

以下就是脚本:

第一种:

var strcmp_adr =  Module.findExportByName("libc.so", "strcmp");
// 使用Module.findExportByName()获取libc.so库中strcmp函数的地址。

Interceptor.attach(strcmp_adr, {
  // Hook strcmp_adr地址对应的函数,将onEnter和onLeave回调函数附加到strcmp_adr地址中

  onEnter: function (args) {
    // 在strcmp函数被调用之前执行的回调函数,args是一个指针数组,提供对函数参数的访问。

    console.log("Hooking the strcmp function");
    // 进入strcmp函数时打印提示语"Hooking the strcmp function"

    var flag = Memory.readUtf8String(args[1]); 
    // 使用Memory.readUtf8String()获取password参数;
    // password参数在strcmp函数中的地址为arg[1]。

    console.log("The flag is "+ flag);
    // 打印获取到的password参数值。

  },
  onLeave: function (retval) {
    // 在strcmp函数被调用之后执行的回调函数,它提供对返回值retval的访问。
  }
});

但是,这个脚本会把程序中所有的strcmp函数都打印出来,因此,我们可以输入一个特定的字符串(例如:”HELLO”),将其作为 Hook 脚本的过滤器。

var strcmp_adr = Module.findExportByName("libc.so", "strcmp");
// 使用Module.findExportByName()获取libc.so库中strcmp函数的地址。
Z
Interceptor.attach(strcmp_adr, {
  // Hook strcmp_adr地址对应的函数,将onEnter和onLeave回调函数附加到strcmp_adr地址中

    onEnter: function (args) {
      // 在strcmp函数被调用之前执行的回调函数,args是一个指针数组,提供对函数参数的访问。

        var arg0 = Memory.readUtf8String(args[0]);
          // 使用Memory.readUtf8String()读取第一参数inputStr内容。

        var flag = Memory.readUtf8String(args[1]);
          // 使用Memory.readUtf8String()获取第二个参数password内容。

        if (arg0.includes("HELLO")) {
          // 只有当第一个参数值是我们输入的特定字符串"HELLO"时才执行下面的操作。

            console.log("Hookin the strcmp function");
             // 打印提示语"Hooking the strcmp function"

            console.log("Input " + arg0);
             // 打印第一个参数inputStr的值。

            console.log("The flag is "+ flag);
             // 打印第二个参数password的值。

        }
    },
    onLeave: function (retval) {
        // 在strcmp函数被调用之后执行的回调函数,它提供对返回值retval的访问。
    }
});

Memory.readUtf8String()是Frida中用于读取内存中UTF-8编码字符串的函数。它的作用是从指定的内存地址读取UTF-8编码的字符串,并将其转换为JavaScript中的字符串类型。
拿到flag

总结:**Frida Hook native 函数的脚本模板

Interceptor.attach(targetAddress, {
  // 将回调附加到指定的函数地址,targetAddress为我们想要挂钩的native函数的地址。

  onEnter: function (args) {
    // 在目标函数被调用之前执行的回调函数,提供对函数参数args的访问。

    console.log('Entering ' + functionName);
    // 根据需要修改或记录参数
  },
  onLeave: function (retval) {
    // 在目标函数被调用之后执行的回调函数,它提供对返回值retval的访问。

    console.log('Leaving ' + functionName);
    // 根据需要修改或记录参数
  }
});

补充

在前面的学习我们知道,Hook一个函数需要知道调用这个函数的程序包名和类名。而Hook native 函数,我们需要知道这个 native 函数的地址,然后使用 Frida 的 Interceptor API 进行 Hook。

**Interceptor API是Frida中一个功能强大的模块,能够帮助我们 Hook C 函数、Objective-C 方法。

Interceptor模块中Interceptor.attach()函数用于拦截函数调用,需要传递两个参数,第一个参数是要拦截的函数地址,第二个参数是包含回调函数的对象,用于定义在目标函数被调用时执行的回调函数,通常包含以下两个回调函数:**

  • onEnter:在目标函数被调用之前执行的回调函数。在这个回调函数中,可以访问函数的参数,修改参数的值,记录函数调用信息等操作。

  • onLeave:在目标函数被调用之后执行的回调函数。在这个回调函数中,可以访问函数的返回值,修改返回值,记录函数执行结果等操作。

导出函数表:指的是库文件提供给外部使用的函数或变量。

导入函数表:指库文件引用的函数或变量。

在Frida 0x8 的这个例子中,我们的目标函数是 strcmp函数,所以我们可以从 libfrida0x8.so 的导入表或者 libc 的导出表中找到该函数地址。

输出libfrida0x8.so的导出表:Module.enumerateExports("libfrida0x8.so")

找strcmp函数的地址:Module.findExportByName("libc.so", "strcmp")或者 Module.enumerateImports("libfrida0x8.so")[4]


找cmpstr函数的地址:Module.getExportByName("libfrida0x8.so", "Java_com_ad2001_frida0x8_MainActivity_cmpstr")

Module.findExportByName("libfrida0x8.so", "Java_com_ad2001_frida0x8_MainActivity_cmpstr")

Module.findExportByName与 Module.getExportByName 相同。唯一的区别是,如果找不到导出符号,Module.getExportByName 会引发异常,而 Module.findExportByName 会返回 null。

有时,如果上面的 API 获取不到指定函数地址,我们可以使用 Module.getBaseAddress,这个 API 返回给定模块的基地址,我们可以用它来找到 libfrida0x8.so 库的基地址。如果我们想找到一个特定函数的地址,可以在基地址的基础上添加偏移量。cmpstr 函数的偏移量,我们可以在 IDA 中查看

Module.getBaseAddress("libfrida0x8.so").add(0x864) (0x864是偏移量,可以在ida中看到)


:由于 Android 默认启用 ASLR 安全机制,系统启动或程序加载时随机化内存地址空间,如果你重新启动了 APP,前后两次获取到的指定函数地址会有差异。

查看libfrida0x8.so导入符号信息:Module.enumerateImports("libfrida0x8.so")

接着,可以利用下标来访问其中的函数的地址:Module.enumerateImports("libfrida0x8.so")[4]["address"] [4]就是下标。

九. .js脚本 更改native函数的返回值
1.首先还是一样,拿到apk文件,先安装到模拟器上运行一下,结果还是一样,一顿点,然后九只有提示你try angin

2.接着用jadx打开,静态分析一下反编译的Java代码,

package com.ad2001.a0x9;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.ad2001.a0x9.databinding.ActivityMainBinding;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    Button btn;

    public native int check_flag();

    static {
        System.loadLibrary("a0x9");
    }

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
        this.binding = inflate;
        setContentView(inflate.getRoot());
        Button button = (Button) findViewById(R.id.button);
        this.btn = button;
        button.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.a0x9.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View v) {
                if (MainActivity.this.check_flag() == 1337) {
                    try {
                        Cipher cipher = Cipher.getInstance("AES");
                        SecretKeySpec secretKeySpec = new SecretKeySpec("3000300030003003".getBytes(), "AES");
                        try {
                            cipher.init(2, secretKeySpec);
                            byte[] decryptedBytes = Base64.getDecoder().decode("hBCKKAqgxVhJMVTQS8JADelBUPUPyDiyO9dLSS3zho0=");
                            try {
                                String decrypted = new String(cipher.doFinal(decryptedBytes));
                                Toast.makeText(MainActivity.this.getApplicationContext(), "You won " + decrypted, 1).show();
                                return;
                            } catch (BadPaddingException e) {
                                throw new RuntimeException(e);
                            } catch (IllegalBlockSizeException e2) {
                                throw new RuntimeException(e2);
                            }
                        } catch (InvalidKeyException e3) {
                            throw new RuntimeException(e3);
                        }
                    } catch (NoSuchAlgorithmException e4) {
                        throw new RuntimeException(e4);
                    } catch (NoSuchPaddingException e5) {
                        throw new RuntimeException(e5);
                    }
                }
                Toast.makeText(MainActivity.this.getApplicationContext(), "Try again", 1).show();
            }
        });
    }
}

可以看到在MainActivity 类中,有一段native功能声明,使用native关键字定义了一个native函数check_flag,该函数不接受任何参数,同时返回一个参数,然后将 a0x9 库加载到程序中,System.loadLibrary 方法提示我们 a0x9 是 native 库文件,check_flag 函数在 a0x9 库中实现。同时,MainActivity 类中还定义了一个 onClick 方法来监控按钮的点击,当点击应用程序按钮时,onClick 方法会将 check_flag 函数的返回值与1337进行比较,如果它们相等,就会解密 flag 并显示在应用程序界面。否则,打印“Try again”。接下来我们需要使用 IDA 来分析 a0x9 库中的 check_flag 函数。

public native int check_flag();

    static {
        System.loadLibrary("a0x9");
    }
public void onClick(View v) {
if (MainActivity.this.check_flag() == 1337) {
    try {
        Cipher cipher = Cipher.getInstance("AES");
        SecretKeySpec secretKeySpec = new SecretKeySpec("3000300030003003".getBytes(), "AES");
        try {
            cipher.init(2, secretKeySpec);
            byte[] decryptedBytes = Base64.getDecoder().decode("hBCKKAqgxVhJMVTQS8JADelBUPUPyDiyO9dLSS3zho0=");
            try {
                String decrypted = new String(cipher.doFinal(decryptedBytes));
                Toast.makeText(MainActivity.this.getApplicationContext(), "You won " + decrypted, 1).show();
                return;
            } 
                ...
        }
        Toast.makeText(MainActivity.this.getApplicationContext(), "Try again", 1).show();
    }

还是一样,改一下文件后缀名,然后找到liba0x9.so文件,然后用ida64打开,找到对应函数,发现啥逻辑没用,就是一个返回1,

那么接下来,我们需要使用 Frida 框架,编写一个 JavaScript 脚本,Hook在应用程序加载的 liba0x9.so native 库中运行的 check_flag 函数,并将它的返回值更改为1337。

通过对 APK 静态分析,我们可以知道 check_flag 函数在 liba0x9.so 库中的名称是Java_com_ad2001_a0x9_MainActivity_check_1flag,同时可以在 liba0x9.so 中的导出表中找到该符号的地址。

以下是脚本:

var check_flag = Module.findExportByName("liba0x9.so", "Java_com_ad2001_a0x9_MainActivity_check_1flag")
// 获取liba0x9.so库中Java_com_ad2001_a0x9_MainActivity_check_1flag函数地址,并将它储存在check_flag中。

Interceptor.attach(check_flag, {
  // Hook check_flag地址对应的函数,将onEnter和onLeave回调函数附加check_flag地址中
  onEnter: function () {
    // 在check_flag函数被调用之前执行的回调函数,根据需要修改或记录参数。

  },
  onLeave: function (retval) {
    // 在check_flag函数被调用之后执行的回调函数,它提供对返回值retval的访问。

    console.log("Original return value :" + retval);
    // 打印函数原本返回值

    retval.replace(1337)  
     // 将check_flag函数返回值修改为1337。
  }
});

然后先让程序运行起来,再输入frida -U 'Frida 0x9' -l F:\桌面\9.js

就得flag了。

总结:Frida 更改native函数返回值的脚本模板

Interceptor.attach(targetAddress, {
  // 将回调附加到指定的函数地址,targetAddress为我们想要挂钩的native函数的地址。

  onEnter: function (args) {
    // 在目标函数被调用之前执行的回调函数,提供对函数参数args的访问。

    console.log('Entering ' + functionName);
    // 根据需要修改或记录参数
  },
  onLeave: function (retval) {
    // 在目标函数被调用之后执行的回调函数,它提供对返回值retval的访问。

    console.log('Leaving ' + functionName);
    // 根据需要修改或记录参数

    retval.replace(value)  
    // 将目标函数返回值修改为value。
  }
});

十. .js脚本调用native函数
1.还是一样,拿到apk文件,安装到模拟器上,结果发现在雷电模拟器上安装不了,然后换成夜神模拟器,可以安装但是不可以运行,就很奇怪了。但是可以看到教程上打开应用有且仅有一个提示词,

2.那就用jadx打开分析一下函数逻辑

package com.ad2001.frida0xa;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.ad2001.frida0xa.databinding.ActivityMainBinding;
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.jvm.internal.Intrinsics;

/* compiled from: MainActivity.kt */
@Metadata(d1 = {"\u0000&\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\u0018\u0000 \u000b2\u00020\u0001:\u0001\u000bB\u0005¢\u0006\u0002\u0010\u0002J\u0012\u0010\u0005\u001a\u00020\u00062\b\u0010\u0007\u001a\u0004\u0018\u00010\bH\u0014J\t\u0010\t\u001a\u00020\nH\u0086 R\u000e\u0010\u0003\u001a\u00020\u0004X\u0082.¢\u0006\u0002\n\u0000¨\u0006\f"}, d2 = {"Lcom/ad2001/frida0xa/MainActivity;", "Landroidx/appcompat/app/AppCompatActivity;", "()V", "binding", "Lcom/ad2001/frida0xa/databinding/ActivityMainBinding;", "onCreate", "", "savedInstanceState", "Landroid/os/Bundle;", "stringFromJNI", "", "Companion", "app_debug"}, k = 1, mv = {1, 8, 0}, xi = ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_VERTICAL_CHAINSTYLE)
/* loaded from: classes4.dex */
public final class MainActivity extends AppCompatActivity {
    public static final Companion Companion = new Companion(null);
    private ActivityMainBinding binding;

    public final native String stringFromJNI();

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
        Intrinsics.checkNotNullExpressionValue(inflate, "inflate(layoutInflater)");
        this.binding = inflate;
        ActivityMainBinding activityMainBinding = null;
        if (inflate == null) {
            Intrinsics.throwUninitializedPropertyAccessException("binding");
            inflate = null;
        }
        setContentView(inflate.getRoot());
        ActivityMainBinding activityMainBinding2 = this.binding;
        if (activityMainBinding2 == null) {
            Intrinsics.throwUninitializedPropertyAccessException("binding");
        } else {
            activityMainBinding = activityMainBinding2;
        }
        activityMainBinding.sampleText.setText(stringFromJNI());
    }

    /* compiled from: MainActivity.kt */
    @Metadata(d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002¨\u0006\u0003"}, d2 = {"Lcom/ad2001/frida0xa/MainActivity$Companion;", "", "()V", "app_debug"}, k = 1, mv = {1, 8, 0}, xi = ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_VERTICAL_CHAINSTYLE)
    /* loaded from: classes4.dex */
    public static final class Companion {
        public /* synthetic */ Companion(DefaultConstructorMarker defaultConstructorMarker) {
            this();
        }

        private Companion() {
        }
    }

    static {
        System.loadLibrary("frida0xa");
    }
}

可以看到应用程序的 MainActivity类中,同样声明了一段native功能:在程序开始时定义了返回值为字符串类型的 native 函数 stringFromJNI,它不接受任何参数;在程序结尾处加载 frida0xa 动态链接库,用于实现 native 函数。MainActivity 类中还定义了onCreate方法,在程序加载时调用 stringFromJNI 函数,将函数返回的 “Hello Hackers”文本设置给 TextView 控件。

3.接下来,我们使用 IDA 对 frida0xa 动态链接库进行分析。

还是一样,先改后缀名,然后找到libfrida0xa.so,用ida64打开,发现函数窗口处一堆函数,并且是用c++写的,直接利用搜索功能找到stringFromJNI函数,可以看到

__int64 __fastcall Java_com_ad2001_frida0xa_MainActivity_stringFromJNI(_JNIEnv *a1)
{
  const char *v1; // rsi
  __int64 v3; // [rsp+18h] [rbp-48h]
  char v4[24]; // [rsp+40h] [rbp-20h] BYREF
  unsigned __int64 v5; // [rsp+58h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  std::string::basic_string<decltype(nullptr)>(v4, "Hello Hackers");
  v1 = (const char *)sub_20690(v4);
  v3 = _JNIEnv::NewStringUTF(a1, v1);
  std::string::~string(v4);
  return v3;
}

同时,我们还发现了一个函数,get_flag函数,这个函数我们在Java层并没有看到有引用,

逻辑很简单,可以直接逆,也可以用Frida框架进行调用,然后程序自解密flag并输出

要使用 Frida 脚本调用 native 函数,我们需要创建一个 NativePointer 对象,将要调用的 native 函数地址传递给 NativePointer 构造函数。然后,我们需要创建一个 NativeFunction 对象,来表示我们要调用的实际 native 函数。NativeFunction 对象的第一个参数应是 NativePointer 对象,第二个参数是 native 函数的返回类型,第三个参数是要传递给 native 函数参数的数据类型列表。

**NativePointer:是 Frida 中一个表示 native 内存地址的 JavaScript 对象,它用于在 Frida 脚本中操作和访问 native 内存地址,比如读取或写入内存中的数据,调用内存中的函数等。

NativeFunction:是 Frida 中用于在 JavaScript 中调用 native 函数的对象。通过 NativeFunction 对象,可以在 Frida 脚本中调用 native 共享库(如动态链接库)中的函数,实现对 native 函数的调用和控制。**

下面就是脚本,具体get_flag函数的地址怎么获取之前有记录

var adr = Module.findBaseAddress("libfrida0xa.so").add(0x1DD60);
// 获取 get_flag() 函数地址。

var get_flag_ptr = new NativePointer(adr);
// 创建一个 NativePointer 对象,用于操作和访问 get_flag() 函数内存地址。

const get_flag = new NativeFunction(get_flag_ptr, 'void', ['int', 'int']);
// 创建一个名为 get_flag 的 NativeFunction 对象,实现对 native 函数的调用和控制。
// get_flag_ptr:表示 使用 get_flag_ptr NativePointer对象;
// 'void':表示函数的返回值类型为viod;
// ['int', 'int']:表示传递给函数的参数类型为两个int类型的参数。

get_flag(1,2);
// 调用 get_flag 函数,传入两个参数1,2。

但是由于测试所用的模拟器无法运行该程序,没办法,用frida无法进行调用,就无法直接让程序运行抛出flag,也可以根据硬编码直接解密。

总结:Frida 调用 native 函数脚本模板

var native_adr = new NativePointer(<address_of_the_native_function>);
// 创建一个 NativePointer 对象,用于操作和访问 <address_of_the_native_function>内存地址。

const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);
// 创建一个 NativeFunction 对象,实现对 native 函数的调用和控制。
// native_adr:使用native_adr NativePointer对象;
// '<return type>':函数的返回值类型;
// ['argument_data_type']:传递给函数的参数类型列表。

native_function(<arguments>);
// 调用 native_function 函数,如果需要,可以传递<arguments>参数。

十一. .js脚本 使用ARM64Writer修改指令
1.这个题和上一个题一样,还是在雷电模拟器上安装不了,然后在夜神模拟器上运行不了,很苦恼,那就只能学思维和操作方法了。

2.用jadx反编译看看函数逻辑

package com.ad2001.frida0xb;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.ad2001.frida0xb.databinding.ActivityMainBinding;
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.jvm.internal.Intrinsics;

/* compiled from: MainActivity.kt */
@Metadata(d1 = {"\u0000\"\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\u0018\u0000 \n2\u00020\u0001:\u0001\nB\u0005¢\u0006\u0002\u0010\u0002J\t\u0010\u0005\u001a\u00020\u0006H\u0086 J\u0012\u0010\u0007\u001a\u00020\u00062\b\u0010\b\u001a\u0004\u0018\u00010\tH\u0014R\u000e\u0010\u0003\u001a\u00020\u0004X\u0082.¢\u0006\u0002\n\u0000¨\u0006\u000b"}, d2 = {"Lcom/ad2001/frida0xb/MainActivity;", "Landroidx/appcompat/app/AppCompatActivity;", "()V", "binding", "Lcom/ad2001/frida0xb/databinding/ActivityMainBinding;", "getFlag", "", "onCreate", "savedInstanceState", "Landroid/os/Bundle;", "Companion", "app_debug"}, k = 1, mv = {1, 8, 0}, xi = ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_VERTICAL_CHAINSTYLE)
/* loaded from: classes3.dex */
public final class MainActivity extends AppCompatActivity {
    public static final Companion Companion = new Companion(null);
    private ActivityMainBinding binding;

    public final native void getFlag();

    /* JADX INFO: Access modifiers changed from: protected */
    @Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
        Intrinsics.checkNotNullExpressionValue(inflate, "inflate(layoutInflater)");
        this.binding = inflate;
        if (inflate == null) {
            Intrinsics.throwUninitializedPropertyAccessException("binding");
            inflate = null;
        }
        setContentView(inflate.getRoot());
        View findViewById = findViewById(R.id.button);
        Intrinsics.checkNotNullExpressionValue(findViewById, "findViewById(R.id.button)");
        Button btn = (Button) findViewById;
        btn.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.frida0xb.MainActivity$$ExternalSyntheticLambda0
            @Override // android.view.View.OnClickListener
            public final void onClick(View view) {
                MainActivity.onCreate$lambda$0(MainActivity.this, view);
            }
        });
    }

    /* JADX INFO: Access modifiers changed from: private */
    public static final void onCreate$lambda$0(MainActivity this$0, View it) {
        Intrinsics.checkNotNullParameter(this$0, "this$0");
        this$0.getFlag();
    }

    /* compiled from: MainActivity.kt */
    @Metadata(d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002¨\u0006\u0003"}, d2 = {"Lcom/ad2001/frida0xb/MainActivity$Companion;", "", "()V", "app_debug"}, k = 1, mv = {1, 8, 0}, xi = ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_VERTICAL_CHAINSTYLE)
    /* loaded from: classes3.dex */
    public static final class Companion {
        public /* synthetic */ Companion(DefaultConstructorMarker defaultConstructorMarker) {
            this();
        }

        private Companion() {
        }
    }

    static {
        System.loadLibrary("frida0xb");
    }
}

可以看到在应用程序的 MainActivity 类中,定义了一个 native 函数 getFlag,该函数不接受任何参数也没有返回值,并在程序结尾处使用 System.loadLibrary 函数加载 frida0xb 动态链接库。MainActivity 类中定义的 onCreate 方法监听按钮的点击,点击按钮时会通过 lambda 表达式调用 onCreate$lambda$0 方法,然后在onCreate$lambda$0 方法中调用 getFlag 函数。接下来,我们使用 IDA 对 frida0xb 动态链接库进行分析,查看 getFlag 函数是如何实现的。

获取.so文件的方法还是一样,改一下后缀名,然后找到对应的.so文件,用ida64打开,找到函数,发现ida反编译出来的伪代码严重不完整

这是反编译的

这是汇编

我们回到流程控制窗口中查看。可以看到程序中出现永远为假的条件跳转,导致 IDA 识别不到解密 flag 并输出的代码指令

然后一跳转就没了。

分析完代码我们可以发现,程序在点击按钮时调用了 getFlag 函数,该函数在 native 代码实现过程中,有一个永久为假的条件跳转,导致解密 flag 并输出的代码块未被执行。为了获取这个 flag ,我们需要使用 Frida 脚本,修改jnz(跳转到程序结尾)这条指令,让它不执行,以绕过条件跳转来执行解码和输出 flag 的程序。

我们需要使用Frida 框架,编写一个 JavaScript 脚本,将 native 函数 getFlag 中的 jnz (条件分支跳转指令)修改为 Nop(空操作指令),在Frida中修改汇编指令需要使用ARM64Writer 类。

首先,我们需要知道 B.NE 指令的地址,该地址同样可以通过Module.getBaseAddress API 得到 frida0xb 库的基地址,再加上 B.NE 指令的偏移量来获取。我们可以在IDA中查看 B.NE 指令地址的偏移量为”0x170CE″。然后创建一个 ARM64Writer 类的实例,将获取到的 B.NE 指令的地址作为传入参数,再调用 ARM64Writer 实例的 putNop 方法在 B.NE 指令的地址上写入一条 Nop 指令,覆盖掉原来的指令。接着调用 ARM64Writer 实例的 flush 方法将修改后的指令写入内存中。

为了绕过应用程序中的内存分页保护机制,成功修改并运行 native 代码中的指令,我们还需要使用 Frida 中的 Memory.protect 函数,将指定内存区域的保护属性修改为”rwx”(可读可写可执行)。

**在Frida中,Memory.protect 函数用于修改内存页的保护属性,以控制对内存的访问权限。这个函数可以用来修改目标进程中的内存页,例如将内存页设置为可读、可写、可执行等。Memory.protect 函数的语法如下:

Memory.protect(ptr, size, protection)

ptr: 表示要修改保护属性的内存地址,通常是一个指向目标内存区域的指针。

size: 表示要修改保护属性的内存区域的大小,以字节为单位。

protection: 表示要设置的内存保护属性。**

脚本如下

var jnz_adr =  Module.getBaseAddress("libfrida0xb.so").add(0x170CE);
//获取 jnz 指令地址。

Memory.protect(jnz_adr,0x1000,"rwx");
// 将对应内存区域的保护属性修改为可读可写可执行。

var writer = new Arm64Writer(jnz_adr);
// 创建 ARM64Writer 类的实例 writer,将 jnz_adr 作为写入的内存地址。

try{
  writer.putNop();
  // 在原本 jnz 指令的地址上写入一条 Nop 指令,替换原本的 jnz 指令

  writer.flush();
  // 将修改后的指令写入内存中

  console.log("Command modification successful.");
  // 指令修改完成后打印一条提示语。

}finally {
  writer.dispose();
  // 释放与 writer 实例关联的资源。
}

还是因为无法运行程序,只能到这里了。

总结:Frida使用ARM64Writer修改指令的脚本模板

var writer = new ARM64Writer(<address_of_the_instruction>);
// 创建一个Arm64Writer类实例,用于编写ARM64指令,codeAddress是要写入的内存地址。

try {
  /*
  我们自己的指令实现
  */

  writer.flush();
  // 将修改后的指令写入内存中

} finally {
  writer.dispose();
  // 释放与 ARM64Writer 实例关联的资源。
}

结语
以上就是一些Frida工具在native层的一些相对进阶的用法和例子,在实际CTF或者实战中肯定不会如此简单,就需要我们掌握最根本的用法,以不变应万变。

附件:
1 条评论
某人
表情
可输入 255
目录