1.frida 核心概念:动态二进制插桩
插桩: 在程序执行流的关键位置插入额外的代码。
动态: 在程序运行时进行插桩,无需修改源代码或重新编译目标程序。
二进制: 直接作用于编译后的可执行文件(二进制文件)。
Frida 的实现: Frida 通过将一个小型引擎(frida-core
)注入到目标进程(如 Android App, iOS App, Windows 桌面程序, macOS 应用等)中实现。这个引擎在目标进程内部运行一个 JavaScript 运行时环境(V8 引擎),允许你执行 JS 脚本并与目标进程的地址空间交互。
1.1.安装
frida是开源工具,直接在github 下载即可,版本要求,
在选择安装的版本之前,需要先确定手机的系统和CPU的架构
可以通过以下命令获取CPU架构
1 2 PS C:\Users\GaoMu> adb shell getprop ro.product.cpu.abiarm64-v8a
根据不同的安装系统版本适配不同的frida版本
同时我的系统是安卓11,因此我选择下载frida16的这个版本
使用adb push 推送解压后的文件到手机目录
1 2 3 4 5 6 7 8 9 10 PS F:\Temp> adb push .\frida-server-16 .7.19 -android-arm64 /data /local/tmp/workplace.\frida-server-16 .7.19 -android-arm64 : 1 file pushed, 0 skipped. 108.7 MB/s (53702368 bytes in 0.471 s) blueline:/data /local/tmp/workplace blueline:/data /local/tmp/workplace total 26 M -rwxrwx--x 1 root everybody 51 M 2025 -06-23 22 :12 frida-server-16 .7.19 -android-arm64 blueline:/data /local/tmp/workplace
1 2 pip install frida==16.7.19 -i https://mirrors.aliyun.com/pypi/simple/ pip install frida-tools==13.7.1 -i https://mirrors.aliyun.com/pypi/simple/
Windwos客户端连接测试 以下显示为安装好的App
1 2 3 4 5 6 7 8 9 10 PS C:\Users\GaoMu> frida-ps -Ua PID Name Identifier ---- ------------ ------------------------------------ 2904 AudioFX org.lineageos.audiofx5172 Aurora Store com.aurora.store5183 F-Droid org.fdroid.fdroid3472 Gboard com.google.android.inputmethod.latin5553 Magisk com.topjohnwu.magisk5471 日历 org.lineageos.etar3652 设置 com.android.settings
1 npm install @types/frida-gum --save
安装证书 Android10 以上安装证书需要使用openssl计算hash,然后命名为hash.0 移动到android的/system/etc/security/cacerts目录下,同时添加对等权限。如果遇到只读文件系统问题,可以使用MT文件管理工具进行移动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 生成哈希并重命名hash.0 ┌──(kali㉿kali)-[~/Desktop/workplace] └─$ openssl x509 -subject_hash_old -in Charles.pem 3b5566cf -----BEGIN CERTIFICATE----- .... mv Charles.pem 3b5566cf.0 # 或者 openssl x509 -inform PEM -subject_hash_old -in myCA.crt # 推送证书到设备 adb root adb remount adb push 3b5566cf.0 /system/etc/security/cacerts/ adb shell chmod 644 /system/etc/security/cacerts/3b5566cf.0 adb reboot
1.2.基础命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 用法: frida [选项] 目标 位置参数: args 额外参数和/或目标 选项: -h, --help 显示此帮助信息并退出 -D ID, --device ID 连接到指定 ID 的设备 -U, --usb 连接到 USB 设备 -R, --remote 连接到远程 frida-server -H HOST, --host HOST 连接到 HOST 上的远程 frida-server --certificate CERTIFICATE 与 HOST 使用 TLS 通信,并验证提供的 CERTIFICATE(证书) --origin ORIGIN 连接到远程服务器时,设置“Origin”请求头为 ORIGIN --token TOKEN 使用 TOKEN 向 HOST 进行身份验证 --keepalive-interval INTERVAL 设置保活间隔(秒),0 表示禁用(默认为 -1,根据传输方式自动选择) --p2p 与目标建立点对点(peer-to-peer)连接 --stun-server ADDRESS 使用 --p2p 时,设置要使用的 STUN 服务器地址 ADDRESS --relay address,username,password,turn-{udp,tcp,tls} 为 --p2p 添加中继(relay)服务器 -f TARGET, --file TARGET 生成(spawn)文件 TARGET(通常是可执行文件) -F, --attach-frontmost 附加到最前端应用程序 -n NAME, --attach-name NAME 附加到名称为 NAME 的进程 -N IDENTIFIER, --attach-identifier IDENTIFIER 附加到标识符为 IDENTIFIER 的进程(如 bundle ID) -p PID, --attach-pid PID 附加到进程 ID 为 PID 的进程 -W PATTERN, --await PATTERN 等待匹配 PATTERN 的进程生成 --stdio {inherit,pipe} 生成进程时的标准输入/输出行为(默认为“inherit”继承) --aux option 设置生成进程时的辅助选项(aux option),例如 “uid=(int)42”(支持的类型:string, bool, int) --realm {native,emulated} 要附加到的执行域(原生域 / 模拟域) --runtime {qjs,v8} 要使用的脚本运行时引擎 --debug 启用 Node.js 兼容的脚本调试器 --squelch-crash 如果启用,不会将崩溃报告输出到控制台 -O FILE, --options-file FILE 包含额外命令行选项的文本文件 --version 显示程序版本号并退出 -l SCRIPT, --load SCRIPT 加载脚本文件 SCRIPT -P PARAMETERS_JSON, --parameters PARAMETERS_JSON 以 JSON 格式提供参数(与 Gadget 相同) -C USER_CMODULE, --cmodule USER_CMODULE 加载 C 模块(CModule) USER_CMODULE --toolchain {any,internal,external} 从源代码编译时使用的 CModule 工具链 -c CODESHARE_URI, --codeshare CODESHARE_URI 加载代码共享 URI CODESHARE_URI -e CODE, --eval CODE 执行(evaluate)代码片段 CODE -q 安静模式(无提示符),并在执行完 -l 和 -e 后退出 -t TIMEOUT, --timeout TIMEOUT 在安静模式下,等待 TIMEOUT 秒后终止 --pause 在生成程序后,保持主线程暂停状态 -o LOGFILE, --output LOGFILE 输出到日志文件 LOGFILE --eternalize 在退出前持久化脚本(eternalize the script) --exit-on-error 在 SCRIPT 中遇到任何异常后以代码 1 退出 --kill-on-exit Frida 退出时杀死生成的程序 --auto-perform 自动将输入的代码包装在 `Java.perform` 中(用于 Java 交互) --auto-reload 启用对提供的脚本和 C 模块的自动重载(默认开启,未来版本将变为必需) --no-auto-reload 禁用对提供的脚本和 C 模块的自动重载
1.3.操作模式 Frida 提供了两种核心操作模式来注入目标进程:Attach (附加)和 Spawn (生成)。这两种模式各有特点,适用于不同场景。
1.3.1.Attach模式
1 2 3 4 graph LR A[运行中的目标进程] --> B[Frida 注入] B --> C[附加脚本] C --> D[操作进程]
1 2 3 4 frida -U -p <PID> -l script.js frida -U -n "Process Name" -l script.js
1.3.2.Spawn模式
Frida 启动并暂停 目标进程,注入脚本后继续执行
1 2 3 4 graph LR A[Frida 启动进程] --> B[暂停进程] B --> C[注入脚本] C --> D[恢复执行]
1.3.3.两者对比
特性
Attach 模式
Spawn 模式
目标状态
已运行
未启动
执行时机
运行时注入
启动前注入
资源占用
低
中等
初始化Hook
可能错过
完整捕获
反调试绕过
有限
有效
适用场景
动态分析
静态分析
启动命令
-p PID
或 -n "Name"
-f package
热重载
支持
不支持
1.3.4.自定义端口启动
1 blueline:/data/local/tmp/workplace
使用adb端口转发将Android的9999端口映射到本地客户端主机。
1 2 PS C:\Users\GaoMu\Desktop\workplace> adb forward tcp:9999 tcp:9999 9999
1 frida -H 127.0.0.1:8888 -f com.example.app
1.4.frida常用关键词解析 Frida 脚本常用关键词深度解析 Frida 脚本使用 JavaScript API 与目标进程交互,下面是核心关键词的详细解析和使用示例:
1. Java
对象 (Android Java 交互) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Java .perform (() => { });Java .choose ("com.example.Class" , { });Java .use ("com.example.Class" ) java.use ("com.example.Class$InnerClass" ) Java .enumerateLoadedClasses () Java .perform (() => { const Activity = Java .use ("android.app.Activity" ); Activity .onCreate .implementation = function (bundle ) { console .log ("Activity created!" ); this .onCreate (bundle); }; });
2. Interceptor
(函数拦截) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Interceptor .attach (targetAddress, { });Interceptor .replace (targetAddress, replacementFunc);Interceptor .detachAll ();const openPtr = Module .getExportByName (null , "open" );Interceptor .attach (openPtr, { onEnter : function (args ) { this .path = args[0 ].readUtf8String (); console .log (`Opening file: ${this .path} ` ); }, onLeave : function (retval ) { console .log (`Returned fd: ${retval} ` ); } });
3. Module
(模块操作) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Module .load ("libtarget.so" ); Module .findBaseAddress ("libtarget.so" ); Module .enumerateImports ("libtarget.so" ); Module .findExportByName ("libc.so" , "printf" ); Module .enumerateExports ("libart.so" , { onMatch : function (exp ) { if (exp.name .indexOf ("JNI" ) !== -1 ) { console .log (`Found JNI export: ${exp.name} ` ); } }, onComplete : function ( ) {} });
4. Process
(进程控制) 1 2 3 4 5 6 7 8 9 10 11 Process .id ; Process .arch ; Process .enumerateModules (); Process .enumerateThreads (); Process .getCurrentThreadId (); Process .enumerateModules ().forEach (mod => { console .log (`Module: ${mod.name} Base: ${mod.base} ` ); });
5. Memory
(内存操作) 1 2 3 4 5 6 7 8 9 10 11 12 Memory .alloc (size); Memory .protect (address, size, protection); Memory .scan (address, size, pattern, callbacks); Memory .readByteArray (address, length); const strPtr = ptr (0x1234 );console .log (`String value: ${strPtr.readUtf8String()} ` );Memory .protect (ptr (0x4000 ), 4096 , 'rwx' );
6. ptr
(指针操作) 1 2 3 4 5 6 7 8 9 ptr ("0x1234" ); ptr.add (offset); ptr.sub (offset); const base = Module .findBaseAddress ("libtarget.so" );const targetFunc = base.add (0x1234 );console .log (`Target function at: ${targetFunc} ` );
7. NativeFunction
(调用原生函数) 1 2 3 4 5 6 7 8 const openPtr = Module .getExportByName (null , "open" );const open = new NativeFunction (openPtr, 'int' , ['pointer' , 'int' ]);const path = Memory .allocUtf8String ("/etc/passwd" );const fd = open (path, 0 );console .log (`File descriptor: ${fd} ` );
8. NativeCallback
(创建回调函数) 1 2 3 4 5 6 7 8 9 const callback = new NativeCallback (function (arg1, arg2 ) { console .log (`Callback called with: ${arg1} , ${arg2} ` ); return 0 ; }, 'int' , ['int' , 'int' ]); const setCallback = Module .getExportByName ("libtarget.so" , "set_callback" );setCallback (callback);
9. Thread
(线程操作) 1 2 3 4 5 6 7 8 9 10 11 Thread .backtrace (context); Thread .sleep (seconds); Interceptor .attach (targetFunc, { onEnter : function (args ) { console .log ("Backtrace:\n" + Thread .backtrace (this .context ).map (DebugSymbol .fromAddress ).join ("\n" )); } });
10. console
(日志输出) 1 2 3 4 console .log ("Info message" ); console .warn ("Warning!" ); console .error ("Error!" ); console .debug ("Debug info" );
1 2 3 4 5 6 7 8 9 setImmediate (() => { console .log ("This runs immediately after script load" ); }); setTimeout (() => { console .log ("This runs after 2 seconds" ); }, 2000 );
12. recv
/ send
(进程通信) 1 2 3 4 5 6 7 8 recv ("command" , (data ) => { console .log ("Received command:" , data.payload ); send ({ result : "success" }); }); script.post ({"type" : "command" , "payload" : "start_monitoring" })
13. Frida
核心对象 1 2 3 Frida .heapSize ; Frida .arch ; Frida .ptrSize ;
14. Stalker
(指令级跟踪) 1 2 3 4 5 6 7 8 9 10 11 12 13 Stalker .follow (threadId, { events : { call : true , ret : false }, onReceive : function (events ) { } }); Stalker .unfollow (threadId);
15. File
(文件操作) 1 2 3 const data = File .readAllBytes ("/path/to/file" );File .writeAllBytes ("/path/to/output" , new Uint8Array ([0x01 , 0x02 ]));
实战模式:Hook 链式调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Java .perform (() => { const StringBuilder = Java .use ("java.lang.StringBuilder" ); StringBuilder .toString .implementation = function ( ) { const result = this .toString (); console .log ("StringBuilder content:" , result); return result; }; const env = Java .vm .getEnv (); const getStringUTFChars = env.getStringUTFChars ; env.getStringUTFChars .implementation = function (str, isCopy ) { const result = getStringUTFChars.call (this , str, isCopy); console .log ("JNI String:" , result.readUtf8String ()); return result; }; });
调试技巧 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 console .log ("Debug mode enabled" );DebugSymbol .enabled = true ;Process .setExceptionHandler (function (exception ) { console .log ("Caught exception:" , exception); return true ; }); Memory .accessWatch ([ptr (0x1234 )], { onAccess : function (details ) { console .log ("Memory accessed at" , details.address ); } });
常用代码片段 获取当前 Activity 1 2 3 4 5 6 Java .perform (() => { const ActivityThread = Java .use ("android.app.ActivityThread" ); const activity = ActivityThread .currentActivityThread () .getActivities ().values ().toArray ()[0 ]; console .log ("Current activity:" , activity); });
修改返回值 1 2 3 4 5 Java .use ("com.example.Crypto" ).encrypt .implementation = function (data ) { const result = this .encrypt (data); return "modified_result" ; };
主动调用方法 1 2 3 4 5 Java .perform (() => { const System = Java .use ("java.lang.System" ); const currentTime = System .currentTimeMillis (); console .log ("Current time:" , currentTime.toInt32 ()); });
1.5.数据类型对比 1.5.1.基本数据类型对比
Java 类型
Frida JavaScript 表示
转换方法/说明
boolean
boolean
直接对应 true/false
byte
number
自动转换
char
number
(Unicode 值)
使用 String.fromCharCode()
转字符
short
number
自动转换
int
number
自动转换
long
number
或 Int64
大数需用 new Int64(value)
float
number
自动转换
double
number
自动转换
void
undefined
无返回值
1.5.2.引用数据类型对比
Java 类型
Frida JavaScript 表示
转换方法/说明
String
string
自动转换
Object
Java.Object
包装器
通过 Java.cast()
处理
数组 (如 int[]
)
Java.Array
对象
使用 Java.array()
创建
自定义类实例
Java.Wrapper
对象
通过 Java.use()
获取类引用
List
/Map
Java.Objects
包装器
特殊方法处理
null
null
直接对应
1.6.frida hook 1.6.1.hook示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.gaomu.fridatest.utils;import java.nio.charset.StandardCharsets;import java.util.Base64;import javax.crypto.Cipher;import javax.crypto.spec.GCMParameterSpec;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;public class AES { public static String aesEncrypt (String str, String str2, String str3, String str4) throws Exception { validateHex("密钥" , str3, 32 ); SecretKeySpec secretKeySpec = new SecretKeySpec (hexToBytes(str3), "AES" ); Cipher cipher = Cipher.getInstance(getTransformation(str)); if (str.equalsIgnoreCase("ECB" )) { cipher.init(1 , secretKeySpec); } else { validateHex("IV" , str4, 32 ); byte [] bArrHexToBytes = hexToBytes(str4); if (str.equalsIgnoreCase("GCM" )) { byte [] bArr = new byte [12 ]; System.arraycopy(bArrHexToBytes, 0 , bArr, 0 , 12 ); cipher.init(1 , secretKeySpec, new GCMParameterSpec (128 , bArr)); } else { cipher.init(1 , secretKeySpec, new IvParameterSpec (bArrHexToBytes)); } } return Base64.getEncoder().encodeToString(cipher.doFinal(str2.getBytes(StandardCharsets.UTF_8))); } ..... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 function hookAes ( ){ var AES = Java .use ("com.gaomu.fridatest.utils.AES" ) AES .aesEncrypt .implementation = function (mode, plainText, keyHex, ivHex ) { console .log ("mode:" + mode); console .log ("plainText:" + plainText); console .log ("keyHex:" + keyHex); console .log ("ivHex:" + ivHex); var res = this .aesEncrypt (mode, plainText, keyHex, ivHex) console .log (res) return res; } } function main ( ) { Java .perform (function ( ) { hookAes () }) } setImmediate (main)
1.6.2.重载方法调用示例
需要调用同名函数不同得方法需要使用overload,来指定方法的参数类型
例如调用四字参数均为字符串类型的aesEncrypt()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hookAes ( ){ var AES = Java .use ("com.gaomu.fridatest.utils.AES" ) AES .aesEncrypt .overload ('java.lang.String' ,'java.lang.String' ,'java.lang.String' ,'java.lang.String' ).implementation = function (mode, plainText, keyHex, ivHex ) { console .log ("tip:" + "use have four args of reload method" ); var res = this .aesEncrypt (mode, plainText, keyHex, ivHex) console .log (res) return res; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var len = utils.getOver .overloads .length ;for (var i=0 ; i<len; i++) { utils.getOver .overloads [i].implementation = function ( ){ for (var a = 0 ; a < arguments .length ; a++) { console .log (a + '---' + arguments [a]) } return this .getOver .apply (this , arguments ) } }
1.6.3.主动调用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function initMD5 ( ) { var MD5 = Java .use ("com.gaomu.fridatest.utils.MD5" ); var md5hash = MD5 .md5Hash ("adminx" ); console .log ("md5: " + md5hash); } function main ( ) { Java .perform (function ( ) { initMD5 () }) } setImmediate (main)function useDesSecurityConstructor ( ) function ( ) { const DesSecurity = Java .use ("com.dodonew.online.util.DesSecurity" ); const ctorOverload = DesSecurity .$overload('java.lang.String' , 'java.lang.String' ); const ds = ctorOverload.$new(b, c); }
1.6.4.hook调用实例方法
1 2 3 4 5 6 7 8 var moneyClass = Java .use ("com.gaomu.Money" );var money = moneyClass.$new();var res = money.getInfo ();var value = money.falg .value ;
RPC全局实例调用
onMatch方法是frida脚本用于匹配特定条件时触发的回调函数。当满足特定的条件时,比如某个函数被调用或者某个字符串被传递等,onMatch方法可以执行相应的操作。
onComplete方法是在Frida脚本执行完毕时触发的回调函数。通常用于在所有操作完成后做一些清理工作或输出总结信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function demo ( ){ Java .choose ("com.gaomu.Money" , { onMatch : function (obj ){ var name = obj.name .value var num = obj.num .value console .log (obj.getInfo ()) }, onComplete : function ( ){ console .log ("内存中Money操作完毕" ) } }); }
1.6.5.hook构造方法 1 2 3 4 5 6 7 8 9 10 var money = Java .use ("com.gaomu.Money" )money.$init .overload ('java.lang.String' , 'int' ).implementation = function (str, num ) { console .log (str, num) this .$init(str, num) }
1.6.6.hook内部类 1 var innerClass = Java .use ('com.gaomu.Money$InnerClass' )
1.6.7.枚举所有的类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 console .log (Java .enumerateLoadedClassesSync ().join ('\n' ));var utils = Java .use ("com.gaomu.Utils" );var methods = utils.class .getDeclaredMethods ();for (var i=0 ; i<methods.length ; i++){ console .log (methods[i].getName ()); } var constructors = utils.class .getDeclaredConstructors ();console .log (constructors);for (var i = 0 ; i < constructors.length ; i++) { var constructor = constructors[i]; console .log (constructor.toString ()); console .log (constructor.getName ()); } var fields = money.class .getDeclaredFields ();
1.6.8.hook map对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 console .log (hashMap);var key = hashMap.keySet ();var it = key.iterator ();while (it.hasNext ()){ var keyStr = it.next (); var valueStr = hashMap.get (keyStr); console .log ("key->" + keystr + " | val->" + valSuetr); } hashMap.put ("name" , "gaomu" );
1.6.9.hook okhttp
如果app使用了okhttp的依赖进行发包,那么就可以使用这个方法进行hook
1 2 3 4 5 6 7 8 9 10 function hookOkhttpURL ( ) { var Builder = Java .use ('okhttp3.Request$Builder' ); Builder .url .overload ('okhttp3.HttpUrl' ).implementation = function (a ) { console .log ('a: ' + a); var res = this .url (a); return res; } }
1 2 3 4 5 6 7 8 var okHttpClinet = Java .use ("okhttp3.OkHttpClient" );OkHttpClient .newCall .overload ("okhttp3.Request" ).implementation = function (request ) { console .log ("HTTP Reqeust -> " + request.url ().toString ()); var call = this .newCall (request); var response = call.execute (); console .log ("HTTP Response -> " + response.body ().string ()); return call; }
1 2 3 4 5 6 7 8 9 var okHttpClinet = Java .use ("okhttp3.Request$Builder" );Builder ["addHeader" ].implementation = function (str, str2 ) { console .log ("key: " + str); console .log ("val: " + str2); var result = this ["addHeader" ](str, str2); return result; }
1.6.10.输出当前堆栈信息 1 2 3 4 5 6 7 function showStacks ( ) { Java .perform (function ( ) { console .log (Java .use ("android.util.Log" ).getStackTraceString ( Java .use ("java.lang.Throwable" ).$new() )) }) }
1.7.实战hook 嘟嘟牛 1.7.1.反编译的APK包
根据反编译的APK源码包,发现大多数代码都是经过混淆的,不具有可读性。
仅部分具有可读性。
1.7.2.hook 记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 setImmediate (main)function main ( ) { Java .perform (function ( ) { hookHashMap (); }) } function hookHashMap ( ) { var hashMap = Java .use ("java.util.HashMap" ); hashMap.put .implementation = function (a, b ){ console .log ("print out ---------->" ,a,b); return this .put (a,b); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hookHashMap ( ) { var hashMap = Java .use ("java.util.HashMap" ); hashMap.put .implementation = function (a, b ){ if (a == "Encrypt" ) { console .log ("print out ---------->" ,a,b); showStacks (); } return this .put (a,b); } } function showStacks ( ) { Java .perform (function ( ) { console .log (Java .use ("android.util.Log" ).getStackTraceString ( Java .use ("java.lang.Throwable" ).$new() )); }) }
在输出的堆栈信息中我们并没用发现我们感兴趣的加密函数。
于是,我换了一个思路,获取这个包下所有的类,看有什么我感兴趣的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 function hookClass ( ) { const targetPackage = "com.dodonew.online" ; const loadedClasses = Java .enumerateLoadedClassesSync (); const filteredClasses = loadedClasses.filter (className => className.startsWith (targetPackage + "." ) ); console .log (`[+] 找到 ${filteredClasses.length} 个类:` ); console .log (filteredClasses.join ('\n' )); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function hookMethod ( ) { var DesSecurity = Java .use ("com.dodonew.online.util.DesSecurity" ); var clazz = DesSecurity .class ; var methods = clazz.getDeclaredMethods (); console .log ("\n[+] DesSecurity 类方法列表:" ); for (var i = 0 ; i < methods.length ; i++) { var method = methods[i]; var methodName = method.getName (); var paramTypes = method.getParameterTypes ().map (t => t.getSimpleName ()); console .log (` - ${methodName} (${paramTypes.join(", " )} )` ); } var fields = clazz.getDeclaredFields (); console .log ("\n[+] DesSecurity 类字段列表:" ); for (var i = 0 ; i < fields.length ; i++) { var field = fields[i]; var fieldName = field.getName (); var fieldType = field.getType ().getSimpleName (); console .log (` - ${fieldType} ${fieldName} ` ); } }
发现了一个加密函数,这个加密函数没有密钥,那么我们完全可以想象一个有密钥的加密函数调用了这个函数。
因此我们hook这个方法,并且输出调用这个方法时的堆栈信息,查找是否有其他加密函数。
1 2 3 4 5 6 7 8 function hookDesSecurity ( ) { var DesSecurity = Java .use ("com.dodonew.online.util.DesSecurity" ); DesSecurity .encrypt64 .implementation = function (byte ){ showStacks (); console .log ("encrypt64 execute!!!!!" ); return this .encrypt64 (byte); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 function hookEncodeDesMap ( ) { try { const RequestUtil = Java .use ("com.dodonew.online.http.RequestUtil" ); const encodeDesMapOverloads = RequestUtil .encodeDesMap .overloads ; console .log ("\n===== encodeDesMap 方法重载分析 =====" ); console .log (`[+] 共找到 ${encodeDesMapOverloads.length} 个重载方法` ); if (encodeDesMapOverloads.length === 0 ) { console .log ("[!] 未找到 encodeDesMap 方法,可能是方法名错误或类未加载" ); return ; } encodeDesMapOverloads.forEach ((overload, index ) => { console .log (`\n[重载 #${index + 1 } ]` ); const paramTypes = overload.argumentTypes ; console .log (` 参数数量: ${paramTypes.length} ` ); paramTypes.forEach ((paramType, paramIndex ) => { try { const className = paramType.className ; const simpleName = className.split ('.' ).pop (); console .log (` 参数 ${paramIndex + 1 } : ${simpleName} (${className} )` ); } catch (e) { console .log (` 参数 ${paramIndex + 1 } : <无法获取类型信息>` ); } }); try { const returnType = overload.returnType .className ; const simpleReturnType = returnType.split ('.' ).pop (); console .log (` 返回类型: ${simpleReturnType} (${returnType} )` ); } catch (e) { console .log (" 返回类型: <未知>" ); } overload.implementation = function ( ) { console .log (`\n[encodeDesMap 重载 #${index + 1 } 被调用]` ); if (arguments .length > 0 ) { console .log (" 调用参数:" ); for (let i = 0 ; i < arguments .length ; i++) { const arg = arguments [i]; if (arg === null ) { console .log (` 参数 ${i + 1 } : null` ); } else if (typeof arg === 'object' && arg.getClass ) { try { const className = arg.getClass ().getName (); console .log (` 参数 ${i + 1 } : ${className} 对象` ); if (className.endsWith ('Map' )) { try { const mapSize = arg.size (); console .log (` Map 大小: ${mapSize} ` ); if (mapSize > 0 ) { const entrySet = arg.entrySet (); const iterator = entrySet.iterator (); console .log (" Map 内容:" ); while (iterator.hasNext ()) { const entry = iterator.next (); const key = entry.getKey (); const value = entry.getValue (); console .log (` ${key} = ${value} ` ); } } } catch (e) { console .log (" 无法解析 Map 内容" ); } } } catch (e) { console .log (` 参数 ${i + 1 } : 对象 (无法获取类信息)` ); } } else { console .log (` 参数 ${i + 1 } : ${arg} (${typeof arg} )` ); } } } else { console .log (" 无参数" ); } const result = this .encodeDesMap .apply (this , arguments ); if (result !== null && result !== undefined ) { console .log (" 返回结果:" , result.toString ()); } else { console .log (" 返回结果: null" ); } return result; }; }); console .log ("\n[+] Hook 已设置,等待方法调用..." ); } catch (e) { console .error ("[!] 分析失败:" , e.message ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ===== encodeDesMap 方法重载分析 ===== [+] 共找到 2 个重载方法 [重载 #1 ] 参数数量: 3 参数 1 : String (java.lang.String ) 参数 2 : String (java.lang.String ) 参数 3 : String (java.lang.String ) 返回类型: String (java.lang.String ) [重载 #2 ] 参数数量: 4 参数 1 : String (java.lang.String ) 参数 2 : String (java.lang.String ) 参数 3 : String (java.lang.String ) 参数 4 : String (java.lang.String ) 返回类型: Map (java.util.Map ) [+] Hook 已设置,等待方法调用...
有两个重载方法,具体调用的哪个重载方法我们不得而知,但是我们可以直接hook尝试。
先hook第一个重载方法,也就是3个String参数的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 setImmediate (main)function main ( ) { Java .perform (function ( ) { hookEncodeDesMap (); hookHashMap (); }) } function hookEncodeDesMap ( ) { const RequestUtil = Java .use ("com.dodonew.online.http.RequestUtil" ); RequestUtil .encodeDesMap .overload ("java.lang.String" ,"java.lang.String" ,"java.lang.String" ).implementation = function (a,b,c ) { console .log ("a--------->" , a); console .log ("b--------->" , b); console .log ("c--------->" , c); return this .encodeDesMap (a, b, c); } } function hookHashMap ( ) { var hashMap = Java .use ("java.util.HashMap" ); hashMap.put .implementation = function (a, b ){ if (a == "Encrypt" ) { console .log ("print out ---------->" ,a,b); } return this .put (a,b); } }
1 2 3 4 5 6 7 8 a---------> {"equtype" :"ANDROID" ,"loginImei" :"Androidb40a5c0f41fef63f" ,"sign" :"C30222B6DF6FF2BCFF34A6E412BE0CA6" ,"timeStamp" :"1751045128588" ,"userPwd" :"1314520" ,"username" :"114614" } b---------> 65102933 c---------> 32028092 print out ----------> Encrypt NIszaqFPos1vd0pFqKlB42Np5itPxaNH 7 KdDY8QjXCrDyjbdep0QyDb+fluS62QBfyio5TUMRlhfetK0JJUAEaNIwv7SujX6+4 EN/CAunlQe cDNagp872sabGkggHRlKAcNuPr0wcSqiY9K9GHk/IrHEw6rClCrfZjYolrq0MXo5I9mSpJA3NEQH Rp5KRms =
1 2 3 4 5 6 7 8 9 10 11 12 13 function hookDesSecurityContructor ( ) { const RequestUtil = Java .use ("com.dodonew.online.http.RequestUtil" ); const encodeDesMapMethod = RequestUtil .encodeDesMap .overload ("java.lang.String" ,"java.lang.String" ,"java.lang.String" ); var cipher = encodeDesMapMethod.call (RequestUtil , "{\"equtype\":\"ANDROID\",\"loginImei\":\"Androidb40a5c0f41fef63f\",\"sign\":\"C30222B6DF6FF2BCFF34A6E412BE0CA6\",\"timeStamp\":\"1751045128588\",\"userPwd\":\"1314520\",\"username\":\"114614\"}" ,"65102933" , "32028092" ); console .log ("cipher: " , cipher); }
我们最后留下一个问题,通过对加密算法的复现,无法复现成功?这是为什么?
其实在处理密钥时做了一次md5算法,并将前64位,十六进制表示的前16个字符作为密钥,如下图所示。
1.8.hook 技巧 1.8.1.hook 用户输入组件
Android开发通常回从EditText组件获取用户的输入信息,需要判断是否为空,通常使用的时TextUtils.isEmpty()方法,来判断用户输入的参数是否为空,那么我们就可以通过hook该方法,在方法内部获取堆栈信息。
1 2 3 4 5 6 String myString = "hello, world" ;if (TextUtils.isEmpty(myString)) { } else { }
1 2 3 4 5 6 7 Java .perform (function ( ) { var TextUtils = java.use ("android.text,TextUtils" ); TextUtils .isEmpty .implementation = function (a ) { console .log ("TextUtils-->" , aa); retrun this .isEmpty (aa); } })
1.8.2.hook JSON对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 var JSON Object = Java .use ('org.json.JSONObject' ); JSON Object .$init .overloads .forEach (function (overload ) { overload.implementation = function ( ) { try { console .log ('\n[JSONObject] Constructor called:' ); console .log ('Arguments: ' + JSON .stringify (arguments )); console .log ('Stack trace: ' + Java .use ("android.util.Log" ).getStackTraceString (Java .use ("java.lang.Exception" ).$new())); var result = this .$init .apply (this , arguments ); console .log ('Created JSON: ' + result.toString (2 )); return result; } catch (e) { console .error ('Error in JSONObject hook: ' + e); return this .$init .apply (this , arguments ); } }; }); JSON Object .toString .overload ().implementation = function ( ) { try { var original = this .toString (); console .log ('\n[JSONObject.toString()]' ); console .log ('Original: ' + original); var modified = original.replace (/\}$/ , ', "hooked":true}' ); console .log ('Modified: ' + modified); return modified; } catch (e) { console .error (e); return this .toString (); } }; var JSON Array = Java .use ('org.json.JSONArray' ); JSON Array .put .overload ('java.lang.Object' ).implementation = function (value ) { console .log ('\n[JSONArray.put]' ); console .log ('Inserting value: ' + value); return this .put (value); }; var JSON Tokener = Java .use ('org.json.JSONTokener' ); JSON Tokener.$init .overload ('java.lang.String' ).implementation = function (jsonStr ) { console .log ('\n[PARSING JSON]' ); console .log ('Raw JSON: ' + jsonStr); return this .$init(jsonStr); };
1.8.3.hook 算法
很多Android开发通常都是使用JAVA自带的包实现的加密或者摘要算法,因此我们可以直接通过hook java原生算法包的方法来获取摘要和加密的明文和密文内容。
1.8.3.1.hook md5
以下代码中就使用了MD5原生算法包。因此我们可以直接hook digest方法,这也是一种通用的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import kotlin.UByte;public class MD5 { public static String md5Hash (String str) throws NoSuchAlgorithmException { byte [] bArrDigest = MessageDigest.getInstance("MD5" ).digest(str.getBytes()); StringBuilder sb = new StringBuilder (); for (byte b : bArrDigest) { String hexString = Integer.toHexString(b & UByte.MAX_VALUE); if (hexString.length() == 1 ) { sb.append('0' ); } sb.append(hexString); } return sb.toString(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function javaMD5 ( ) { const MessageDigest = Java .use ('java.security.MessageDigest' ); MessageDigest .digest .overload ('[B' ).implementation = function (input ) { const inputString = Java .use ('java.lang.String' ).$new(input, "UTF-8" ); console .log ("\nInput: " + inputString); console .log ("\n=== MD5 Hook Called ===" ); const result = this .digest (input); const hex = Array .from (result, byte => ('0' + (byte & 0xFF ).toString (16 )).slice (-2 )).join ('' ); console .log ("MD5 Result: " + hex); console .log ("=========================" ); return result; } }
1.8.3.2.hook AES
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.gaomu.fridatest.utils;import java.nio.charset.StandardCharsets;import java.util.Base64;import javax.crypto.Cipher;import javax.crypto.spec.GCMParameterSpec;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;public class AES { public static String aesEncrypt (String str, String str2, String str3, String str4) throws Exception { validateHex("密钥" , str3, 32 ); SecretKeySpec secretKeySpec = new SecretKeySpec (hexToBytes(str3), "AES" ); Cipher cipher = Cipher.getInstance(getTransformation(str)); if (str.equalsIgnoreCase("ECB" )) { cipher.init(1 , secretKeySpec); } else { validateHex("IV" , str4, 32 ); byte [] bArrHexToBytes = hexToBytes(str4); if (str.equalsIgnoreCase("GCM" )) { byte [] bArr = new byte [12 ]; System.arraycopy(bArrHexToBytes, 0 , bArr, 0 , 12 ); cipher.init(1 , secretKeySpec, new GCMParameterSpec (128 , bArr)); } else { cipher.init(1 , secretKeySpec, new IvParameterSpec (bArrHexToBytes)); } } return Base64.getEncoder().encodeToString(cipher.doFinal(str2.getBytes(StandardCharsets.UTF_8))); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function hookJavaAes ( ) { const Cipher = Java .use ("javax.crypto.Cipher" ); Cipher .doFinal .overload ('[B' ).implementation = function (input ) { var plainText = Java .use ('java.lang.String' ).$new(input, "UTF-8" ); console .log ("\nAES Encrypt plainText: ->>>> " , plainText); return this .doFinal (input); } const SecretKeySpec = Java .use ("javax.crypto.spec.SecretKeySpec" ); SecretKeySpec .$init .overload ('[B' , 'java.lang.String' ).implementation = function (keyBytes, alg ) { var keyHex = bytesToHex (keyBytes); console .log ("\nAES Encrypt keyHex: ->>>>" , keyHex); console .log ("\nAES Encrypt ALG: ->>>>" , alg); return this .$init(keyBytes, alg); } const IvParameterSpec = Java .use ("javax.crypto.spec.IvParameterSpec" ); IvParameterSpec .$init .overload ('[B' ).implementation = function (keyBytes ) { var ivHex = bytesToHex (keyBytes); console .log ("\nAES Encrypt ivHex: ->>>>" , ivHex); return this .$init(keyBytes); } } function bytesToHex (bytes ) { return Array .from (bytes, byte => ('0' + (byte & 0xFF ).toString (16 )).slice (-2 ) ).join ('' ); }
2.抓包检测 几种证书验证的机制 1.常规HTTPS
HTTPS认证为客户端获取服务端证书,加密请求服务端。
因此只需要利用中间人攻击,让客户端使用中间人伪造的证书,加密请求,中间人再截获请求解密,再使用服务端端证书加密请求服务器,就实现了中间人攻击,窃取或者伪造传输数据。具体可以查看这篇文章【HTTPS加密流程以及抓包原理解析】
常见的中间人攻击的抓包软件有burpsutie、Charles、yakit。
针对这种常规的SSL证书加密只需要安装普通用户证书即可
2.系统证书验证
Android7 以上安装系统证书,因此需要使用openssl计算hash,并且移动到系统证书目录,然后命名为hash.0 移动到android的/system/etc/security/cacerts目录下,同时添加对等权限。如果遇到只读文件系统问题,可以使用MT文件管理工具进行移动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ┌──(kali㉿kali)-[~/Desktop/workplace] └─$ openssl x509 -subject_hash_old -in Charles.pem 3b5566cf -----BEGIN CERTIFICATE----- .... mv Charles.pem 3b5566cf.0adb root adb remount adb push 3b5566cf.0 /system/etc/security/cacerts/ adb shell chmod 644 /system/etc/security/cacerts/3b5566cf.0
3.SSL pinning单向证书认证
即客户端会强制验证服务端证书是否为对应证书,否则不接受该证书。
对于SSL pinning单向证书认证问题,可以使用利用SSL Unpinning 脚本 hook绕过过代码检测,或者一些其他的SSL检测。
还可以通过frida hook URL 将HTTPS协议转换为HTTP
4.双向认证
通常我们SSL传输加密都是服务端返回证书客户端使用证书中的公钥进行加密,但是在一些高安全要求的情况下,可能会使用双向证书校验,也就是说不仅服务端返回公钥证书,同时客户端APK中也存在一个证书文件,用于对服务端的证书进行证书验证。
一般APK中的证书后缀名为.p12
双向认证如何绕过也可以看这个【双向证书绕过】
5.自定义协议或者TCP
6.VPN检测
3.1.如何判断一个APK是否加壳? 3.1.1.dex完整性校验
使用dexdump
解析Dex头信息,若显示code_offset=0
或指令段全零,表明代码被加密。
3.1.2.AndroidManifest.xml
启动类替换 :主Activity指向壳的代理类(如com.stub.StubApp
),而非业务逻辑类。
权限声明异常 :声明android:extractNativeLibs="false"
(防止so解密到磁盘)。
3.2.查壳方式 3.2.1.凭借so文件特征 如果加壳会在apk的lib目录下生成.so文件,根据文件名称进行判断。
加固厂商
特有标识
腾讯乐固
libshellx-*.so
360加固
libjiagu*.so
、/assets/jiami
百度加固
libbaiduprotect.so
阿里聚安全
libmobisec.so
3.2.2.查壳工具
使用查壳工具快速检测
查壳工具
GDA :直接拖入APK,自动识别加固类型及版本(如”Tencent Legu 3.9.0”)。
PKiD :基于机器学习的查壳工具,检测未知壳变种。
APKDeepLens :扫描OWASP漏洞同时输出加壳信息。
例如PKID查壳工具:
3.3.脱壳方式 3.3.1.都有哪些壳
壳的发展
壳的种类非常多,根据其种类不同,使用的技术也不同,这里稍微简单分个类
一代整体型壳,采用Dex整体加密,动态加载运行的机制。
二代函数抽取型壳,粒度更细,将方法单独抽取出来,加密保存,解密执行。
三代 VMP、Dex2C壳:独立虚拟机解释执行、语义等价语浙迁移,强度最高。
3.3.2.frida-dexdump
CLI arguments base on frida-tools , you can quickly dump the foreground application like this:
Or specify and spawn app like this:
1 frida-dexdump -U -f com.app.pkgname
Additionally, you can see in -h
that the new options provided by frida-dexdump are:
1 2 3 -o OUTPUT, --output OUTPUT Output folder path, default is './<appname>/' . -d, --deep-search Enable deep search mode. --sleep SLEEP Waiting times for start, spawn mode default is 5s.
When using, I suggest using the -d, --deep-search
option, which may take more time, but the results will be more complete.
之后通过运行app脱壳,生成大量的dex文件。
这些dex文件都可以通过GDA/JDX-GUI直接阅读。
但是可能生成的文件较多,因此需要过滤,通过检索存在主类的dex文件,之后只需要重点阅读存在主类的dex文件即可,无需再阅读其他的dex文件。
windows使用findstr /s /i /m "主类全类名" *
搜索所有存在主类的dex文件。
3.3.3.加固对抗 Frida 的常见手段 加固厂商会采用多种技术阻止 Hook:
反调试检测 :扫描 /proc/self/maps
查找 frida-agent
特征
端口检测 :检查 27042/27043 等 Frida 默认端口
线程监控 :检测异常线程(如 Frida 的 gum-js-loop
)
代码混淆 :关键逻辑转移到 Native 层(SO 文件)
双进程守护 :监控进程状态,崩溃即重启
3.4.so文件处理
3.5.Smali 基础语法详解 Smali 是 Android 平台上 Dalvik 虚拟机的汇编语言,用于表示 .dex
文件中的字节码指令。掌握 Smali 是 Android 逆向工程的基础,下面详细介绍其核心语法:
一、文件结构与类定义 1. 类声明 1 2 3 .class <访问修饰符> <类名>; .super <父类>; .source "SourceFile"
示例 :
1 2 3 .class public Lcom/example/MyClass; .super Ljava/lang/Object; .source "MyClass.java"
2. 接口实现
3. 注解 1 2 3 .annotation <注解类型> key = value .end annotation
二、字段定义 (Fields) 1 .field <访问修饰符> [static] [final] <字段名>:<类型>
示例 :
1 2 .field private count:I .field public static final TAG:Ljava/lang/String;
三、方法定义 (Methods) 1. 方法声明 1 2 3 4 5 6 .method <访问修饰符> [static] <方法名>(<参数类型>)<返回类型> .registers <N> .locals <M> [.param annotations] [代码指令] .end method
示例 :
1 2 3 4 5 6 7 8 9 10 11 .method public static main([Ljava/lang/String; )V .registers 5 .locals 2 .param p0, "args" const-string v0, "Hello" sget-object v1, Ljava/lang/System; ->out:Ljava/io/PrintStream; invoke-virtual {v1, v0}, Ljava/io/PrintStream; ->println(Ljava/lang/String; )V return-void .end method
四、数据类型 1. 基本类型
类型
符号
示例
void
V
()V
boolean
Z
Z
byte
B
B
short
S
S
int
I
I
long
J
J
float
F
F
double
D
D
char
C
C
2. 对象类型
示例 :
1 2 Ljava/lang/String; Landroid/app/Activity;
3. 数组类型
示例 :
1 2 [I [[Ljava/lang/String;
五、寄存器操作 1. 寄存器命名
v0
, v1
, v2
… - 本地寄存器
p0
, p1
, p2
… - 参数寄存器(p0 在非静态方法中是 this)
2. 常用指令
指令
功能
示例
move vA, vB
寄存器间移动值
move v1, v0
const vA, value
加载常量
const v0, 0x1
const-string vA, "str"
加载字符串常量
const-string v0, "OK"
const-class vA, Lcls;
加载类引用
const-class v0, Ljava/lang/Object;
六、方法调用 1. 调用类型 1 invoke-<类型> {参数}, <方法签名>
类型
用途
static
静态方法
virtual
虚方法(非私有)
direct
直接方法(私有/构造方法)
interface
接口方法
super
调用父类方法
2. 方法签名格式 1 Lpackage/name/ObjectName; ->methodName(LparamTypes; )ReturnType
示例 :
1 2 3 4 sget-object v0, Ljava/lang/System; ->out:Ljava/io/PrintStream; const-string v1, "Hello" invoke-virtual {v0, v1}, Ljava/io/PrintStream; ->println(Ljava/lang/String; )V
七、控制流指令 1. 条件分支 1 if-<cond> vA, vB, :label
条件
含义
eq
等于 (==)
ne
不等于 (!=)
lt
小于 (<)
le
小于等于 (<=)
gt
大于 (>)
ge
大于等于 (>=)
eqz
等于零 (==0)
nez
不等于零 (!=0)
2. 无条件跳转
3. Switch 语句 1 2 packed-switch vA, :switch_table sparse-switch vA, :switch_table
4. 标签定义
八、异常处理 1. 异常捕获 1 .catch <异常类型> {from_label} {to_label} {target_label}
示例 :
1 2 3 4 5 6 7 :try_start :try_end .catch Ljava/lang/Exception; {:try_start .. :try_end } :catch_block :catch_block
2. 抛出异常
九、调试信息 1 2 3 .line <行号> .local vA, "name" :<type> .param pA, "name"
十、完整方法示例 1 2 3 4 5 6 7 8 9 10 .method public sum(II)I .registers 5 .param p1, "a" .param p2, "b" .line 10 add-int v0, p1, p2 return v0 .end method
学习建议
实践操作 :使用 apktool 反编译 APK,查看生成的 smali 代码
1 apktool d app.apk -o output_dir
工具辅助 :
Jadx:尝试将 smali 转回 Java
Bytecode Viewer:可视化分析
Android Studio + smalidea 插件:调试 smali 代码
参考资源 :
掌握 Smali 语法需要实践积累,建议从简单的 APK 开始分析,逐步理解寄存器操作和控制流结构。