抖音資料采集Frida教程,Frida Java Hook 詳解:代碼及示例(上)
短視頻、直播資料實時采集介面,請查看檔案: TiToData
免責宣告:本檔案僅供學習與參考,請勿用于非法用途!否則一切后果自負,
**前言
1.1 FRIDA SCRIPT的"hello world"
1.1.1 "hello world"腳本代碼示例
1.1.2 "hello world"腳本代碼示例詳解
1.2 Java層攔截普通方法
1.2.1 攔截普通方法腳本示例
1.2.2 執行攔截普通方法腳本示例
1.3 Java層攔截建構式
1.3.1 攔截建構式腳本代碼示例
1.3.2 攔截建構式腳本代碼示例解詳解
1.4 Java層攔截方法多載
1.4.1 攔截方法多載腳本代碼示例
1.5 Java層攔截構造物件引數
1.5.1 攔截構造物件引數腳本示例
1.6 Java層修改成員變數的值以及函式的回傳值
1.6.1 修改成員變數的值以及函式的回傳值腳本代碼示例
1.6.2 修改成員變數的值以及函式的回傳值之小實戰
結語
咱們在這篇來深入學習如何HOOK Java層函式,應用于與各種不同的Java層函式,結合實際APK案例使用FRIDA框架對其APP進行附加、hook、以及FRIDA腳本的詳細撰寫,
1.1 FRIDA SCRIPT的"hello world"
在本章節中,依然會大量使用注入模式附加到一個正在運行行程程式,亦或是在APP程式啟動的時候對其APP行程進行劫持,再在目標行程中執行我們的js檔案代碼邏輯,FRIDA腳本就是利用FRIDA動態插樁框架,使用FRIDA匯出的API和方法,對記憶體空間里的物件方法進行監視、修改或者替換的一段代碼,FRIDA的API是使用JavaScript實作的,所以我們可以充分利用JS的匿名函式的優勢、以及大量的hook和回呼函式的API,那么大家跟我一起來操作吧,先打開在vscode中創建一個js檔案:helloworld.js:
1.1.1 "hello world"腳本代碼示例
setTimeout(function(){
Java.perform(function(){
console.log("hello world!");
});
});
1.1.2 "hello world"腳本代碼示例詳解
這基本上就是一個FRIDA版本的Hello World!,我們把一個匿名函式作為引數傳給了setTimeout()函式,然而函式體中的Java.perform()這個函式本身又接受了一個匿名函式作為引數,該匿名函式中最侄訓呼叫console.log()函式來列印一個Hello world!字串,我們需要呼叫setTimeout()方法因為該方法將我們的函式注冊到JavaScript運行時中去,然后需要呼叫Java.perform()方法將函式注冊到Frida的Java運行時中去,用來執行函式中的操作,當然這里只是打了一條log,然后我們在手機上將frida-server運行起來,在電腦上進行操作:
roysue@ubuntu:~$ adb shell
sailfish:/ $ su
sailfish:/ $ ./data/local/tmp/frida-server
這個時候,我們需要再開啟一個終端運行另外一條命令:frida -U com.roysue.roysueapplication -l helloworld.js
這句代碼是指通過USB連接對Android設備中的com.roysue.roysueapplication行程對其附加并且注入一個helloworld.js腳本,注入完成之后會立刻執行helloworld.js腳本所寫的代碼邏輯!
我們可以看到成功注入了腳本以及附加到自己所撰寫包名為:com.roysue.roysueapplication的apk應用程式中,并且列印了一條 hell world!,
roysue@ubuntu:~$ frida -U -l helloworld.js com.roysue.roysueapplication
____
/ _ | Frida 12.7.24 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://www.frida.re/docs/home/
Attaching...
hello world!
執行了該命令之后,FRIDA回傳一個了CLI終端工具與我們互動,在上面可見列印出來了一些資訊,顯示了我們的FRIDA版本資訊還有一個反向R的圖形,往下看,需要退出的時候我們只需要在終端輸入exit即可完成退出對APP的附加,其后我們看到的是Attaching...正在對目標行程附加,當附加成功了列印了一句hello world!,至此,我們的helloworld.js通過FRIDA的-l命令成功的注入到目標行程并且執行完畢,學會了注入可不要高興的太早喲~~咱們繼續深入學習HOOK Java代碼中的普通函式,
1.2 Java層攔截普通方法
Java層的普通方法相當常見,也是我們要學習的基礎中的基礎,我們先來看幾個比較常見的普通的方法,見下圖1-1,
圖1-1 JADX-GUI軟體打開的反編譯代碼
通過圖1-1我們能看三個函式分別是建構式a()、普通函式a()和b(),諸如這種普通函式會特別多,那我們在本小章節中嘗試hook普通函式、查看函式中的引數的具體值,
在嘗試寫FRIDA HOOK腳本之前咱們先來看看需要hook的代碼吧~,Ordinary_Class類中有四個函式,都是很普通的函式,add函式的功能也很簡單,引數a+b;sub函式功能是引數a-b;而getNumber只回傳100;getString方法回傳了 getString()+引數的str,見下圖1-2,
圖1-2 反編譯的Ordinary_Class類的代碼
然后咱們再看MainActivity中的撰寫的代碼,通過反編譯出來的代碼一共有四個按鈕(Button),當btn_add點擊時會運行Ordinary_Class類中add方法,計算100+200的結果,通過String.valueOf函式把計算結構轉字串然后通過Toast彈出資訊;點擊btn_sub按鈕的時候觸發點擊事件會運行Ordinary_Class類中sub方法,計算100-100的結果,通過String.valueOf函式把計算結構轉字串然后通過Toast彈出資訊,在MainActivity類中的onCreate方法中的四個按鈕分別對應情況是ADD按鈕對應btn_add點擊事件,SUB對應btn_sub的點擊事件,見下圖1-3,
圖1-3 MainActivity中的撰寫的代碼
按照正常流程當我們點擊ADD的按鈕界面會彈出一條資訊顯示,其中的值是300,因為我們在ADD的點擊事件中添加了Toast,將ADD方法運行的結果放在Toast引數中,通過它顯示了我們的計算結果;而SUB函式會顯示0,見下圖1-4,圖1-5,
圖1-4 點擊ADD按鈕時顯示的結果
圖1-5 點擊SUB按鈕時顯示的結果
我們現在知道已經知道它的運行流程以及函式的執行結果和所填寫的引數,我們現在來正式撰寫一個基本的使用Frida鉤子來攔截圖1-2中add和sub函式的呼叫并且在終端顯示每個函式所傳入的引數、回傳的值,開始寫roysue_0.js:
1.2.1 攔截普通方法腳本示例
setTimeout(function(){
//判斷是否加載了Java VM,如果沒有加載則不運行下面的代碼
if(Java.available) {
Java.perform(function(){
//先列印一句提示開始hook
console.log("start hook");
//先通過Java.use函式得到Ordinary_Class類
var Ordinary_Class = Java.use("com.roysue.roysueapplication.Ordinary_Class");
//這里我們需要進行一個NULL的判斷,通常這樣做會排除一些不必要的BUG
if(Ordinary_Class != undefined) {
//格式是:類名.函式名.implementation = function (a,b){
//在這里使用鉤子攔截add方法,注意方法名稱和引數個數要一致,這里的a和b可以自己任意填寫,
Ordinary_Class.add.implementation = function (a,b){
//在這里先得到運行的結果,因為我們要輸出這個函式的結果
var res = this.add(a,b);
//把計算的結果和引數一起輸出
console.log("執行了add方法 計算result:"+res);
console.log("執行了add方法 計算引數a:"+a);
console.log("執行了add方法 計算引數b:"+b);
//回傳結果,因為add函式本身是有回傳值的,否則APP運行的時候會報錯
return res;
}
Ordinary_Class.sub.implementation = function (a,b){
var res = this.sub(a,b);
console.log("執行了sub方法 計算result:"+res);
console.log("執行了sub方法 計算引數a:"+a);
console.log("執行了sub方法 計算引數b:"+b);
return res;
}
Ordinary_Class.getString.implementation = function (str){
var res = this.getString(str);
console.log("result:"+res);
return res;
}
} else {
console.log("Ordinary_Class: undefined");
}
console.log("hook end");
});
}
});
1.2.2 執行攔截普通方法腳本示例
寫完腳本后我們執行:frida -U com.roysue.roysueapplication -l roysue_0.js,當我們執行了腳本后會進入cli控制臺與frida互動,可以看到已經對該app附加并且成功注入腳本,立刻列印出了start hook和hook end,正是我們剛剛所寫的,再繼續點擊app應用中的ADD按鈕和SUB按鈕會在終端立刻輸出計算的結果和引數,在這里甚至可以看到清晰,引數、回傳值一覽無余,
roysue@ubuntu:~$ frida -U com.roysue.roysueapplication -l roysue_0.js
____
/ _ | Frida 12.7.24 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://www.frida.re/docs/home/
Attaching...
start hook
hook end
[Google Pixel::com.roysue.roysueapplication]-> 執行了add方法 計算result:300
執行了add方法 計算引數a:100
執行了add方法 計算引數b:200
執行了sub方法 計算result:0
執行了sub方法 計算引數a:100
執行了sub方法 計算引數b:100
這樣我們就已經成功的列印了來了我們想要知道的值,每個引數的值和回傳值的結構,我們就對普通函式的鉤子完成了一個基本操作,大家可以自己多多嘗試對其他的普通的函式進行hook,多多練習,那咱們這一章節就愉快的完成了~~繼續深入吧,
1.3 Java層攔截建構式
那咱們這章來玩如何HOOK類的建構式,很多時候在實體化類的瞬間就會把引數傳遞到內部為成員變數賦值,這樣一來就省的類中的成員變數一個個去賦值,在Android逆向中,也有很多的類似的場景,利用有參建構式實體化類并且賦值,我建立了一個class類,類名是User,其中代碼這樣寫:
public class User {
public int age;
public String name;
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("User{name='");
sb.append(this.name);
sb.append('\'');
sb.append(", age=");
sb.append(this.age);
sb.append('}');
return sb.toString();
}
public User(String name2, int age2) {
this.name = name2;
this.age = age2;
}
public User() {
}
}
我們可以看到User類中有2個成員變數分別是age和name,還有2個構造方法,分別是無參構造有有參構造,我們現在要做的是在User類進行有參實體化時查看所填入的引數分別是什么值,在圖1-3中,可以看到btn_init的點擊事件時會對User類進行實體化引數分別填寫了roysue和30,然后再繼續呼叫了toString方法把它們組合到一起并且通過Toast彈出資訊顯示值,TEST_INTI對應btn_init的點擊事件,點擊時效果見下圖1-6,
圖1-6 點擊TEST_INIT時顯示的值
1.3.1 攔截建構式腳本代碼示例
現在開始撰寫frida鉤子來攔截User類的建構式的腳本,來列印出建構式的引數,撰寫roysue_1.js,
setTimeout(function(){
if(Java.available) {
Java.perform(function(){
console.log("start hook");
//同樣的在這里先獲取User類物件
var User = Java.use("com.roysue.roysueapplication.User");
if(User != undefined) {
//注意在使用鉤子攔截建構式時需要使用到 $init 也要注意引數的個數,因為該建構式是2個所以此處填2個引數
User.$init.implementation = function (name,age){
//這里列印成員變數name和age的在運行中被呼叫填寫的值
console.log("name:"+name);
console.log("age:"+age);
//最終要執行原本的init方法否則運行時會報例外導致原程式無法正常運行,
this.$init(name, age);
}
} else {
console.log("User: undefined");
}
console.log("hook end");
});
}
});
1.3.2 攔截建構式腳本代碼示例解詳解
腳本寫好之后打開終端執行frida -U com.roysue.roysueapplication -l roysue_1.js,把剛剛寫的腳本注入的目標行程,然后我們在APP應用中按下TEST_INIT按鈕,注入的腳本會立即攔截建構式并且列印2個引數的具體的值,見下圖1-7,

