https://github.com/tacesrever/frida-tsplugin
使用 node-java 在node环境中获取Java类信息
利用了TypeScript Language Server的可扩展性, 参考 Writing-a-Language-Service-Plugin
调试TypeScript语法树 https://ast.carlosroso.com/
我并未深度测试这个插件, 在使用的过程中你有可能会遇到自动完成信息不准确, 卡顿, 死循环, tsserver崩溃等问题,
欢迎评论或者提issue, 也欢迎有闲心的老哥提出pr改进.
如果你安装了Frida, 不管你熟不熟悉nodejs的生态, 肯定已经安装好了npm (
你需要在你编写注入js文件的目录下运行
(可以不事先创建package.json, 只是会出现一条警告)
npm install @types/frida-gum
之后使用附带TypeScript代码完成功能的编辑器(比如vscode)打开js文件即可.
在任意目录下git clone https://github.com/tacesrever/frida-tsplugin
后在frida-tsplugin目录下运行npm install
npm run compile
因为vscode使用的是electron, 其中内置的node版本可能和你所安装并使用的node版本不同, 导致使用npm install
直接安装并编译的java npm库不能使用.
你需要点击vscode中上方工具栏的Help -> About 查看electron版本,
并在tsplugin目录下运行electron-rebuild -w java -v version_of_electron
来重新编译vscode可用的node-java.
在注入js文件的同目录下创建一个tsconfig.json,
内容参考 tsconfig.json
之后在compilerOptions中添加一个plugin:
{ "compilerOptions": { ... , "plugins": [{ "name": path_to_frida-tsplugin, "classPaths": [ path_to_android_jar(usually at SDKROOT/platforms/android-sdklevel/android.jar), path_to_apk_dex2jar_jar, ... ], "logfile"?: path_to_logfile }] } }
其中classPaths可以指定任意jar或者classes目录, 你可以将使用dex2jar转换出来的jar添加在这里.
注意如果使用的是sdk里的android.jar, 那么因为一些内部方法或者成员不会被包括在这个android.jar里, 所以自动完成插件不会显示(而frida可以访问这些内部方法), 或许有方法可以从android设备中把boot.art之类的转换一下 (或许也可以费点劲从aosp源码platform/frameworks/base/core/java
编译一个
logfile 是调试日志, 可以不指定.
在vscode中, 这些路径都是基于vscode安装目录/resources/app/extensions来进行相对路径查找的, 所以推荐使用绝对路径.
someJavaFunction[.overload(...)].implementation = function(...) {...}
函数块中的参数类型和this类型 ps. 对于未能追踪到的类型, 可以使用Java.cast来为其做一个声明
tsserver提供了一种插件机制, 让我们可以编写一个插件, 在自动完成触发时检视代码语法树, 并可以编辑自动完成的结果.
在插件创建时会将info: ts.server.PluginCreateInfo
传递给我们, info.languageService
是原始的languageService, 其中包含了一系列api函数, 比如findReferences(fileName, position)
可以让我们得到处于某个位置的变量的所有引用;
同时我们也可以 "hook" 这些函数, 自动完成插件的实现就是基于hook getCompletionsAtPosition
.
在getCompletionsAtPosition
触发时, 我们会得到一个文件名和一个位置作为参数, 这个位置是自动完成触发时用户的输入焦点;
根据文件名, 我们可以调用info.languageService.getNonBoundSourceFile(fileName)
得到一个SourceFile, 而这个SourceFile中就包含了当前的TypeScript语法树.
getNonBoundSourceFile
是未公开导出的内部方法, 返回languageService内部经过缓存的SourceFile.
我们可以在这个语法树中找到位于输入焦点位置的结点(通常是字符"."), 并以它的父节点(通常为PropertyAccessExpression)的第一个子节点为出发点, 来寻找它是不是最终来自于Java.use,
该过程参见 frida-tsplugin/src/index.ts
中 findInfoProviderForExpr
函数.
在寻找节点的Java类型时首先需要判断节点的语法类型; 在TypeScript中定义了400+种语法类型, 然而我们并不需要处理全部的类型, 只需要处理常见类型即可, 剩下的默认返回为undefined表示没有找到.
常见的情况有:
const someVar = Java.use(classname); someVar.<trigger here>
此时someVar的语法类型为Identifier;
const someVar = Java.use(classname); someVar.someProp.<trigger here>
此时someVar.someProp的语法类型为PropertyAccessExpression;
Java.use(classname).<trigger here>
此时前缀的语法类型为CallExpression.
等.
可以利用 https://ast.carlosroso.com/ 来查看语法树结构
当遇到
const varA = Java.use(classname); let varB = varA; varB.<trigger here>
时, 一开始会拿到varB节点, 而它是一个Identifier, 这时可以通过findReferences(fileName, position)
函数来查找对varB的最后一个写入引用. findReferences
函数会返回一个由{definition, references}
组成的数组, 因为一个变量名可能会在多个地方有不同的定义; references是对该定义所对应的变量所有的引用数组.
references的元素具有isWriteAccess属性, 从中也能拿到该引用位于代码中的位置, 我们就可以遍历findReferences的结果, 找到最后一个写入引用的位置, 再根据该位置在语法树中找到赋值表达式, 之后就可以找到并继续追踪赋值表达式的右值.
const varA = Java.use(classname); let varB = varA.fieldA.value; varB.<trigger here>
在这种情况下我们可以从varB追踪到varA.fieldA.value, 而它的类型是PropertyAccessExpression. 借助语法树我们可以看到它的结构是 (varA.fieldA).(value), 将前面的表达式作为一个整体, 后面的值作为name.
在这时我们其实有两种情况, 一种是varA是一个来自于Java的类型, 又或者它是一个js对象. 这时我们对它的区别判断基于前面是否出现过(expr).(name) = ...
表达式, 可以通过findReferences对value所在的位置查找写入引用来判断.
找到了的话则继续追踪赋值表达式的右值
未找到则我们推测它是一个对Java类型的属性获取, 并递归调用追踪并获取前面的(varA.fieldA)的类型信息, 之后在去查找value在类中所对应的类型.
但是根据frida的语法, 我们默认对一些特殊属性名, 比如"value"等的访问直接推测是Java类型并追踪前缀, 因为有可能出现:
const varA = Java.use(classname); // fieldA's type is java.lang.String varA.fieldA.value = "somestr"; varA.fieldA.value.<trigger here>
在这时对value进行findReferences是可以找到写入引用的, 但是它是一个Java类型.
对于Java函数的调用可能有多种不同的情况:
const javaClass = Java.use(classname); let varA = javaClass.methodA(...args); let varB = javaClass.methodA.call(someInstance, ...args); let varC = javaClass.methodA.overload(...sometype)(...args); let varD = javaClass.methodA.overload(...sometype).call(someInstance, ...args);
overload(...sometype)的语义是找到对应参数类型为...sometype的重载函数, 如果没有overload则会根据参数类型自动判断.
那么我们也可以跟随这个逻辑, 在遇到overload函数调用表达式时, 得益于TypeScript语法树信息的完善, 我们可以从函数调用表达式中直接提取字符串参数作为类型描述寻找对应的重载函数. 没有遇到overload而是直接遇到函数调用表达式时需要继续追踪参数符号的类型, 并根据frida可以进行的自动参数转换, 写出对应的常见语法类型转换为Java变量类型的函数.
目前暂时还没有实现对javaClass.method.apply(someInstance, argArray)
中参数类型的判断.
当我们最终跟踪到Java.use(classname)
时, 我们就可以借助node-java从jar文件中拿到classname对应的类的信息, 回溯属性访问链, 找到起点的类型信息.
还有另外一种Java类型传递的方式:
const someClass = Java.use("classname") someClass.someFunction.implementation = function(arg1) { arg1.<trigger here> this.<trigger here> }
这时我们可以追踪到this
是一个ThisKeyword, arg1来自于Parameter类型, 之后我们可以根据语法树寻找(someClass.someFunction).implementation = ...
这个表达式, 从而找到左值, 继续追踪(someClass.someFunction)的类型, 再对arg1 或者this的类型进行判断.
为了避免无用的跟踪, 在查找变量的写入引用前, 会先判断变量的定义类型是否为any或者Java.Warpper(@types/frida-gum 中定义的Java.use返回值类型), 如果不是则停止跟踪查找.
对于frida封装的JavaWarpper中内部的一些属性, 可以从 https://github.com/frida/frida-java-bridge/blob/master/lib/class-factory.js 中找到, 类, 成员函数, 成员变量 分别对应 Wrapper.prototype
, Function.prototype
, Field.prototype
.
除了自动完成外, vscode的 typescript-language-features 通过getQuickInfoAtPosition
来获取hover时的展示信息, 通过getCompletionEntryDetails
来获取当选择某个自动完成项时右侧展示的符号信息, 我们也可以hook这些函数来提供这些信息.
更具体的细节实现可以查看源码.
2020安全开发者峰会(2020 SDC)议题征集 中国.北京 7月!
最后于 15小时前 被tacesrever编辑 ,原因: