为Frida编写一个Java代码提示及自动补全插件
源代码
https://github.com/tacesrever/frida-tsplugin
引用
利用了TypeScript Language Server的可扩展性, 参考 Writing-a-Language-Service-Plugin
调试TypeScript语法树 https://ast.carlosroso.com/
安装及使用方式
我并未深度测试这个插件, 在使用的过程中你有可能会遇到自动完成信息不准确, 卡顿, 死循环, tsserver崩溃等问题,
欢迎评论或者提issue, 也欢迎有闲心的老哥提出pr改进.
基础frida代码完成
如果你安装了Frida, 不管你熟不熟悉nodejs的生态, 肯定已经安装好了npm (
你需要在你编写注入js文件的目录下运行
(可以不事先创建package.json, 只是会出现一条警告)
npm install @types/frida-gum
之后使用附带TypeScript代码完成功能的编辑器(比如vscode)打开js文件即可.
下载并配置插件
git clone https://github.com/tacesrever/frida-tsplugin
在使用时
cd frida-tsplugin
frida -U -l agent/tsplugin.js target
adb forward tcp:28042 tcp:28042
以将frida-tsplugin/agent/tsplugin.js
注入进目标app中并进行端口转发.
该脚本会开放一个http服务, 来让插件可以远程获取Java类的信息.
若有修改监听地址或为早期版本的frida编译等需求, 可以使用frida-compile自行编译agent/tsplugin.ts
并注入输出文件.
在你的项目下创建一个tsconfig.json,
内容参考 frida-agent-example/tsconfig.json
之后在compilerOptions中添加一个plugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 | {
"compilerOptions" : {
...
,
"plugins" : [{
"name" : frida - tsplugin文件夹的绝对路径,
"host" ?: 目标设备ip, 默认为 "127.0.0.1"
"port" ?: agent脚本的监听端口,默认是 28042
"logfile" ?: 日志文件路径
}]
}]
}
}
|
在vscode中, 这些路径都是基于vscode安装目录/resources/app/extensions来进行相对路径查找的, 所以推荐使用绝对路径.
特性
- 可以识别Java.use和Java.cast
- 可以追踪变量赋值传递
- 可以识别并追踪类成员函数及成员变量的类型
- 可以根据重载函数的参数类型识别对应的重载函数
- 可以识别
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表示没有找到.
常见的情况有:
1 2 | const someVar = Java.use(classname);
someVar.<trigger here>
|
此时someVar的语法类型为Identifier;
1 2 | const someVar = Java.use(classname);
someVar.someProp.<trigger here>
|
此时someVar.someProp的语法类型为PropertyAccessExpression;
1 | Java.use(classname).<trigger here>
|
此时前缀的语法类型为CallExpression.
等.
可以利用 https://ast.carlosroso.com/ 来查看语法树结构
跟踪赋值传递
当遇到
1 2 3 | const varA = Java.use(classname);
let varB = varA;
varB.<trigger here>
|
时, 一开始会拿到varB节点, 而它是一个Identifier, 这时可以通过findReferences(fileName, position)
函数来查找对varB的最后一个写入引用. findReferences
函数会返回一个由{definition, references}
组成的数组, 因为一个变量名可能会在多个地方有不同的定义; references是对该定义所对应的变量所有的引用数组.
references的元素具有isWriteAccess属性, 从中也能拿到该引用位于代码中的位置, 我们就可以遍历findReferences的结果, 找到最后一个写入引用的位置, 再根据该位置在语法树中找到赋值表达式, 之后就可以找到并继续追踪赋值表达式的右值.
跟踪属性获取
1 2 3 | 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类型并追踪前缀, 因为有可能出现:
1 2 3 4 | 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函数调用语法以及重载
对于Java函数的调用可能有多种不同的情况:
1 2 3 4 5 | 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类型传递的方式:
1 2 3 4 5 | 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这些函数来提供这些信息.
更具体的细节实现可以查看源码.
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
最后于 2021-3-6 19:43
被tacesrever编辑
,原因: 代码更新