圖1-7 終端顯示
列印的值就是圖1-3中所填的roysue和30,這就說明我們使用FRIDA鉤子攔截到了User類的有參建構式并且有效的列印了引數的值,需要注意的是在輸入列印引數的值之后一定要記得執行原本的有參建構式,這樣程式才可以正常執行,
1.4 Java層攔截方法多載
在學習HOOK之前,咱們先了解一下什么是方法多載,方法多載是指在同一個類內定義了多個相同的方法名稱,但是每個方法的引數型別和引數的個數都不同,在呼叫方法多載的函式編譯器會根據所填入的引數的個數以及型別來匹配具體呼叫對應的函式,總結起來就是方法名稱一樣但是引數不一樣,在逆向JAVA代碼的時候時常會遇到方法多載函式,見下圖1-8,
圖1-8 反編譯后多載函式樣本代碼
在圖1-8中,我們能看到一共有三個方法多載的函式,有時候實際情況甚至更多,咱們也不要怕,擼起袖子加油干,對于這種多載函式在FRIDA中,js會寫.overload來攔截方法多載函式,當我們使用overload關鍵字的時候FRIDA會非常智能把當前所有的多載函式的所需要的型別列印出來,在了解了這個之后我們來開始實戰使用FRIDA鉤子攔截我們想攔截的多載函式的腳本吧!還是之前的那個app,還是之前的那個類,原汁原味~~,我新增了一些add的方法,使add方法多載,見下圖1-9,
圖1-9 反編譯后的Ordinary_Class中的多載函式樣本代碼
在圖1-9中add有三個重名的方法名稱,但是引數的個數不同,在圖1-3中的btn_add點擊事件會執行擁有2個引數的方法多載的add函式,當我在腳本中寫Ordinary_Class.add..implementation = function (a,b),然后繼續注入所寫好的腳本,FRIDA在終端提示了紅色的字體,一看嚇一跳!但咱們仔細看,它說add是一個方法多載的函式,有三個引數不同的add函式,讓我們寫.overload(xxx),以識別hook的到底是哪個add的方法多載函式,
當我們寫了這樣的js腳本去運行的時候,frida提示報錯了,因為有三個多載函式,我用紅色的框圈出了,可以看到frida十分的智能,三個多載的引數型別完全一致的列印出來了,當它列印出來之后我們就可以復制它的這個智能提示的overloadxxx多載來修改我們自己的腳本了,進一步完善我們的腳本代碼如下,
1.4.1 攔截方法多載腳本代碼示例
function hook_overload() {
if(Java.available) {
Java.perform(function () {
console.log("start hook");
var Ordinary_Class = Java.use("com.roysue.roysueapplication.Ordinary_Class");
if(Ordinary_Class != undefined) {
//要做的僅僅是將frida提示出來的overload復制在此處
Ordinary_Class.add.overload('int', 'int').implementation = function (a,b){
var res = this.add(a,b);
console.log("result:"+res);
return res;
}
Ordinary_Class.add.overload('int', 'int', 'int').implementation = function (a,b,d){
var res = this.add(a,b,d);
console.log("result:"+res);
return res;
}
Ordinary_Class.add.overload('int', 'int', 'int', 'int').implementation = function (a,b,d,c){
var res = this.add(a,b,d,c);
console.log("result:"+res);
return res;
}
} else {
console.log("Ordinary_Class: undefined");
}
console.log("start end");
});
}
}
setImmediate(hook_overload);
修改完相應的地方之后我們保存代碼時終端會自動再次運行js中的代碼,不得不說frida太強大了~,當js再次運行的時候我們在app應用中點擊圖1-4中ADD按鈕時會立刻列印出結果,因為FRIDA鉤子已經對該類中的所有的add函式進行了攔截,執行了自己所寫的代碼邏輯,點擊效果如下圖1-11,

