Frida Java Hook 詳解(安卓9):代碼及示例(上)


Frida Java Hook 詳解(安卓9):代碼及示例(上)

轉載自:https://www.secpulse.com/archives/132082.html

前言

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 修改成員變量的值以及函數的返回值之小實戰 結語

 

Frida Java Hook 詳解(安卓9):代碼及示例(上)

前言

咱們在這篇來深入學習如何HOOK Java層函數,應用於與各種不同的Java層函數,結合實際APK案例使用FRIDA框架對其APP進行附加、hook、以及FRIDA腳本的詳細編寫。

1.1 FRIDA SCRIPT的"hello world"

在本章節中,依然會大量使用注入模式附加到一個正在運行進程程序,亦或是在APP程序啟動的時候對其APP進程進行劫持,再在目標進程中執行我們的js文件代碼邏輯。FRIDA腳本就是利用FRIDA動態插樁框架,使用FRIDA導出的API和方法,對內存空間里的對象方法進行監視、修改或者替換的一段代碼。FRIDAAPI是使用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()方法將函數注冊到FridaJava運行時中去,用來執行函數中的操作,當然這里只是打了一條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.roysueapplicationapk應用程序中,並且打印了一條 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只返回100getString方法返回了 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中addsub函數的調用並且在終端顯示每個函數所傳入的參數、返回的值,開始寫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 hookhook 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,其中代碼這樣寫:

image.png

 

我們可以看到User類中有2個成員變量分別是agename,還有2個構造方法,分別是無參構造有有參構造。我們現在要做的是在User類進行有參實例化時查看所填入的參數分別是什么值。在圖1-3中,可以看到btn_init的點擊事件時會對User類進行實例化參數分別填寫了roysue30,然后再繼續調用了toString方法把它們組合到一起並且通過Toast彈出信息顯示值。TEST_INTI對應btn_init的點擊事件,點擊時效果見下圖1-6。

 

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中所填的roysue30,這就說明我們使用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):動態獲取classNameJavaScript包裝器,通過對其調用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個公開成員變量分別是agename;還有2個方法分別是User的有參構造函數和一個toString函數打印成員變量的函數。我們要做就是在User類實例化的時候攔截程序並且修改掉agename的值,從而改寫成我們自己需要的值再運行程序,那我們接下開始編寫JS腳本來修改成員變量的值。

這段代碼主要有的功能是:通過User.$new("roysue",29)拿到User類的有參數構造的實例化對象,這個恰好也是使用了上章節中學到的知識自己構建對象,這里我們也學習了如何使用FRIDA框架通過有參構造函數實例化對象,實例化之后先是調用了類本身的toString方法打印出未修改前的成員變量的值,打印了之后再通過User_instance.age.value = 0;來修改對象當前的成員變量的值,可以看到修改age修改為0name修改為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 = 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方法獲取成員的變量的值,這個時候我們可以通過鈎子攔截setget方法自己定義值也是可以達到修改和獲取的效果。現在學完了如何修改成員變量了,那我們接下來要學習如何修改函數的返回值,假設在逆向的過程中已知檢測函數A的結果為B,正確結果為C,那我們可以強行修改函數A的返回值,不論在函數中執行了什么與返回結果無關,我們只要修改結果即可。

1.6.2 修改成員變量的值以及函數的返回值之小實戰

我在rdinary_Class類建立了2個函數分別是isCheckisCheckResult,假設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層函數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM