-
Unidbg補(bǔ)環(huán)境實(shí)戰(zhàn)第一篇:補(bǔ)環(huán)境入門(mén)
-
為什么要補(bǔ)環(huán)境
-
Unidbg補(bǔ)環(huán)境的案例情景復(fù)現(xiàn)
-
模擬執(zhí)行so
-
參數(shù)獲取
-
unidbg 代碼初始化
-
目標(biāo)函數(shù)的調(diào)用
-
補(bǔ)環(huán)境說(shuō)明
-
實(shí)戰(zhàn)補(bǔ)環(huán)境
-
-
本章小節(jié)
-
Unidbg補(bǔ)環(huán)境實(shí)戰(zhàn)第一篇:補(bǔ)環(huán)境入門(mén)
Unidbg
是一個(gè)基于unicorn
的逆向工具,可以黑盒調(diào)用安卓和iOS中的so文件。這使得逆向人員可以在無(wú)需了解so內(nèi)部算法原理的情況下,主動(dòng)調(diào)用so中的函數(shù),讓其中的算法“為我所用”,只需要傳入所需的參數(shù)、補(bǔ)全運(yùn)行所需的環(huán)境,即可運(yùn)行出所需要的結(jié)果。及由此衍生的輔助分析、算法還原、SO
調(diào)試與逆向等等功能。
對(duì)于Android逆向來(lái)說(shuō),Unidbg的特點(diǎn)有以下幾種:
-
模擬JNI調(diào)用的API,因此可以調(diào)用
JNI_OnLoad
函數(shù)。 -
支持
JavaVM
和JNIEnv
。 -
支持模擬系統(tǒng)調(diào)用指令。
-
支持
ARM32
和ARM64
。 -
支持基于
Dobby
的inline hook。 -
支持基于
xHook
的GOT hook。 -
unicorn
后端支持簡(jiǎn)單的控制臺(tái)調(diào)試器,gdb stub,指令追蹤和內(nèi)存讀寫(xiě)追蹤。 -
支持
dynarmic
快速的后端。
為什么要補(bǔ)環(huán)境
使用unidbg最主要的問(wèn)題就是補(bǔ)環(huán)境,補(bǔ)環(huán)境對(duì)于so的模擬執(zhí)行太重要了,并且在這塊很容易跌跟頭。我們知道unidbg的作用是模擬執(zhí)行so中的函數(shù),也就是使用C/C++編寫(xiě)的函數(shù),它處于Native層。而Native的函數(shù)是Java層的函數(shù)通過(guò)JNI調(diào)用起來(lái)的,
那么Native也可以通過(guò)JNI這座橋梁去調(diào)用Java層的函數(shù)。
在Native層調(diào)用Java層的函數(shù)的時(shí)候,unidbg中并沒(méi)有這些函數(shù)的實(shí)現(xiàn),那么這些so就無(wú)法正常的通過(guò)unidbg加載起來(lái)。所以我們需要手動(dòng)的把Java層的函數(shù)補(bǔ)充起來(lái),讓Native層的函數(shù)去調(diào)用。
PS:以下所有分析均在
r0env2022
版安卓逆向環(huán)境下完成。r0env2022
版集成了Unidbg,打開(kāi)終端輸入unidbg
回車(chē),就是一個(gè)安裝好所有依賴(lài)包的可以直接跑項(xiàng)目的完整的Unidbg
運(yùn)行環(huán)境。
PS2:案例涉及的代碼及附件會(huì)放在我的Github中,地址:https://github.com/r0ysue/AndroidSecurityStudy
Unidbg補(bǔ)環(huán)境的案例情景復(fù)現(xiàn)
為了給大家講清本章的內(nèi)容,筆者開(kāi)發(fā)了一個(gè)樣本APK構(gòu)造了一些環(huán)境問(wèn)題。首先我們先熟悉一下這個(gè)APP。APP的打開(kāi)后的界面如圖1所示:
圖1 樣本放置的目錄
屏幕中央顯示了一串字符,感覺(jué)像是16進(jìn)制。然后,我們使用Jadx-gui反編譯APK,其中MainActivity.java的代碼如下所示:
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
// 檢測(cè)文件
public native void detectFile();
// 檢測(cè)是否有Hook
public native void detectHookTool();
// native函數(shù),獲取哈希結(jié)果
public native String getHash(String str);
// 加載 so
static {
System.loadLibrary("dogpro");
}
/* 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.mo170getRoot());
TextView tv = this.binding.sampleText;
detectFile();
detectHookTool();
// 獲取Hash結(jié)果
String r1 = getHash(getApplicationContext().getPackageCodePath());
tv.setText(r1);
}
}
在onCreate中有兩個(gè)檢測(cè),我們先不要理會(huì),他暫時(shí)不會(huì)對(duì)我們本章的內(nèi)容產(chǎn)生任何的影響。但是可以給大家看看他們做了什么事情:
-
detectFile
int __fastcall Java_com_example_dogpro_MainActivity_detectFile(_JNIEnv *a1)
{
int v2; // [sp+8h] [bp-30h]
int v3; // [sp+Ch] [bp-2Ch]
int v4; // [sp+14h] [bp-24h]
int MethodID; // [sp+18h] [bp-20h]
int v6; // [sp+1Ch] [bp-1Ch]
int v7; // [sp+20h] [bp-18h]
int Class; // [sp+24h] [bp-14h]// 反射去Java層找File類(lèi) Class = _JNIEnv::FindClass(a1, "java/io/File"); v7 = _JNIEnv::AllocObject(a1, Class); // 檢測(cè)的路徑 v6 = _JNIEnv::NewStringUTF(a1, "/sys/class/power_supply/battery/voltage_now"); MethodID = _JNIEnv::GetMethodID(a1, Class, "<init>", "(Ljava/lang/String;)V"); _JNIEnv::CallVoidMethod(a1, v7, MethodID, v6); v4 = _JNIEnv::GetMethodID(a1, Class, "exists", "()Z"); if ( (unsigned __int8)_JNIEnv::CallBooleanMethod(a1, v7, v4) ) _android_log_print(6, "lilac", byte_35D7); else _android_log_print(6, "lilac", byte_35F0); v3 = _JNIEnv::AllocObject(a1, Class); v2 = _JNIEnv::NewStringUTF(a1, "/data/local/tmp/nox"); _JNIEnv::CallVoidMethod(a1, v3, MethodID, v2); if ( (unsigned __int8)_JNIEnv::CallBooleanMethod(a1, v3, v4) ) return _android_log_print(6, "lilac", byte_361D); else return _android_log_print(6, "lilac", byte_3636);
}
首先檢測(cè)了電池的相關(guān)信息,我們嘗試去/sys/class/power_supply/battery/voltage_now
下查看,如圖2所示:
圖21-2 電池屬性
其它文件表示的含義如下所示:
//電池充電狀態(tài)
cat /sys/class/power_supply/battery/status
//電池電量
cat /sys/class/power_supply/battery/capacity
//電池運(yùn)行狀況
cat /sys/class/power_supply/battery/health
//顯示電池溫度
cat /sys/class/power_supply/battery/temp
//電池電壓 mV
cat /sys/class/power_supply/battery/voltage_now
第二處檢測(cè)的是/data/local/tmp/nox
,nox是夜神模擬器,模擬器創(chuàng)建的時(shí)候會(huì)在此路徑有文件的創(chuàng)建。幸運(yùn)的是,當(dāng)檢測(cè)到的時(shí)候,并不會(huì)有任何的操作,所以我們不予理會(huì),這里只帶領(lǐng)大家看看它是如何做檢測(cè)的。
-
detectHookTool
int __fastcall Java_com_example_dogpro_MainActivity_detectHookTool(_JNIEnv *a1)
{
int v1; // r0
int v2; // r0
const char *StringUTFChars; // [sp+28h] [bp-A0h]
int ObjectClass; // [sp+34h] [bp-94h]
int ObjectArrayElement; // [sp+38h] [bp-90h]
int i; // [sp+3Ch] [bp-8Ch]
int ArrayLength; // [sp+40h] [bp-88h]
int v9; // [sp+44h] [bp-84h]
int v10; // [sp+48h] [bp-80h]
int v11; // [sp+4Ch] [bp-7Ch]
int MethodID; // [sp+50h] [bp-78h]
int Class; // [sp+54h] [bp-74h]
size_t n; // [sp+6Ch] [bp-5Ch]
size_t v16; // [sp+7Ch] [bp-4Ch]
char v17[24]; // [sp+80h] [bp-48h] BYREF
char v18[36]; // [sp+98h] [bp-30h] BYREF// 反射找到 Throwable => 異常處理 Class = _JNIEnv::FindClass(a1, "java/lang/Throwable"); MethodID = _JNIEnv::GetMethodID(a1, Class, "<init>", "()V"); v11 = _JNIEnv::NewObject(a1, Class, MethodID); // 獲取異常堆棧 v10 = _JNIEnv::GetMethodID(a1, Class, "getStackTrace", "()[Ljava/lang/StackTraceElement;"); // 調(diào)用方法 v9 = _JNIEnv::CallObjectMethod(a1, v11, v10); ArrayLength = _JNIEnv::GetArrayLength(a1, v9); // 復(fù)制值,檢測(cè) Xposed 框架 strcpy(v18, "de.robv.android.xposed.XposedBridge"); // 復(fù)制值,檢測(cè) substrate 框架 strcpy(v17, "com.saurik.substrate"); for ( i = 0; i < ArrayLength; ++i ) { ObjectArrayElement = _JNIEnv::GetObjectArrayElement(a1, v9, i); ObjectClass = _JNIEnv::GetObjectClass(a1, ObjectArrayElement); // 每個(gè)堆棧中的信息反射獲取類(lèi)名 v1 = _JNIEnv::GetMethodID(a1, ObjectClass, "getClassName", "()Ljava/lang/String;"); v2 = _JNIEnv::CallObjectMethod(a1, ObjectArrayElement, v1); StringUTFChars = (const char *)_JNIEnv::GetStringUTFChars(a1, v2, 0); n = _strlen_chk(v18, 0x24u); // 比對(duì) if ( !strncmp(StringUTFChars, v18, n) ) { _android_log_print(6, "lilac", "%s", StringUTFChars); _android_log_print(6, "lilac", byte_389E); } v16 = _strlen_chk(v17, 0x15u); if ( !strncmp(StringUTFChars, v17, v16) ) { _android_log_print(6, "lilac", "%s", StringUTFChars); _android_log_print(6, "lilac", byte_38AE); } } return _stack_chk_guard;
}
上述代碼也很簡(jiǎn)單,獲取當(dāng)前的調(diào)用堆棧,并利用反射把每條堆棧信息的類(lèi)找到,比對(duì)類(lèi)名,是否使用Xposed框架和substrate框架,這里也沒(méi)有任何的操作。接下來(lái),我們直接去用unidbg去調(diào)用起來(lái)這個(gè)so。
模擬執(zhí)行so
參數(shù)獲取
首先,我們來(lái)看下入?yún)⒌臉?gòu)造:
String r1 = getHash(getApplicationContext().getPackageCodePath());
很明顯是應(yīng)用的一些信息,對(duì)于這種的系統(tǒng)級(jí)別的API,可以去官網(wǎng)查看,也可以通過(guò)Hook快速去獲取值,這里我們使用Frida去Hook應(yīng)用程序快速拿到值,畢竟這個(gè)不是重要的環(huán)節(jié)。
Frida的Hook代碼如下所示:
function main(){
Java.perform(function(){
var MainActivity = Java.use("com.example.dogpro.MainActivity");
MainActivity.onCreate.overload("android.os.Bundle").implementation = function(var_0){
console.log("info:",this.getApplicationContext().getPackageCodePath())
var ret = this.onCreate.overload("android.os.Bundle").call(this,var_0);
}
})
}
setImmediate(main)
Hook后的結(jié)果如圖3所示:
圖21-3 Hook的結(jié)果
getPackageCodePath()返回此上下文的主要 Android 包的完整路徑。 Android 包是一個(gè) ZIP
文件,其中包含應(yīng)用程序的主要代碼和資產(chǎn)。也就是我們看到的base.apk文件。
拿到入?yún)⒑缶涂梢蚤_(kāi)始構(gòu)造unidbg的模擬執(zhí)行代碼了。在很多場(chǎng)景下,為了快速獲取結(jié)果,甚至都不需要打開(kāi)IDA去分析又臭又長(zhǎng)的偽代碼,直接把so放到unidbg中去跑,減少對(duì)IDA的依賴(lài)。
如果單純的做算法的分析,毋庸置疑我們一定會(huì)使用到IDA。如果我們只是去獲取一個(gè)執(zhí)行的結(jié)果,我們使用unidbg去模擬就可以了。
unidbg 代碼初始化
unidbg的代碼初始化是把對(duì)應(yīng)的模擬器、內(nèi)存以及module等接口都配置好,這部分經(jīng)過(guò)網(wǎng)上案例大量的練習(xí),相信大家已經(jīng)可以熟能生巧了,這里再次給大家展示一下,代碼如下所示:
public class MainActivity extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Memory memory;
private final Module module;
public MainActivity(){
emulator = AndroidEmulatorBuilder
// 創(chuàng)建32位的模擬器
.for32Bit()
// 建立模擬器
.build();
// 實(shí)現(xiàn)內(nèi)存接口
memory = emulator.getMemory();
// 設(shè)置解析的庫(kù)的SDK
memory.setLibraryResolver(new AndroidResolver(23));
// 創(chuàng)建虛擬機(jī)
vm = emulator.createDalvikVM();
// 日志開(kāi)關(guān)
vm.setVerbose(true);
// 實(shí)現(xiàn) JNI
vm.setJni(this);
// 加載so
DalvikModule dalvikModule = vm.loadLibrary(
new File("unidbg-android/src/test/java/com/r0ysue/Chap22/apkfile/lib/armeabi-v7a/libdogpro.so"), false);
module = dalvikModule.getModule();
vm.callJNI_OnLoad(emulator,module);
}
public static void main(String[] args) {
MainActivity mainActivity = new MainActivity();
mainActivity.getHash();
}
}
目標(biāo)函數(shù)的調(diào)用
我們調(diào)用的就是so中的getHash函數(shù),它是一個(gè)non-static方法,需要一個(gè)實(shí)例來(lái)調(diào)用,讓我們先看看代碼是怎么寫(xiě)的:
private void getHash() {
// 找到調(diào)用它的類(lèi),和哪個(gè)類(lèi)綁定就使用哪個(gè)類(lèi)
DvmObject<?> dvmObject = vm.resolveClass("com/example/dogpro/MainActivity").newObject(null);
// 上面找到的入?yún)? String input = "/data/app/com.example.dogpro-pnF2J3-qBi8ei74vXTNXmQ==/base.apk";
//
DvmObject<?> ret = dvmObject.callJniMethodObject(emulator, "getHash(Ljava/lang/String;)Ljava/lang/String;", input);
System.out.println("result ==> "+ret.getValue());
}
補(bǔ)環(huán)境說(shuō)明
首先需要找到調(diào)用這個(gè)方法的類(lèi)是哪個(gè),和哪個(gè)類(lèi)綁定就使用哪個(gè)類(lèi)。因?yàn)榉椒ㄊ且粋€(gè)實(shí)例方法,我們通過(guò)newObject來(lái)實(shí)例化這個(gè)類(lèi)。調(diào)用是通過(guò)dvmObject來(lái)操作,對(duì)于JNI方法有如下幾種類(lèi)型,如圖4所示:
圖4 callJnixxx的幾種類(lèi)型
具體的選擇需要看函數(shù)的返回值,樣本中的getHash函數(shù)返回的類(lèi)型是String,而String的本質(zhì)就是一個(gè)Object,所以使用callJniMethodObject來(lái)操縱。
callJniMethodObject中需要傳遞三個(gè)參數(shù),第一個(gè)是emulator;第二個(gè)是方法及簽名,這個(gè)可以通過(guò)Jadx-
gui反編譯的結(jié)果查看,如圖5所示:
圖5 callJniMethodObject中的方法簽名
最后就是方法中參數(shù)的傳遞,它是一個(gè)可變長(zhǎng)度的參數(shù)列表。
至此,getHash的調(diào)用就構(gòu)造完成了,然后我們?nèi)ミ\(yùn)行代碼,看看是否可以正常的運(yùn)行。第一次運(yùn)行后,運(yùn)行結(jié)果主要報(bào)錯(cuò)信息如下所示:
java.lang.UnsupportedOperationException: java/util/zip/ZipFile-><init>(Ljava/lang/String;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObjectV(AbstractJni.java:791)
at com.github.unidbg.linux.android.dvm.AbstractJni.newObjectV(AbstractJni.java:746)
...
額外說(shuō)明一點(diǎn),如果我們沒(méi)有去繼承AbstractJNI,會(huì)出現(xiàn)setJNI的錯(cuò)誤,這里已經(jīng)補(bǔ)全了,就不會(huì)出現(xiàn)了。這段報(bào)錯(cuò)具體是什么含義呢?大致就是要找java/util/zip/ZipFile這個(gè)類(lèi)的構(gòu)造方法,但是找不到,所以報(bào)了上述的錯(cuò)誤。找不到類(lèi)之后,后續(xù)就不知道改怎么往下執(zhí)行,最終拋出了異常。這種就是Java的一個(gè)環(huán)境問(wèn)題。
實(shí)戰(zhàn)補(bǔ)環(huán)境
那我們?cè)趺慈パa(bǔ)充這個(gè)環(huán)境呢?其實(shí)它已經(jīng)很智能了,框架都填好了,只需要我們稍作改動(dòng)即可。補(bǔ)環(huán)境,即它要什么,我們就給他什么,根據(jù)這個(gè)提示來(lái)就好。給我們拋出的異常就是一種提示。找到下述異常的所在位置:
// com.github.unidbg.linux.android.dvm.AbstractJni.newObjectV(AbstractJni.java:791)
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "java/io/ByteArrayInputStream-><init>([B)V": {
ByteArray array = vaList.getObjectArg(0);
assert array != null;
return vm.resolveClass("java/io/ByteArrayInputStream").newObject(new ByteArrayInputStream(array.value));
}
...
}
}
這里僅僅展示了一個(gè)case,在這個(gè)方法中,有很多case,這是unidbg作者在設(shè)計(jì)的時(shí)候幫我們做好的,補(bǔ)好的這些具有很強(qiáng)的通用性。而沒(méi)補(bǔ)齊的是比較特殊的:可能是引用了三方的SDK中的函數(shù),也有可能是廠商自己的函數(shù),這就需要使用者自己去補(bǔ)充。補(bǔ)環(huán)境就是在這個(gè)方法中接著case繼續(xù)去寫(xiě)分支。但是這個(gè)是在unidbg的項(xiàng)目中,為了代碼的可移植性,建議大家寫(xiě)到自己的代碼中,因?yàn)橐呀?jīng)繼承了AbstraceJNI,只需要重寫(xiě)就可以了。對(duì)于上面的報(bào)錯(cuò),補(bǔ)的代碼如下所示:
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if (signature.equals("java/util/zip/ZipFile-><init>(Ljava/lang/String;)V")){
return vm.resolveClass("java/util/zip/ZipFile").newObject(null);
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}
但是大家思考一個(gè)問(wèn)題,這是一個(gè)構(gòu)造方法,并且報(bào)錯(cuò)異常中也提供了方法的簽名,它的入?yún)⑹且粋€(gè)String類(lèi)型的,沒(méi)有返回值,那我們只是簡(jiǎn)單的給他傳一個(gè)null對(duì)象肯定是不行的。先把他的入?yún)⒋蛴〕鰜?lái)看看:
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if (signature.equals("java/util/zip/ZipFile-><init>(Ljava/lang/String;)V")){
String name = (String) vaList.getObjectArg(0).getValue();
System.out.println("name => " + name);
return vm.resolveClass("java/util/zip/ZipFile").newObject(null);
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}
打印的結(jié)果如下所示:
name => /data/app/com.example.dogpro-pnF2J3-qBi8ei74vXTNXmQ==/base.apk
很明顯這里傳了一個(gè)APK文件,其主要作用就是解析獲取APK內(nèi)部的資源。那我們同樣去構(gòu)造一個(gè)這樣的數(shù)據(jù),把它作為對(duì)象讓unidbg去解析,補(bǔ)完后的結(jié)果如下所示:
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if (signature.equals("java/util/zip/ZipFile-><init>(Ljava/lang/String;)V")){
String name = (String) vaList.getObjectArg(0).getValue();
// System.out.println("name => " + name);
// return vm.resolveClass("java/util/zip/ZipFile").newObject(null);
try {
ZipFile zipFile = new ZipFile(name);
return vm.resolveClass("java/util/zip/ZipFile").newObject(zipFile);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}
再次運(yùn)行代碼,又有了新的異常報(bào)錯(cuò),如下所示:
[16:39:44 871] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:532) - handleInterrupt intno=2, NR=-1073744324, svcNumber=0x11f, PC=unidbg@0xfffe0284, LR=RX@0x400016e3[libdogpro.so]0x16e3, syscall=null
java.lang.NullPointerException
at com.github.unidbg.linux.android.dvm.DalvikVM$32.handle(DalvikVM.java:540)
at com.github.unidbg.linux.ARM32SyscallHandler.hook(ARM32SyscallHandler.java:131)
at com.github.unidbg.arm.backend.UnicornBackend$11.hook(UnicornBackend.java:345)
at unicorn.Unicorn$NewHook.onInterrupt(Unicorn.java:128)
at unicorn.Unicorn.emu_start(Native Method)
at com.github.unidbg.arm.backend.UnicornBackend.emu_start(UnicornBackend.java:376)
at com.github.unidbg.AbstractEmulator.emulate(AbstractEmulator.java:380)
...
[16:39:44 873] WARN [com.github.unidbg.AbstractEmulator] (AbstractEmulator:420) - emulate RX@0x40001281[libdogpro.so]0x1281 exception sp=unidbg@0xbffff610, msg=java.lang.NullPointerException, offset=7ms
java.nio.file.NoSuchFileException: /data/app/com.example.dogpro-pnF2J3-qBi8ei74vXTNXmQ==/base.apk
at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:92)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:111)
at java.base/sun.nio.fs.UnixException.rethrowAsIOException(UnixException.java:116)
我們來(lái)看最下面的模擬器拋出的異常,即NoSuchFileException,是java中最常見(jiàn)的異常,沒(méi)有找到這個(gè)文件的異常。大家注意:我們當(dāng)前使用的是unidbg,而不是一個(gè)手機(jī)的真實(shí)環(huán)境,所以打印出來(lái)的name,即文件的路徑我們根本沒(méi)有,那怎么辦呢?傳一個(gè)本地的APK到模擬器中,最終的代碼如下所示:
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if (signature.equals("java/util/zip/ZipFile-><init>(Ljava/lang/String;)V")){
String name = (String) vaList.getObjectArg(0).getValue();
// System.out.println("name => " + name);
// return vm.resolveClass("java/util/zip/ZipFile").newObject(null);
try {
if(name.equals("/data/app/com.example.dogpro-pnF2J3-qBi8ei74vXTNXmQ==/base.apk")){
ZipFile zipFile = new ZipFile("unidbg-android/src/test/java/com/r0ysue/unidbgBook/Chap22/dogpro.apk");
// ZipFile zipFile = new ZipFile(name);
return vm.resolveClass("java/util/zip/ZipFile").newObject(zipFile);
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}
這里直接根據(jù)簽名信息返回了一個(gè)ZipFile的對(duì)象,然后我們把unidbg中補(bǔ)好的方法return回去,即執(zhí)行unidbg中補(bǔ)好的環(huán)境。別的問(wèn)題暫時(shí)沒(méi)有出現(xiàn),我們先不做處理。補(bǔ)充完后再次運(yùn)行代碼,報(bào)錯(cuò)如下所示:
java.lang.UnsupportedOperationException: java/util/zip/ZipFile->entries()Ljava/util/Enumeration;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.r0ysue.unidbgBook.Chap22.MainActivity.callObjectMethodV(MainActivity.java:124)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
...
報(bào)錯(cuò)的異常顯示,缺少了ZipFie的entries方法,這個(gè)方法是空參,返回值類(lèi)型是Enumeration對(duì)象。我們繼續(xù)使用上面的方法去補(bǔ)環(huán)境,補(bǔ)完后的代碼如下所示:
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/util/zip/ZipFile->entries()Ljava/util/Enumeration;")){
// 拿到操作的對(duì)象
ZipFile zipFile = (ZipFile) dvmObject.getValue();
// 通過(guò)對(duì)象來(lái)調(diào)用方法
Enumeration<? extends ZipEntry> entries = zipFile.entries();
return vm.resolveClass("java/util/Enumeration").newOnject(entries);
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
注意這里要補(bǔ)的環(huán)境是在callObjectMethodV方法中,根據(jù)名稱(chēng)我們也能知道,它是在調(diào)用對(duì)象中的方法,參數(shù)dvmObject就是對(duì)象,而這里是調(diào)用ZipFile形成的對(duì)象中的entries方法,ZipFile對(duì)象是在上一個(gè)方法中做的實(shí)例化,并傳入了待解析的APK文件。我們要正真的獲取這個(gè)對(duì)象就需要通過(guò)getValue()方法來(lái)獲取。然后通過(guò)這個(gè)對(duì)象來(lái)調(diào)用entries方法。根據(jù)簽名可以知道要返回的類(lèi)型是Enumeration,直接通過(guò)前面的方法返回回去。繼續(xù)運(yùn)行代碼,看看用這種方式能不能起作用:
[17:09:25 902] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:532) - handleInterrupt intno=2, NR=-1073744324, svcNumber=0x122, PC=unidbg@0xfffe02b4, LR=RX@0x40001253[libdogpro.so]0x1253, syscall=null
java.lang.ClassCastException: class com.github.unidbg.linux.android.dvm.DvmObject cannot be cast to class com.github.unidbg.linux.android.dvm.Enumeration (com.github.unidbg.linux.android.dvm.DvmObject and com.github.unidbg.linux.android.dvm.Enumeration are in unnamed module of loader 'app')
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethodV(AbstractJni.java:609)
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethodV(AbstractJni.java:602)
最開(kāi)始顯示了報(bào)錯(cuò)的主要信息,即xxx.DvmObject cannot be cast to
xxx.Enumeration,它們的關(guān)系不能通過(guò)強(qiáng)轉(zhuǎn)來(lái)實(shí)現(xiàn)。一般出現(xiàn)這種疑難雜癥,我們的首要手段是到unidbg的框架中搜索,看看它有沒(méi)有相關(guān)的處邏輯,在AbstractJNI中,我們果真找到了它的實(shí)現(xiàn)邏輯:
@Override
public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/util/Enumeration->hasMoreElements()Z":
return ((Enumeration) dvmObject).hasMoreElements();
}
}
在AbstractJNI的同級(jí)目錄下,可以發(fā)現(xiàn)unidbg對(duì)它的實(shí)現(xiàn)
package com.github.unidbg.linux.android.dvm;
import java.util.Iterator;
import java.util.List;
public class Enumeration extends DvmObject<List<?>> {
private final Iterator<? extends DvmObject<?>> iterator;
public Enumeration(VM vm, List<? extends DvmObject<?>> value) {
super(vm.resolveClass("java/util/Enumeration"), value);
this.iterator = value == null ? null : value.iterator();
}
public boolean hasMoreElements() {
return iterator != null && iterator.hasNext();
}
public DvmObject<?> nextElement() {
return iterator.next();
}
}
在Java的中同樣有Enumeration類(lèi)的實(shí)現(xiàn),為什么會(huì)這樣呢?這是因?yàn)閡nidbg對(duì)簡(jiǎn)單的數(shù)據(jù)類(lèi)型都做了封裝,并優(yōu)先使用。比如,我們要返回一個(gè)String的對(duì)象,一般是這樣來(lái)寫(xiě)的:
return new StringObject(vm,"");
而不是使用resolveClass的方式:
return vm.resolveClass("java/lang/String"),newOnject(");
這是為了后續(xù)的處理,unidbg是一個(gè)完善的系統(tǒng),每個(gè)環(huán)節(jié)都有相應(yīng)的承接,如果使用后者,那么后續(xù)的操作需要去做強(qiáng)轉(zhuǎn)就無(wú)法識(shí)別,從而強(qiáng)轉(zhuǎn)失敗。我們既然知道了unidbg中基本的數(shù)據(jù)類(lèi)型,就要使用它。
到了這步,就該往里面?zhèn)魅雲(yún)?shù)了,看下unidbg的Enumeration構(gòu)造方法:
public Enumeration(VM vm, List<? extends DvmObject<?>> value) {
super(vm.resolveClass("java/util/Enumeration"), value);
this.iterator = value == null ? null : value.iterator();
}
需要傳入的是一個(gè)List類(lèi)型的對(duì)象,所以我們?nèi)パa(bǔ)環(huán)境的時(shí)候同樣也需要給他一個(gè)List對(duì)象,最終的代碼如下所示:
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/util/zip/ZipFile->entries()Ljava/util/Enumeration;")){
ZipFile zipFile = (ZipFile) dvmObject.getValue();
Enumeration<? extends ZipEntry> entries = zipFile.entries();
// return vm.resolveClass("java/util/Enumeration").newObject(entries);
DvmClass ZipEntryClass = vm.resolveClass("java/util/zip/ZipEntry");
List<DvmObject<?>> objs = new ArrayList<>();
while (entries.hasMoreElements()){
ZipEntry zipEntry = entries.nextElement();
objs.add(ZipEntryClass.newObject(zipEntry));
}
return new com.github.unidbg.linux.android.dvm.Enumeration(vm,objs);
}
同理,我們繼續(xù)往下補(bǔ),先運(yùn)行上述的代碼,拋出的異常如下所示:
java.lang.UnsupportedOperationException: java/util/zip/ZipEntry->getName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.r0ysue.unidbgBook.Chap22.MainActivity.callObjectMethodV(MainActivity.java:128)
同樣是在調(diào)用對(duì)象的方法,我們需要先獲取對(duì)象,然后再用對(duì)象調(diào)用對(duì)應(yīng)的方法,它需要的返回值是一個(gè)Sting類(lèi)型,即通過(guò)StrongObject返回,代碼如下所示:
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/util/zip/ZipEntry->getName()Ljava/lang/String;")){
ZipEntry zipEntry = (ZipEntry) dvmObject.getValue();
String name = zipEntry.getName();
return new StringObject(vm,name);
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
繼續(xù)運(yùn)行代碼,報(bào)錯(cuò)如下所示:
java.lang.UnsupportedOperationException: java/lang/String->endsWith(Ljava/lang/String;)Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethodV(AbstractJni.java:624)
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethodV(AbstractJni.java:602)
這個(gè)報(bào)錯(cuò)出現(xiàn)在callBooleanMethodV方法中,代碼如下所示:
@Override
public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/lang/String->endsWith(Ljava/lang/String;)Z")){
String value = (String) dvmObject.getValue();
String suffix = (String) vaList.getObjectArg(0).getValue();
return value.endsWith(suffix);
}
return super.callBooleanMethodV(vm, dvmObject, signature, vaList);
}
同樣,我們需要先拿到對(duì)象,而且endsWith函數(shù)中有參數(shù)的傳遞,我們需要把參數(shù)也構(gòu)造出來(lái),vaList是一個(gè)可變的參數(shù)列表,拿第一個(gè)參數(shù)即可。繼續(xù)運(yùn)行代碼,報(bào)錯(cuò)異常如下所示:
java.lang.UnsupportedOperationException: java/util/zip/ZipFile->getInputStream(Ljava/util/zip/ZipEntry;)Ljava/io/InputStream;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.r0ysue.unidbgBook.Chap22.MainActivity.callObjectMethodV(MainActivity.java:128)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
這個(gè)也是在callObjectMethodV方法中,代碼如下所示:
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/util/zip/ZipFile->getInputStream(Ljava/util/zip/ZipEntry;)Ljava/io/InputStream;")){
ZipFile zipFile = (ZipFile) dvmObject.getValue();
ZipEntry zipEntry = (ZipEntry) vaList.getObjectArg(0).getValue();
try {
InputStream inputStream = zipFile.getInputStream(zipEntry);
return vm.resolveClass("java/io/InputStream").newObject(inputStream);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
由于涉及到了IO的操作,需要包裹到try…catch…中,繼續(xù)運(yùn)行代碼,報(bào)錯(cuò)如下所示:
java.lang.UnsupportedOperationException: java/io/InputStream->read([B)I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:562)
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:528)
at com.github.unidbg.linux.android.dvm.DvmMethod.callIntMethodV(DvmMethod.java:109)
at com.github.unidbg.linux.android.dvm.DalvikVM$47.handle(DalvikVM.java:821)
報(bào)錯(cuò)發(fā)生在callIntMethodV中,需要補(bǔ)的是InputStream中的read方法,根據(jù)參數(shù)和返回值,補(bǔ)充的代碼如下所示:
@Override
public int callIntMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/io/InputStream->read([B)I")){
InputStream inputStream = (InputStream) dvmObject.getValue();
byte[] bytes = (byte[]) vaList.getObjectArg(0).getValue();
try {
int read = inputStream.read(bytes);
return read;
} catch (IOException e) {
e.printStackTrace();
return -1;
}
}
return super.callIntMethodV(vm, dvmObject, signature, vaList);
}
繼續(xù)運(yùn)行代碼,報(bào)錯(cuò)信息如下所示:
java.lang.UnsupportedOperationException: java/security/MessageDigest->update([B)V
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:995)
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:978)
at com.github.unidbg.linux.android.dvm.DvmMethod.callVoidMethodV(DvmMethod.java:228)
at com.github.unidbg.linux.android.dvm.DalvikVM$59.handle(DalvikVM.java:1045)
調(diào)用了Java SDK中的MessageDigest類(lèi),我們繼續(xù)補(bǔ),代碼如下所示:
@Override
public void callVoidMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/security/MessageDigest->update([B)V")){
MessageDigest md = (MessageDigest) dvmObject.getValue();
byte[] bytes = (byte[]) vaList.getObjectArg(0).getValue();
md.update(bytes);
return;
}
super.callVoidMethodV(vm, dvmObject, signature, vaList);
}
繼續(xù)運(yùn)行代碼,報(bào)錯(cuò)如下所示:
java.lang.UnsupportedOperationException: java/security/MessageDigest->digest()[B
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:416)
at com.r0ysue.unidbgBook.Chap22.MainActivity.callObjectMethodV(MainActivity.java:128)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
還缺少了digest方法,我們繼續(xù)來(lái)補(bǔ)齊,代碼如下所示:
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/security/MessageDigest->digest()[B")){
MessageDigest md = (MessageDigest) dvmObject.getValue();
byte[] digest = md.digest();
return new ByteArray(vm, digest);
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
再次運(yùn)行代碼,返回的結(jié)果就出來(lái)了,于此同時(shí),給大家打開(kāi)了setVarbose開(kāi)關(guān),把JNI的執(zhí)行流也輸出了,結(jié)果如下所示:
JNIEnv->CallObjectMethodV(java.util.Enumeration@c730b35, nextElement() => java.util.zip.ZipEntry@3f6f6701) was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV(java.util.zip.ZipEntry@3f6f6701, getName() => "res/drawable/design_ic_visibility.xml") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV("res/drawable/design_ic_visibility.xml", toLowerCase() => "res/drawable/design_ic_visibility.xml") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->GetStringUtfChars("res/drawable/design_ic_visibility.xml") was called from RX@0x40001749[libdogpro.so]0x1749
JNIEnv->CallBooleanMethodV("res/drawable/design_ic_visibility.xml", endsWith("dog.png") => false) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->CallBooleanMethodV(java.util.Enumeration@c730b35, hasMoreElements() => true) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->CallObjectMethodV(java.util.Enumeration@c730b35, nextElement() => java.util.zip.ZipEntry@1ed6388a) was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV(java.util.zip.ZipEntry@1ed6388a, getName() => "res/drawable/design_ic_visibility_off.xml") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV("res/drawable/design_ic_visibility_off.xml", toLowerCase() => "res/drawable/design_ic_visibility_off.xml") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->GetStringUtfChars("res/drawable/design_ic_visibility_off.xml") was called from RX@0x40001749[libdogpro.so]0x1749
JNIEnv->CallBooleanMethodV("res/drawable/design_ic_visibility_off.xml", endsWith("dog.png") => false) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->CallBooleanMethodV(java.util.Enumeration@c730b35, hasMoreElements() => true) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->CallObjectMethodV(java.util.Enumeration@c730b35, nextElement() => java.util.zip.ZipEntry@4f80542f) was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV(java.util.zip.ZipEntry@4f80542f, getName() => "res/drawable/design_password_eye.xml") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV("res/drawable/design_password_eye.xml", toLowerCase() => "res/drawable/design_password_eye.xml") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->GetStringUtfChars("res/drawable/design_password_eye.xml") was called from RX@0x40001749[libdogpro.so]0x1749
JNIEnv->CallBooleanMethodV("res/drawable/design_password_eye.xml", endsWith("dog.png") => false) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->CallBooleanMethodV(java.util.Enumeration@c730b35, hasMoreElements() => true) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->CallObjectMethodV(java.util.Enumeration@c730b35, nextElement() => java.util.zip.ZipEntry@130c12b7) was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV(java.util.zip.ZipEntry@130c12b7, getName() => "res/drawable/design_snackbar_background.xml") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV("res/drawable/design_snackbar_background.xml", toLowerCase() => "res/drawable/design_snackbar_background.xml") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->GetStringUtfChars("res/drawable/design_snackbar_background.xml") was called from RX@0x40001749[libdogpro.so]0x1749
JNIEnv->CallBooleanMethodV("res/drawable/design_snackbar_background.xml", endsWith("dog.png") => false) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->CallBooleanMethodV(java.util.Enumeration@c730b35, hasMoreElements() => true) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->CallObjectMethodV(java.util.Enumeration@c730b35, nextElement() => java.util.zip.ZipEntry@5d534f5d) was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV(java.util.zip.ZipEntry@5d534f5d, getName() => "res/drawable/dog.png") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->CallObjectMethodV("res/drawable/dog.png", toLowerCase() => "res/drawable/dog.png") was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->GetStringUtfChars("res/drawable/dog.png") was called from RX@0x40001749[libdogpro.so]0x1749
JNIEnv->CallBooleanMethodV("res/drawable/dog.png", endsWith("dog.png") => true) was called from RX@0x40001253[libdogpro.so]0x1253
JNIEnv->GetMethodID(java/util/zip/ZipFile.getInputStream(Ljava/util/zip/ZipEntry;)Ljava/io/InputStream;) => 0xb225c4d4 was called from RX@0x40001195[libdogpro.so]0x1195
JNIEnv->CallObjectMethodV(java.util.zip.ZipFile@557caf28, getInputStream(java.util.zip.ZipEntry@5d534f5d) => java.io.InputStream@a38c7fe) was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->GetMethodID(java/io/InputStream.read([B)I) => 0x7b2c3fda was called from RX@0x40001195[libdogpro.so]0x1195
JNIEnv->NewByteArray(256) was called from RX@0x40001769[libdogpro.so]0x1769
JNIEnv->FindClass(java/security/MessageDigest) was called from RX@0x40001127[libdogpro.so]0x1127
JNIEnv->GetStaticMethodID(java/security/MessageDigest.getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;) => 0x5c20796 was called from RX@0x40001799[libdogpro.so]0x1799
JNIEnv->NewStringUTF("MD5") was called from RX@0x40001165[libdogpro.so]0x1165
JNIEnv->CallStaticObjectMethodV(class java/security/MessageDigest, getInstance("MD5") => java.security.MessageDigest@25641d39) was called from RX@0x400017eb[libdogpro.so]0x17eb
JNIEnv->CallIntMethodV(java.io.InputStream@a38c7fe, read([B@7b36aa0c) => 0x100) was called from RX@0x4000185f[libdogpro.so]0x185f
JNIEnv->GetMethodID(java/security/MessageDigest.update([B)V) => 0x7d1a6599 was called from RX@0x40001195[libdogpro.so]0x1195
JNIEnv->CallVoidMethodV(java.security.MessageDigest@25641d39, update([B@7b36aa0c)) was called from RX@0x400011e7[libdogpro.so]0x11e7
JNIEnv->GetMethodID(java/security/MessageDigest.digest()[B) => 0x6ccd1d46 was called from RX@0x40001195[libdogpro.so]0x1195
JNIEnv->CallObjectMethodV(java.security.MessageDigest@25641d39, digest() => [B@5824a83d) was called from RX@0x400016e3[libdogpro.so]0x16e3
JNIEnv->GetArrayLength([B@5824a83d => 16) was called from RX@0x400018c7[libdogpro.so]0x18c7
JNIEnv->NewStringUTF("D3E550889725A6A7C5E834ECCDB4B73E") was called from RX@0x40001165[libdogpro.so]0x1165
result ==> D3E550889725A6A7C5E834ECCDB4B73E
明顯的可以看到,JNI的執(zhí)行流就是我們剛才補(bǔ)充環(huán)境的順序,當(dāng)然有一些是unidbg幫我們補(bǔ)好的。
跟著筆者的思路一路下來(lái)終于把結(jié)果運(yùn)行了出來(lái),過(guò)程中不知補(bǔ)了多少函數(shù)。但是獲取最終結(jié)果的那一剎那,前面做的所有的努力都是值得的。有意思的是,我們并不知道這其中會(huì)有多少函數(shù),也不知道我們會(huì)在哪一個(gè)節(jié)點(diǎn)放棄,這可能就是unidbg的魅力吧。就像人生一樣,我們并不知道是否會(huì)有一個(gè)結(jié)果,在每一個(gè)時(shí)間節(jié)點(diǎn)都在努力,但是不知道這樣的堅(jiān)持是否會(huì)有結(jié)果,期待每一個(gè)讀者都會(huì)有一個(gè)完美的結(jié)局。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-449801.html
最后
對(duì)于從來(lái)沒(méi)有接觸過(guò)網(wǎng)絡(luò)安全的同學(xué),我們幫你準(zhǔn)備了詳細(xì)的學(xué)習(xí)成長(zhǎng)路線圖??梢哉f(shuō)是最科學(xué)最系統(tǒng)的學(xué)習(xí)路線,大家跟著這個(gè)大的方向?qū)W習(xí)準(zhǔn)沒(méi)問(wèn)題。
同時(shí)每個(gè)成長(zhǎng)路線對(duì)應(yīng)的板塊都有配套的視頻提供:
當(dāng)然除了有配套的視頻,同時(shí)也為大家整理了各種文檔和書(shū)籍資料&工具,并且已經(jīng)幫大家分好類(lèi)了。
因篇幅有限,僅展示部分資料,有需要的小伙伴,可以【掃下方二維碼】免費(fèi)領(lǐng)取:
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-449801.html
到了這里,關(guān)于Unidbg補(bǔ)環(huán)境實(shí)戰(zhàn)第一篇:補(bǔ)環(huán)境入門(mén)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!