ctf中的Frida——脚本详解
前言
看到网上有很多frida hook的解题思路,却鲜有人对脚本进行讲解。本文将以一个小白的视角,从零基础讲解ctf中常用的几种hook脚本的实现,frida配置不再赘述,网上资料很多。文章末尾附刚结束的ciscn国赛安卓题目
Frida hook
我们常说‘‘用frida脚本去hook’’。hook一词即为钩子,程序运行时我们的脚本钩住目标函数,对其进行修改从而达到目的
Frida 脚本基本框架(js)
多说几句:js中,function用于定义函数,末尾的main()的作用是调用函数。console.log()即在控制台输出;var 或const都可以用于声明变量。javascript对缩进无要求,末尾分号加不加都可
先来看Java.perform : Java.perform(fn): ensure that the current thread is attached to the VM and call fn. ——Java.perform(fn):确保当前线程连接到虚拟机并调用fn。这个Api确保我们后面写的函数附加到进程而生效
注入进去看看
了解了Frida基本框架,以下将通过闯关的形式进行更深入的探索Frida的魅力
主动调用函数
第0关
a方法接受两个相同的参数,返回第一个参数的sha256值
基本逻辑:若username的sha256值与password的值相等则验证通过
脚本如下
Java,use() 用来获取当前类,在上述脚本中赋值给变量LoginActivity。
LoginActivity.a即调用这个class里的a方法,传入值123,那么result的值为123的sha256值
因此,username填入123,password填result,就可以通过验证
修改函数参数或返回值
第一关使用jeb分析汇编,因为第二关的过关条件永远为假,jadx看不到汇编无法分析
基本逻辑:“R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=”若与“R4jSLLLLLLLLLRknplkBpZDpis69kh7i+YAPTmMn2ABsOLLLLL==”相等则过关,这显然不现实。而后者字符串是a函数的返回值,由此我们可以对返回值修改成“R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=”使比较条件为真
.implentation作用是修改函数实现,将a函数替换为上述脚本中的function(),强行改变返回值
点击按钮就进入了下一关
调用非静态函数
仍然分析反汇编
static_bool_var和bool_var都为真即过关
而setStatic_bool_var函数和setBool_var函数的作用是将那两个值设置为true
思路显然易见,调用这两个函数不就好了吗!
但是,setStatic_bool_var函数前有static字样,而setBool_var函数没有,这说明setBool_var函数是非静态函数,像这样setBool_var()来调用将会报错,因为没有创建它的实例
详解见图片
这里要调用非静态函数,使用Java.choose这一Api
Java.choose(className, callbacks):通过扫描Java堆枚举className类的活动实例,其中callbacks是一个对象,指定:
onMatch(instance):用一个现成的实例调用每个活动实例
onComplete():在枚举所有实例后调用
设置成员变量与设置和函数名相同的成员变量
来到第三关
跟上一关差不多,但是没有现成的函数来设置值为真,需要手动设置,使用.value进行修改
重点:在这一关的class中,既有same_name_bool_var属性,又有same_name_bool_var函数。
我们想对属性进行修改,则一定要使用xxx._same_name_bool_var.value=true
多加的这个下划线代表着这里是对属性设置值而不是函数。
hook动态加载的dex
package com.example.androiddemo.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import com.example.androiddemo.Dynamic.CheckInterface;
import dalvik.system.DexClassLoader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/* loaded from: classes.dex */
public class FridaActivity5 extends BaseFridaActivity {
private CheckInterface DynamicDexCheck = null;
@Override // com.example.androiddemo.Activity.BaseFridaActivity
public String getNextCheckTitle() {
return "当前第5关";
}
/* JADX WARN: Removed duplicated region for block: B:46:0x0060 A[Catch: IOException -> 0x005c, TRY_LEAVE, TryCatch #6 {IOException -> 0x005c, blocks: (B:42:0x0058, B:46:0x0060), top: B:55:0x0058 }] */
/* JADX WARN: Removed duplicated region for block: B:55:0x0058 A[EXC_TOP_SPLITTER, SYNTHETIC] */
/*
Code decompiled incorrectly, please refer to instructions dump.
*/
public static void copyFiles(Context context, String str, File file) {
InputStream inputStream;
FileOutputStream fileOutputStream;
FileOutputStream fileOutputStream2 = null;
fileOutputStream2 = null;
InputStream inputStream2 = null;
try {
try {
inputStream = context.getApplicationContext().getAssets().open(str);
} catch (IOException e) {
e = e;
fileOutputStream = null;
} catch (Throwable th) {
th = th;
inputStream = null;
}
try {
fileOutputStream = new FileOutputStream(file.getAbsolutePath());
try {
byte[] bArr = new byte[1024];
while (true) {
int read = inputStream.read(bArr);
if (read == -1) {
break;
}
fileOutputStream.write(bArr, 0, read);
}
if (inputStream != null) {
inputStream.close();
}
fileOutputStream.close();
} catch (IOException e2) {
e = e2;
inputStream2 = inputStream;
try {
e.printStackTrace();
if (inputStream2 != null) {
inputStream2.close();
}
if (fileOutputStream != null) {
fileOutputStream.close();
}
} catch (Throwable th2) {
th = th2;
inputStream = inputStream2;
fileOutputStream2 = fileOutputStream;
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e3) {
e3.printStackTrace();
throw th;
}
}
if (fileOutputStream2 != null) {
fileOutputStream2.close();
}
throw th;
}
} catch (Throwable th3) {
th = th3;
fileOutputStream2 = fileOutputStream;
if (inputStream != null) {
}
if (fileOutputStream2 != null) {
}
throw th;
}
} catch (IOException e4) {
e = e4;
fileOutputStream = null;
} catch (Throwable th4) {
th = th4;
if (inputStream != null) {
}
if (fileOutputStream2 != null) {
}
throw th;
}
} catch (IOException e5) {
e5.printStackTrace();
}
}
private void loaddex() {
File cacheDir = getCacheDir();
if (!cacheDir.exists()) {
cacheDir.mkdir();
}
String str = cacheDir.getAbsolutePath() + File.separator + "DynamicPlugin.dex";
File file = new File(str);
try {
if (!file.exists()) {
file.createNewFile();
copyFiles(this, "DynamicPlugin.dex", file);
}
} catch (IOException e) {
e.printStackTrace();
}
try {
this.DynamicDexCheck = (CheckInterface) new DexClassLoader(str, cacheDir.getAbsolutePath(), null, getClassLoader()).loadClass("com.example.androiddemo.Dynamic.DynamicCheck").newInstance();
if (this.DynamicDexCheck == null) {
Toast.makeText(this, "loaddex Failed!", 1).show();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
public CheckInterface getDynamicDexCheck() {
if (this.DynamicDexCheck == null) {
loaddex();
}
return this.DynamicDexCheck;
}
/* JADX INFO: Access modifiers changed from: protected */
@Override // com.example.androiddemo.Activity.BaseFridaActivity, androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
loaddex();
}
@Override // com.example.androiddemo.Activity.BaseFridaActivity
public void onCheck() {
if (getDynamicDexCheck() != null) {
if (getDynamicDexCheck().check()) {
CheckSuccess();
startActivity(new Intent(this, FridaActivity6.class));
finishActivity(0);
return;
}
super.CheckFailed();
return;
}
Toast.makeText(this, "onClick loaddex Failed!", 1).show();
}
}
这关代码有点长,check函数在动态加载的dex中,像上文直接hook会提示找不到路径
看到代码中的dexclassloader其实就意味着要用此方法了!
Java.enumerateClassLoaders(): Java. enumerateclassloaders (callbacks):枚举Java VM中存在的类加载器,其中callbacks是一个对象,指定:
onMatch(loader):使用loader对每个类加载器调用,loader是特定java.lang.ClassLoader的包装器。
onComplete():在枚举了所有类加载器后调用。
这个函数仍然有两个回调。
要点1:使用Java.enumerateClassLoaders()查找所有classloader
要点2:使用Java.classFactory.loader切换classloader为当前loader(dalvik.system.DexClassLoader),而不是其他pathloader,只有dexloader才能找到我们动态加载的dex中的check函数
实战——2024ciscn安卓题目
check部分:
private boolean legal(String paramString) {
return paramString.length() == 38 && paramString.startsWith("flag{") && paramString.charAt(paramString.length() - 1) == '}' && !inspect.inspect(paramString.substring(5, paramString.length() - 1));
逻辑在inspect函数中,跟进看看
加密文本有了,解一个DES即可,看看key和iv
key和iv在native层,静态函数,可以直接hook
hook两次把key和iv都整出来
flag就出来了
总结
通过实战做题提升熟练度,同一结果可能有多种实现方式,不要使思维固化
官方网站查看Api:https://frida.re/docs/javascript-api/#script