圖1-11 終端顯示效果
在這一章節中我們學會了處理方法多載的函式,我們只要依據FRIDA的終端提示,將智能提示出來的代碼銜接到自己的代碼就能夠對方法多載函式進行攔截,執行我們自己想要執行的代碼,
1.5 Java層攔截構造物件引數
很多時候,我們不但要HOOK使用鉤子攔截函式對函式的引數和回傳值進行記錄,而且還要自己主動呼叫類中的函式使用,FRIDA中有一個new()關鍵字,而這個關鍵字就是實體化類的重要方法,
在官方API中有這樣寫道:“Java.use(ClassName):動態獲取className的JavaScript包裝器,通過對其呼叫new()來呼叫建構式,可以從中實體化物件,對實體呼叫Dispose()以顯式清理它(或等待JavaScript物件被垃圾收集,或腳本被卸載),靜態和非靜態方法都是可用的,”,那我們就知道通過Java.use獲取的class類可以呼叫$new()來呼叫建構式,可以從實體化物件,在圖1-9中有6個函式,了解了API的呼叫之后我們來開始入手撰寫我們的js檔案,(在這里我覺得大家一定要動手做測驗,動手嘗試,你會發現其中的妙趣無窮!),
1.5.1 攔截構造物件引數腳本示例
function hook_overload_1() {
if(Java.available) {
Java.perform(function () {
console.log("start hook");
//還是先獲取類
var Ordinary_Class = Java.use("com.roysue.roysueapplication.Ordinary_Class");
if(Ordinary_Class != undefined) {
//這里因為add是一個靜態方法能夠直接呼叫方法
var result = Ordinary_Class.add(100,200);
console.log("result : " +result);
//呼叫方法多載無壓力
result = Ordinary_Class.add(100,200,300);
console.log("result : " +result);
//呼叫方法多載
result = Ordinary_Class.add(100,200,300,400);
console.log("result : " +result);
//呼叫方法多載
result = Ordinary_Class.getNumber();
console.log("result : " +result);
//呼叫方法多載
result = Ordinary_Class.getString(" HOOK");
console.log("result : " +result);
//在這里,使用了官方API的$new()方法來實體化類,實體化之后回傳一個實體物件,通過這個實體物件來呼叫類中方法,
var Ordinary_Class_instance = Ordinary_Class.$new();
result = Ordinary_Class_instance.getString("Test");
console.log("instance --> result : " +result);
result = Ordinary_Class_instance.add(1,2,3,4);
console.log("instance --> result : " +result);
} else {
console.log("Ordinary_Class: undefined");
}
console.log("start end");
});
}
}
setImmediate(hook_overload_1);
當我們執行了上面寫的腳本之后終端會列印呼叫方法之后的結果,見下圖1-12,

圖1-12 終端顯示呼叫函式的結果
因為很多時候類中的方法并不一定是靜態的,所以這里提供了2種呼叫方法,第一種呼叫方式十分的方便,不需要實體化一個物件,再通過物件調本身的方法,但是遇到了沒有static關鍵字的函式時只能使用第二種方式來實作方法呼叫,在這一章節中我們學會了如何自己主動去呼叫類中的函式了~~大家也可以嘗試主動呼叫有參的建構式玩玩,
1.6 Java層修改成員變數的值以及函式的回傳值
我們上章學完了如何自己主動呼叫JAVA層的函式了,經過上章的學習我們的功夫又精進了一些~~,現在我們來深入內部修改類的物件的成員變數和回傳值,打入敵人內部,提高自己的內功,現在我們來看下圖1-13,
圖1-13 User類
上圖中的User類是我之前建的一個類,類中寫了2個公開成員變數分別是age是name;還有2個方法分別是User的有參建構式和一個toString函式列印成員變數的函式,我們要做就是在User類實體化的時候攔行程式并且修改掉age和name的值,從而改寫成我們自己需要的值再運行程式,那我們接下開始撰寫JS腳本來修改成員變數的值,
這段代碼主要有的功能是:通過User.$new("roysue",29)拿到User類的有引數構造的實體化物件,這個恰好也是使用了上章節中學到的知識自己構建物件,這里我們也學習了如何使用FRIDA框架通過有參建構式實體化物件,實體化之后先是呼叫了類本身的toString方法列印出未修改前的成員變數的值,列印了之后再通過User_instance.age.value = https://www.cnblogs.com/titodata/p/0;來修改物件當前的成員變數的值,可以看到修改age修改為0,name修改為roysue_123,然后再次呼叫toString方法查看其成員變數的最新值是否已經被更改,
1.6.1 修改成員變數的值以及函式的回傳值腳本代碼示例
function hook_overload_2() {
if(Java.available) {
Java.perform(function () {
console.log("start hook");
//拿到User類
var User = Java.use("com.roysue.roysueapplication.User");
if(User != undefined) {
//這里利用上章學到知識來自己構建一個User的有參構造的實體化物件
var User_instance = User.$new("roysue",29);
//并且呼叫了類中的toString()方法
var str = User_instance.toString();
//列印成員變數的值
console.log("str:"+str);
//這里獲取了屬性的值以及列印
console.log("User_instance.name:"+User_instance.name);
console.log("User_instance.age:"+User_instance.age);
//這里重新設定了age和name的值
User_instance.age.value = https://www.cnblogs.com/titodata/p/0;
User_instance.name.value ="roysue_123";
str = User_instance.toString();
//再次列印成員變數的值
console.log("str:"+str);
} else {
console.log("User: undefined");
}
console.log("start end");
});
}
}
可以看到終端顯示了原本有參建構式的值roysue和30修改為roysue_123和0已經成功了,效果見下圖1-14,
圖1-14 終端顯示修改效果
通過上面的學習,我們學會了如何修改類的成員變數,上個例子中是使用的有參建構式給與成員變數賦值,通常在寫代碼類似這種物體類會定義相關的get set方法以及修飾符為私有權限,外部不可呼叫,這個時候他們可能會通過set方法來設定其值和get方法獲取成員的變數的值,這個時候我們可以通過鉤子攔截set和get方法自己定義值也是可以達到修改和獲取的效果,現在學完了如何修改成員變數了,那我們接下來要學習如何修改函式的回傳值,假設在逆向的程序中已知檢測函式A的結果為B,正確結果為C,那我們可以強行修改函式A的回傳值,不論在函式中執行了什么與回傳結果無關,我們只要修改結果即可,
1.6.2 修改成員變數的值以及函式的回傳值之小實戰
我在rdinary_Class類建立了2個函式分別是isCheck和isCheckResult,假設isCheck是一個檢測方法,經過add運行后必然結果2,代表被檢測到了,在isCheckResult方法進行了判斷呼叫isCheck函式結果為2就是錯誤的,那這個時候要把isCheck函式或者add函式的結果強行改成不是2之后isCheckResult即可列印Successful,見下圖1-15,
圖1-15 isCheck函式與isCheckResult函式
我們現在要做的是使sCheckResult函式成功列印出"Successful",而不是errer,那我們現在開始來寫js腳本吧~~
function hook_overload_7() {
if(Java.available) {
Java.perform(function () {
console.log("start hook");
//先獲取類
var Ordinary_Class = Java.use('com.roysue.roysueapplication.Ordinary_Class');
if(Ordinary_Class != undefined) {
//先呼叫一次肯定會輸出error
Ordinary_Class.isCheckResult();
//在這里重寫isCheck方法將回傳值強行改為123并且輸出了一句Ordinary_Class: isCheck
Ordinary_Class.isCheck.implementation = function (){
console.log("Ordinary_Class: isCheck");
return 123;
}
//再呼叫一次isCheckResult()方法
Ordinary_Class.isCheckResult();
} else {
console.log("Ordinary_Class: undefined");
}
console.log("hook end");
});
}
}
上面這段代碼的主要功能是:首先通過Java.use獲取Ordinary_Class,因為isCheckResult()是靜態方法,可以直接呼叫,在這里先呼叫一次,因為這樣比較好看效果,第一次呼叫會在Android LOG中列印errer,之后緊接著利用FRIDA鉤子對isCheck函式進行攔截,改掉其回傳值為123,這樣每次呼叫isCheck函式時回傳值都必定會是123,再呼叫一次isCheckResult()方法,isCheckResult()方法中判斷isCheck回傳值是否等于2,因為我們已經重寫了isCheck函式,所以不等于2,所以程式往下執行,會列印Successful字串到Android Log中,實際運行效果見下圖1-16,
圖1-16 終端顯示以及Android Log資訊
可以清晰的看到先是列印了errer后列印了Successful了,這說明我們已經成功過掉isCheck的判斷了,這是一個小小的綜合例子,建議大家多多動手嘗試,
結語
在這章中我們學習了HOOK Java層的一些函式,如攔截普通函式、建構式、以及修改成員變數以及函式回傳值,下一篇中我們來列舉所有的類、所有的方法多載、所有的子類以及RPC遠程呼叫Java層函式,
短視頻、直播資料實時采集介面,請查看檔案: TiToData
免責宣告:本檔案僅供學習與參考,請勿用于非法用途!否則一切后果自負,
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/251612.html
標籤:大數據
上一篇:Hadoop 學習筆記 生態
