admin 管理员组

文章数量: 887018

1、网页 js 逆向分析( v_jstools )

From:https://mp.weixin.qq/s/LisYhDKK_6ddF-19m1gvzg

Python 爬虫工具篇:必用的 Chrome 插件

  • EditThisCookie 是一个 Cookie 管理器,可以很方便的添加,删除,编辑,搜索,锁定和屏蔽 Cookies。可以将登录后的 Cookies 先保存到本地,借助 cookielib 库,直接爬取登录后的数据。避免了抓包和模拟登录,帮助我们快速地进行爬虫。
  • Web Scraper 是一款免费的、适用于任何人,包含没有任何编程基础的爬虫工具。操作简单,只需要鼠标点击和简单的配置,就能快速的爬取 Web 端的数据。它支持复杂的网站结构,数据支持文本、连接、数据块、下拉加载数据块等各种数据类型。此外,还能将爬取的数据导出到 CSV 文件中。
  • Xpath Helper 是一种结构化网页元素选择器,支持列表和单节点数据获取,它可以快速地定位网页元素。对比 Beautiful Soup,由于 Xpath 网页元素查找性能更有优势,Xpath 相比正则表达式编写起来更方便。编写 Xpath 之后会实时显示匹配的数目和对应的位置,方便我们判断语句是否编写正确。
  • Toggle JavaScript 插件可以用来检测当前网页哪些元素是通过 AJAX 动态加载的。使用它可以快速在容许加载 JS 、禁止加载 JS 两种模式中切换。
  • User-Agent Switcher for Chrome 插件可以很方便的修改浏览器的 User-Agent。可以模拟不同的浏览器、客户端,包含 Android、IOS 去模拟请求。对于一些特殊网站,切换 User-Agent 可以更方便地进行数据爬取。
  • JSON-handle 是一款功能强大的 JSON 数据解析 Chrome 插件。它以简单清晰的树形图样式展现 JSON 文档,并可实时编辑。针对数据量大的场景,可以做局部选取分析。
  • 前端助手:功能和JSON-handle 差不多。但是功能更强大
  • ReRes、ReRes、URLRedirector、XSwitch:重定向 URL。可以把请求的 js 文件重定向本地的js,方便进行调试。
  • Console Importer:用一个命令,从控制台导入 JavaScript 和 CSS 资源。

下载、安装 v_jstools 插件

v_jstools 是一款浏览器插件,可以进行 web hook,功能非常强大。工具地址:https://github/cilame/v_jstools

浏览器打开上面的网站后,点击 code 按钮,选择 Download ZIP 选项,将文件下载下来,然后解压,会看到一个  v_jstools-main  的文件夹。

安装插件

  • 谷歌浏览器地址栏输入:chrome://extensions/  打开扩展程序页面,并打开开发者模式。如果是其他浏览器,请使用菜单栏打开。
  • 打开后,点击左上角的 加载已解压的扩展程序 按钮,然后选择 v_jstools-main  文件夹,插件就已经加载进来了。

插件功能

在扩展程序中,打开该插件,看看支持哪些功能:

打开配置页面后,来到这里:

可以看到,有三个功能。 

dom对象hook

第一个功能是 dom对象hook ,它内置了很多常见方法的hook,如eval,cookie,Function等,想hook哪个方法,直接打钩即可,非常的方便。有了这个,再也不用写油猴脚本啦。

ast hook替换

第二个功能是 ast hook替换,顾名思义,就是拦截某个js代码,通过ast处理后,再返回ast处理后的代码,让混淆代码见鬼去吧。

它给的示例代码是这个:

function fetch_hook(code, url) {
    var ast = parser.parse(code);
    
    function removedebugger(path) {
        path.replaceWith(t.identifier("/*debugger*/"))
    }
    traverse(ast, {
        DebuggerStatement: removedebugger
    });
    var {code} = generator(ast, {
        jsescOption: {
            minimal: true,
        }
    });
    return code
}

这里简单说一下, fetch_hook 函数 接受2个参数,可以不用理会,如果你想hook特定的url,可以在函数的首行加入这样一段代码:

if (url.indexOf("https://www.xxx/xxx.js") == -1)
{
    return code // 检测到不是这个 url 就直接返回原始代码
}

再就是编写 ast 插件了,按照这样下就好:

function removedebugger(path) {
    path.replaceWith(t.identifier("/*debugger*/"))
}
traverse(ast, {DebuggerStatement: removedebugger});

先写方法,再遍历,也可以按照我星球里的方式进行编写,都是一样的。

不过在我使用了几次以后,还是觉得线下处理混淆代码香,当然要看你的使用场景了

AST 的解密混淆

第三个功能是一些 AST的解密混淆,如图:

 把混淆代码复制到上面的白色框中,点击对应的按钮即可解混淆,也是非常的方便。这里能解混淆的有 sojson,ob混淆,jsfuck以及压缩代码等,都是比较常用的功能。

试用了一番,可以还原最新版的ob混淆,效果还是很棒的。当然一些修改特征了的代码无法还原,这个要做到通杀确实有些难度。

v_jstools 补环境

点击 打开配置按钮:

按图进行如下设置:

实战案例 1:知乎搜索

实战:https://www.zhihu/search?type=content&q=python

抓包,查看 请求参数

需要的加密参数:

等网站加载完毕后,我们再点开这个插件,选择 生成临时环境 命令:

点击后,会有个弹框提示已复制到剪切板。这样,临时环境就帮我们生成好了,新建一个js文件 v.js, 将生成的环境复制过来进来并保存。我们再将这个参数加密的代码也复制进来,保存到一起,具体请参考这篇文章:https://blog.csdn/qq523176585/article/details/123814707

在加密函数处打上断点,记录实参和结果。

实参: '7861d84e110af123f2fc8c05ff38601f';
结果: 'a8O0QQuy28xYcR28f0NBkQe0kXtYUuY0mLxq66L0S7Yp'
在v.js中构造好实参,并打印结果:console.log(b('7861d84e110af123f2fc8c05ff38601f'));

保存后,运行结果

经过仔细比对后可以发现,和网页上面的结果是一模一样的。非常的 nice。

基本就靠一些CV操作,就把结果弄出来了,可以说非常的简单。

如果你不想打印上面的日志,可以将这个函数进行改写:var v_console_log = function(){{}}

这个插件试用了几天,已经解决了很多网站的加密,几乎可以说js逆向有手就行,当然,你需要一些js逆向分析的功底。毕竟它只是帮你补环境。注意,这个并不是万能的,因此有效网站如果不行的话,可能需要想其他的办法了哈。

实战案例 2:同花顺

  • :https://blog.csdn/weixin_43411585/article/details/132440165
  • :https://blog.csdn/qiulin_wu/article/details/133909883

实战:https://q.10jqka/

反爬参数如图

知识点:cookie 一般是两种,要么服务器生成,要么 js 生成,可通过如下方式判断

为啥要补环境:英文我们知道这个js文件代码内容会生成我们想要的参数,但是放到本地 nodejs 环境下运行不出结果,因为缺少浏览器环境特有的一些window/document/navigator/localstorage等参数,所以我们需要把这些缺少的浏览器环境补上,让这份js代码在本地nodejs环境下也能运行出结果来。请看这个图10秒钟

补环境分三部分:上、中、下。

  • 在最上面放好你补的环境参数,
  • 中间部分放好js代码,
  • 最下面部分放生成的目标参数,

补环境的好处:就是可以完全不用考虑内部的算法逻辑,让它能正常跑起来输出就行

详细分析流程

  • 1.先点击打开如下两个开关,然后打开配置页面

  • 2. 如下插件配置详情,勾选上总开关,DOM开关,以及常用的挂钩,然后关掉该配置页面

  • 3.直接看视频-缺啥补啥的方法补环境:https://www.bilibili/video/BV1FN4y1d7av/ 或者星球文章 后,我们知道生成 cookie 的 js 代码,如图 hook 到了cookie,然后堆栈回溯查看定位到具体的 js 文件内容

对比 cookie,发现 cookie 一致,说明找的位置正确。

  • 4. 在 vscode 里面新建一个js文件,把 js 代码和希望输出的函数写好。还是3部分:

第 1 部分:待补的环境,把插件生成的临时环境放过来。后面

第 2 部分:复制 js 代码,也就是生成 cookie 的 js 文件。

第3部分:调用生成 cookie 的函数,生成 cookie

现在开始 "第1部分:待补的环境",使用插件生成临时环境

  • 继续回到浏览器里面,先清掉缓存 cookie

  • 然后刷新网页 ( 可以把所有断点全部删除 ),在滑动鼠标下滚下,防止无法正常生成临时环境,如下弹出,代表环境参数已经生成好

  • 回到 vscode 里面,在刚才新建的 js 文件里面 ctrl+v 粘贴刚刚生成的临时环境,放在最上方

  • 接下来,运行已经补好环境的 js 文件,最后发现可能会出现如下几种情况:
        1. 生成的补环境,函数之间调用正常,可以直接使用;
        2. 已经能生成正常cookie,但是不能用;例如:一直是固定的。
        3. 完全生成不出 cookie 结果值;

下面针对两种情况进行分析:

  • 一种是直接能用的,
  • 一种是需要调试才能出结果的情况

方法 1:本案例操作流程之-生成临时环境-直接可以用的情况

1. 我们还是按之前的步骤清掉缓存,打开插件的勾选项,然后这里唯一要注意的点就是需要勾选script断点,然后用它生成的临时环境

2.刷新网页后,跳到目标生成cookie的js文件被断住的js后,我们取消script断点,直接下一步调试过去

3. 这时候点击插件的生成临时环境,可能会报错如下,生成的临时环境无法保存至剪贴板

4. 多点击几次生成临时环境就可以了,我们把它复制到本地js文件里面

5. ctrl+v粘贴到本地js文件中,运行能生成正常cookie,但是程序处于卡死无法退出状态

6.ctrl+s保存文件后,再次运行,这个时候已经能正常生成cookie值了,但是同样出现了我们上一篇文章介绍的虽然出来了cookie值,但是无法中断程序退出的现象,就是run后不能自动停掉程序,这时候我们可以尝试将setInterval()定时函数给置空试试,这是因为setinterval不会清除定时器队列,每重复执行1次都会导致定时器叠加,最终卡死你的网页(具体的大家可以调试看看)

7.我们在js代码的最上方的位置添加setInterval = function(){}将定时器置空即可,这时候能正常生成cookie了,并且把日志输出置空var v_console_log = function(){{}}

8.接下来我们验证下这份通过插件补的环境,与js代码生成的cookie最终能不能用,用python调用js文件试试,如下我们调用js生成的cookie验证,发现能成功拿到cookie,这说明我们用插件补环境也能用,而上面插件补环境我们只做了两个操作置空定时器,把日志输出关掉即可,接下来分析调试过程中,可能你生成的临时环境不能用的解决方法

方法 2:本案例操作流程之-生成临时环境-不可以直接用,需要调试补下

1.勾选Caught Exceptions,即使所发生运行时异常的代码在 try/catch 范围内,Chrome 开发者工具也能够在错误代码处停住,这里就不详细介绍了,大概思路如下

文章与视频

  • 原文文章
  • 十一姐b站视频
  • 时光漫漫星球

JS hook 脚本

hook 又称钩子,可以在调用系统函数之前, 先执行我们的函数. 例如, hook eval

eval_ = eval; // 先保存系统的eval函数
eval = function(s){
    console.log(s);
    debugger;
    return eval_(s);
}
eval()
// 可能会被检测到, 用这种方案来进行
eval.toString = function(){return 'function eval() { [native code] }'}  

对 Function 的 hook, 主要为了解决无限 debugger

var qiaofu_function_constructor = (function(){}).__proto__.constructor;
(function(){}).__proto__.constructor = function(arg){
    console.log("我爱你大大");
    if(arg ==='debugger'){
        return function(){}
    } else {
        return new qiaofu_function_constructor(arg);
    }
}

上面都是 hook 的系统函数. 但有时, 我们需要hook某个属性. 此时应该怎么办?

var v;
Object.defineProperty(document, "cookie", {
    set: function(val) {
        console.log("有人来存cookie了");
    	v = val;
        debugger;
        return val;
    },
    get() {
        console.log("有人提取cookie了∂
        debugger;
        return v;
    }
});

在逆向时, 常用的主要有: hook eval 、hook Function 、hook JSON.stringify、JSON.parse 、hook cookie、hook window对象

Js 代码 替换

JS逆向|JavaScript代码线上替换(js 逆向 系列文章):https://blog.csdn/qq523176585/article/details/126258689

有的网站在用 debuger 调试之后,每一步都变得非常卡顿非常影响效率和心情,项目经过webpack处理了,请问这种有什么好的办法吗?

答案是:可以把源码保存到本地,然后用fiddler的atuo response替换线上的js调试

方法 1:使用谷歌浏览器自带的替换功能

Chrome 的 local overrides:https://zhuanlan.zhihu/p/36677472

方法 2:使用 fiddler 的 auto response 替换功能

fiddler 本地资源替换线上文件:https://wwwblogs/shichangchun/p/10731297.html

方法 3:使用 reres 插件进行替换

Github地址:https://github/annnhan/ReRes

使用方法:找不到变量生成的位置?让插件来帮你轻松定位:https://blog.csdn/qq523176585/article/details/109508013

方法 4:使用 Netify 插件进行替换

插件下载地址:https://chrome.google/webstore/detail/netify/mdafhjaillpdogjdigdkmnoddeoegblj
使用方法:工具分析 | Akamai2.0 还原后的js线上替换方案分享:https://blog.csdn/qq523176585/article/details/125093543

方法 5:v_jstools 插件进行替换

使用方法:AST实战技巧|使用v神插件动态替换AST还原后的代码:https://blog.csdn/qq523176585/article/details/124395678

2、jshook ( 安卓上用Js实现Hook )

jshook相关文件下载地址:https://github/JsHookApp/Download

官网文档:https://doc.jshook/#/README

介 绍

jshook 是对应用程序注入 rhino/frida,只需要会 js 就可以快速实现 hook,并且支持 java层 和native层,简化 hook 流程

  • rhino 包含了 xposed 的 api,主要用于java层的hook;
  • frida注入后js可以完全访问内存,支持java层和native层。

在 root 环境下可以安装 jshook 的 Magisk 模块,免root环境下使用Lspatch或者其他虚拟机环境。

频道: https://t.me/jshookapp
交流群: https://t.me/jshookgroup

安 装

真机 root 环境激活

确保手机已经拥有root权限,并且已经安装magisk,https://github/topjohnwu/Magisk
jshook属于xp模块,一般使用lsposed进行激活,https://github/LSPosed/LSPosed
安装lsposed后在模块中启用jshook,并勾选系统框架,重启手机后完成激活
如果没有lsposed也可以安装jshook的面具模块进行激活,jshook应用首页点击安装magisk模块前两个模块用于激活,同时,支持与lsposed共存
lsposed激活的情况下在jshook中启用hook服务后,需要在lsposed的模块作用域中也要勾选对应启用hook服务的应用

真机 免root 环境激活

如果你手机无法root,可以尝试使用lspatch https://github/LSPosed/LSPatch

lspatch激活xp模块的方式是给目标应用进行修补过签名验证并修改程序入口,入口处会加入注入xposed框架以及调用lsp相关服务

安装lspatch后jshook会显示激活

安装完lspatch后你需要先将原应用 提取成apk文件,可以使用 mt管理器 进行操作,lspatch中点击管理右下角+号新建修补,选择apk文件,修补完成后,用mt管理器找到修补后的apk文件,并安装,安装完成后在lspatch的管理中即可看到被修补成功的应用,点击这个应用,会出现菜单,选择模块作用域,勾选jshook即可完成激活

虚拟机环境激活

使用 "vmos、光速虚拟机" 等类似产品进行安装激活操作,操作方式同上两种流程一样

模拟器环境激活

示例:使用夜神模拟器,下载 https://www.yeshen/

添加模拟器选择android9,可以先安装magisk,在这里 下载Magisk Terminal Emulator.apk直接拖到模拟器中会自动安装,打开该应用依次输入以下指令即可完成安装magisk:

inmagisk
y
1
a
1
完成后重启模拟器你可以安装上面root环境激活的方式继续操作

常见问题

目前加密脚本只能使用fridamod框架运行

确定jshook在后台正常运行中且拥有悬浮窗权限,点击设置中的日志悬浮窗即可跳转悬浮窗权限申请,你可以新建脚本,在这里 测试悬浮窗功能

部分机型实时注入脚本会直接发生闪退情况,可以尝试使用启动配置中的延时注入功能,一般设置3000(3秒)左右,部分游戏应用需要更长的时间30000(30秒),在测试之前先取消勾选脚本,排除脚本原因。

连接 计算机

可以使用 Visual Studio Code 实时推送脚本到手机,或者从手机获取脚本到电脑。下载 vscode 插件: https://marketplace.visualstudio/items?itemName=JsonET.jshook-vscode-extension

安装后在js文件右键或者快捷键操作

Shift+Alt+D: 推送脚本

Shift+Alt+C: 获取脚本

Shift+Alt+F: 清空日志

Shift+Alt+G: 查看日志,自动刷新

同步脚本时(脚本名+相对路径)与手机上一致即可,使用插件时需要jshook设置中启用服务端,使用快捷键后会出现输入ip的对话框,让电脑与手机在同一个wifi网络下。

Rhino

使用 rhino 调用 xposed 相关 api 的示例

java 调用xposed 相关 api 说明:https://api.xposed.info/reference/packages.html

而 jshook 中的 rhino 框架提供了以下 xposed 的 api:

XposedBridge
XposedHelpers
AndroidAppHelper
XC_MethodHook
XC_MethodReplacement

现在你只需要使用 rhino 语法用简单的方式去实现。

rhino 语法教程:https://p-bakker.github.io/rhino/tutorials/scripting_java/

以下是常见的一个示例:

XposedBridge.hookAllMethods(XposedHelpers.findClass("android.app.Application", runtime.classLoader), "onCreate", XC_MethodReplacement({
    replaceHookedMethod: function (param) {
        console.log('hook');
        return XposedBridge.invokeOriginalMethod(param.method, param.thisObject, param.args);
    }
}));
XposedBridge.hookAllMethods(XposedHelpers.findClass("android.app.Application", runtime.classLoader), "onCreate", XC_MethodHook({
    beforeHookedMethod: function (param) {
        console.log('hook before');
    },
    afterHookedMethod: function (param) {
        console.log('hook after');
    }
}));

从示例中我们可以看到,几乎和java的写法一致,只是进行了简化

XposedHelpers.findClass(className,classLoader)

XposedHelpers.findClass('com.test.test',runtime.classLoader);

XposedBridge.hookAllConstructors(hookClass,callback)

XposedBridge.hookAllConstructors(XposedHelpers.findClass('com.test.test',runtime.classLoader),XC_MethodHook({
    beforeHookedMethod: function (param) {
        console.log('hook before');
    },
    afterHookedMethod: function (param) {
        console.log('hook after');
    }
}));

XposedHelpers.findAndHookConstructor(clazz,parameterTypesAndCallback)

XposedHelpers.findAndHookConstructor(XposedHelpers.findClass('com.test.test',runtime.classLoader),'java.lang.String','java.lang.String',XC_MethodHook({
    beforeHookedMethod: function (param) {
        console.log('hook before');
    },
    afterHookedMethod: function (param) {
        console.log('hook after');
    }
}));

XposedHelpers.findAndHookConstructor(className,classLoader,parameterTypesAndCallback)

XposedHelpers.findAndHookConstructor('com.test.test',runtime.classLoader,'java.lang.String','java.lang.String',XC_MethodHook({
    beforeHookedMethod: function (param) {
        console.log('hook before');
    },
    afterHookedMethod: function (param) {
        console.log('hook after');
    }
}));

XposedBridge.hookAllMethods(hookClass,methodName,callback)

XposedBridge.hookAllMethods(XposedHelpers.findClass('com.test.test',runtime.classLoader),'method',XC_MethodHook({
    beforeHookedMethod: function (param) {
        console.log('hook before');
    },
    afterHookedMethod: function (param) {
        console.log('hook after');
    }
}));

XposedBridge.hookMethod(hookMethod,callback)

XposedBridge.hookMethod(param.method,XC_MethodHook({
    beforeHookedMethod: function (param) {
        console.log('hook before');
    },
    afterHookedMethod: function (param) {
        console.log('hook after');
    }
}));

XposedHelpers.setStaticObjectField(clazz,fieldName,value)

XposedHelpers.setStaticObjectField(XposedHelpers.findClass('com.test.test',runtime.classLoader),'name','test');

XposedHelpers.setObjectField(obj,fieldName,value)

XposedHelpers.setObjectField(param.thisObject,'name','test');

XposedHelpers.getStaticObjectField(clazz,fieldName)

XposedHelpers.getStaticObjectField(XposedHelpers.findClass('com.test.test',runtime.classLoader),'name');

XposedHelpers.getObjectField(clazz,fieldName)

XposedHelpers.getObjectField(param.thisObject,'name');

XposedHelpers.callMethod(obj,methodName,args)

XposedHelpers.callMethod(param.thisObject,'method','123','456');

XposedHelpers.callStaticMethod(clazz,methodName,parameterTypes,args)

XposedHelpers.callStaticMethod(XposedHelpers.findClass('com.test.test',runtime.classLoader),'method','123','456');

XposedBridge.invokeOriginalMethod(method,thisObject,args)

XposedBridge.invokeOriginalMethod(param.method,param.thisObject,param.args);

更多同上,方式调用都差不多。

如何 hook 加壳的应用

以下是常用的示例,有一些企业壳做了特殊处理,可能不适用

XposedBridge.hookAllMethods(XposedHelpers.findClass('android.app.ActivityThread', runtime.classLoader), 'performLaunchActivity', XC_MethodHook({
    beforeHookedMethod: function (param) {
        console.log('hook before');
    },
    afterHookedMethod: function (param) {
        console.log('hook after');
        var mInitialApplication = XposedHelpers.getObjectField(param.thisObject, 'mInitialApplication');
        var classLoader = XposedHelpers.callMethod(mInitialApplication, 'getClassLoader');
        XposedBridge.hookAllMethods(XposedHelpers.findClass('com.test.test', classLoader), 'test', XC_MethodHook({
            beforeHookedMethod: function (param) {
                console.log('hook before');
            },
            afterHookedMethod: function (param) {
                console.log('hook after');
            }
        }));
    }
}));

API 说明

global 

简易的调用全局函数

toast(message)
        参数: message: string

alert(message)
        参数: message: string

confirm(message,callback)
        参数: message: string
        参数: callback: function

示例:
confirm('is ok?', {
    ok: function () {
        //...
    },
    cancel: function () {
        //...
    }
})

uuid()
        说明:返回32位小写uuid
        返回值:string

setTimeout(function,time)
        参数:function: function,time: int 毫秒
        返回值: int 事件标识
示例:
setTimeout(function () {
    //...
}, 100);

clearTimeout(id)
        参数:id: int 事件标识

setInterval(function,time)
        参数:function: function,time: int 毫秒
        返回值:int 事件标识
示例:

setInterval(function () {
    //...
}, 100);

clearInterval(id)
        参数:id: int 事件标识

runtime

用于获取当前hook运行时相关基础信息

runtime.jsContent
        获取当前注入的脚本内容,注意,如果是加密脚本,获取的不会是解密后的文本
        返回值: string

runtime.appInfo
        返回值: ApplicationInfo

runtime.packageName
        返回值: string

runtime.processName
        返回值: string

runtime.classLoader
        返回值: ClassLoader

runtime.isFirstApplication
        返回值: boolean

runtime.coreVersionCode
        获取当前jshook的版本号
        返回值: int

runtime.coreType
        获取当前注入的类型(1:rhino 2:frida)
        返回值: int

app

用于获取当前hook应用的基本信息

app.isAppRoot()
        判断 App 是否有 root 权限
        返回值: boolean

app.isAppSystem()
        判断 App 是否是系统应用
        返回值: boolean

app.isAppForeground()
        判断 App 是否处于前台
        返回值: boolean

app.exitApp()
        关闭应用

app.getAppInfo()
        获取 App 信息
        返回值: object

app.openUrl(url)
        打开指定网址
        参数:url: string 网址

app.startActivity(activity)
        启动 Activity
        参数:activity: Activity

app.getActivityList()
        获取 Activity 栈链表
        返回值: List

app.finishActivity(activity)
        结束 Activity
        参数:activity: Activity

app.finishToActivity(activity)
        结束到指定 Activity
        参数:activity: Activity

app.startHomeActivity()
        回到桌面

app.dpToPx(value)
        dp转px
        参数:value: float
        返回值: int

app.pxToDp(value)
        px转dp
        参数:value: float
        返回值: int

app.getInternalAppDataPath()
        获取内存应用数据路径
        返回值: string

app.getExternalAppDataPath()
        获取外存应用数据路径
        返回值: string

app.getExternalAppObbPath()
        获取外存应用 OBB 路径
        返回值: string

app.getExternalStoragePath()
        获取外存路径
        返回值: string

base64

用于进行字符串base64编码解码

base64.encode(data)
        参数:data: string
        返回值: string

base64.encodeBytes(data)
        参数:data: byte[]
        返回值: byte[]

base64.decode(data)
        参数:data: string
        返回值: string

base64.decodeBytes(data)
        参数:data: byte[]
        返回值: byte[]

console

用于输出日志

console.log(message)
参数:message: object 会强转为string

crypto

用于进行字符串加密解密

crypto.encrypt(key,data,enctype,transformation)
        参数:
        key: string
        data: string
        enctype: CRYPTO_AES | CRYPTO_DES
        transformation: 转换的名称 例如 DES/CBC/PKCS5Padding
        返回值: string

crypto.encryptBytes(key,data,enctype,transformation)
        参数:
        key: byte[]
        data: byte[]
        enctype: CRYPTO_AES | CRYPTO_DES
        transformation: 转换的名称 例如 DES/CBC/PKCS5Padding
        返回值: byte[]

crypto.decrypt(key,data,enctype,transformation)
        参数:
        key: string
        data: string
        enctype: CRYPTO_AES | CRYPTO_DES
        transformation: 转换的名称 例如 DES/CBC/PKCS5Padding
        返回值: string

crypto.decryptBytes(key,data,enctype,transformation)
        参数:
        key: byte[]
        data: byte[]
        enctype: CRYPTO_AES | CRYPTO_DES
        transformation: 转换的名称 例如 DES/CBC/PKCS5Padding
        返回值: byte[]

crypto.rc4Encrypt(key,data)
        参数:
        key: string
        data: string
        返回值: string

crypto.rc4EncryptBytes(key,data)
        参数:
        key: byte[]
        data: byte[]
        返回值: byte[]

crypto.rc4Decrypt(key,data)
        参数:
        key: string
        data: string
        返回值: string

crypto.rc4DecryptBytes(key,data)
        参数:
        key: byte[]
        data: byte[]
        返回值: byte[]

crypto.md5(data)
        参数:
        data: string
        返回值: string

crypto.md5Bytes(data)
        参数:
        data: byte[]
        返回值: string

crypto.sha1(data)
        参数:
        data: string
        返回值: string

crypto.sha1Bytes(data)
        参数:
        data: byte[]
        返回值: string

crypto.sha256(data)
        参数:
        data: string
        返回值: string

crypto.sha256Bytes(data)
        参数:
        data: byte[]
        返回值: string

device

用于获取当前设备的基础信息

device.isDeviceRooted()
        判断设备是否 rooted
        返回值: boolean

device.isAdbEnabled()
        判断设备 ADB 是否可用
        返回值: boolean

device.getSDKVersionName()
        获取设备系统版本号
        返回值: string

device.getSDKVersionCode()
        获取设备系统版本码
        返回值: int

device.getAndroidID()
        获取设备 AndroidID
        返回值: string

device.getMacAddress()
        获取设备 MAC 地址
        返回值: string

device.getManufacturer()
        获取设备厂商
        返回值: string

device.getModel()
        获取设备型号
        返回值: string

device.getABIs()
        获取设备 ABIs
        返回值: string

device.isTablet()
        判断是否是平板
        返回值: boolean

device.isEmulator()
        判断是否是模拟器
        返回值: boolean

device.isDevelopmentSettingsEnabled()
        开发者选项是否打开
        返回值: boolean

device.getScreenWidth()
        获取屏幕的宽度(单位:px)
        返回值: int

device.getScreenHeight()
        获取屏幕的高度(单位:px)
        返回值: int

device.getAppScreenWidth()
        获取应用屏幕的宽度(单位:px)
        返回值: int

device.getAppScreenHeight()
        获取应用屏幕的高度(单位:px)
        返回值: int

device.getScreenDensity()
        获取屏幕密度
        返回值: float

device.getScreenDensityDpi()
        获取屏幕密度 DPI
        返回值: int

device.isLandscape()
        判断是否横屏
        返回值: boolean

device.isPortrait()
        判断是否竖屏
        返回值: boolean

device.screenShot(activity)
        截屏
        参数:activity: Activity
        返回值: Bitmap

device.setClipboard(data)
        设置剪贴板内容
        参数:data: string

device.getClipboard()
        获取剪贴板内容
        返回值: string

file

用于对文件进行相关操作

file.isFile(path)
        参数:path: string
        返回值: boolean

file.isDir(path)
        参数:path: string
        返回值: boolean

file.isExists(path)
        参数:path: string
        返回值: boolean

file.read(path)
        参数:path: string
        返回值: string

file.readBytes(path)
        参数:path: string
        返回值: byte[]

file.write(path,content)
        参数:
        path: string
        content: string

file.writeBytes(path,content)
        参数:
        path: string
        content: byte[]

file.append(path,content)
        参数:
        path: string
        content: string

file.appendBytes(path,content)
        参数:
        path: string
        content: byte[]

file.copy(path,topath)
        参数:
        path: string
        topath: string

file.move(path,topath)
        参数:
        path: string
        topath: string

file.rename(path,newname)
        参数:
        path: string
        newname: string

file.delete(path)
        参数:path: string

file.getName(path)
        参数:path: string

file.getSize(path)
        返回值: long

file.zip(path,topath,passwd)
        参数:
        path: string
        topath: string
        passwd: string

file.zip(path,topath)
        参数:
        path: string
        topath: string

file.unzip(path,topath,passwd)
        参数:
        path: string
        topath: string
        passwd: string

file.unzip(path,topath)
        参数:
        path: string
        topath: string

http

用于进行网络请求,注意,需要被hook的应用拥有网络访问权限

http.get(url,headers,function)
        参数:
        url: string
        headers: object
        function: function

http.get('http://xxxxx', {
    'test': '1'
}, {
    success: function (result) {
        console.log(result);
    },
    error: function (err) {
        console.log(err);
    }
});

http.post(url,data,headers,function)
        参数:
        url: string
        data: object
        headers: object
        function: function

http.post('http://xxxxx', {
    'user': 'me'
}, {
    'test': '1'
}, {
    success: function (result) {
        console.log(result);
    },
    error: function (err) {
        console.log(err);
    }
});

//或者

http.post('http://xxxxx', JSON.stringify({
    'user': 'me'
}), {
    'content-type': 'application/json'
}, {
    success: function (result) {
        console.log(result);
    },
    error: function (err) {
        console.log(err);
    }
});

注意事项:headers中的key和value必须都是string类型,当data为object时为表单提交,header头部会加入content-type:application/x-www-form-urlencoded,如果提交content-type:application/json,需要确定data参数类型为string

json

用于对字符串进行json编码解码

json.toGsonString(data)
        参数:
        data: object
        返回值: string

json.gsonStringToClass(data,class)
        参数:
        data: string
        class: class
        返回值: class

json.toJSONString(data)
        参数:
        data: object

json.parseObject(data)
        参数:
        data: string
        返回值: JSONObject

json.parseArray(data)
        参数:
        data: string
        返回值: JSONArray

modmenu

这是一个mod菜单,可以定制简单的菜单悬浮窗,注意,你需要让jshook保持后台运行状态,并且授权悬浮窗权限,可以创建多个悬浮窗实例

modmenu.create(title,options,function)
        参数:
        title: string
        options: object
        function: function
        返回值: 实例

modmenu.create('test mod',
    [
        {
            'id': '1',
            'type': 'category',
            'title': 'category title'
        },
        {
            'id': '2',
            'type': 'switch',
            'title': 'switch1 title',
            'enable': true
        },
        {
            'id': '3',
            'type': 'switch',
            'title': 'switch2 title'
        },
        {
            'id': '4',
            'type': 'webview',
            'data': '<font color="red"><b>text</b></font>',
            //or
            //'url': 'http://xxxxx'
        },
        {
            'id': '5',
            'type': 'button',
            'title': 'button title'
        },
        {
            'id': '6',
            'type': 'input',
            'title': 'input title',
            'val': 'default value'
        },
        {
            'type': 'collapse',
            'title': 'collapse title',
            'item': [
                {
                    'id': '7',
                    'type': 'switch',
                    'title': 'switch title'
                }
            ],
            'enable': true
        },
        {
            'id': '8',
            'type': 'slider',
            'title': 'slider title',
            'val': 88,
            'min': 1,
            'max': 100
        },
        {
            'id': '9',
            'type': 'checkbox',
            'title': 'checkbox title',
            'enable': true
        },
        {
            'type': 'checkboxs',
            'item': [
                {
                    'id': '10',
                    'type': 'checkbox',
                    'title': '1 title'
                },
                {
                    'id': '11',
                    'type': 'checkbox',
                    'title': '2 title'
                },
                {
                    'id': '12',
                    'type': 'checkbox',
                    'title': '33333333 title'
                }
            ]
        },
        {
            'id': '13',
            'type': 'radio',
            'title': 'radio title',
            'item': ['test', 'test2', 'test3'],
            'check': 0
        }
    ]
    , {
        onchange: function (result) {
            //注意在这里需要进行一下转换
            result = JSON.parse(result);
            switch(result.id) {
                case '1':
                    console.log('ok');
                    break;
            }
            console.log(result);
        }
    })

[实例].close()

var menu1 = modmenu.create('test mod', [], {
    onchange: function (result) {
    }
});
setTimeout(function () {
    menu1.close();
}, 2000);

[实例].state()

显示悬浮窗底部状态栏,目前只有简单的包名显示

[实例].size(width,height)
        调整悬浮窗大小,px值
        参数:
        width: int
        height: int

[实例].position(type,x,y)
        设置悬浮窗默认位置,type为1到9对应9个默认位置,x和y是px值
        参数:
        type: int
        x: int
        y: int

[实例].icon(img)
        设置悬浮窗图标图片,img为图片base64字符串或者是图片url地址
        参数:
        img: string

[实例].webviewcall(id,args)
        调用webview组件中的方法,id为webview组件id,args为参数string字符串
        参数:
        id: int
        args: string

var menu1 = modmenu.create('test mod',
[
    {
        'id': '4',
        'type': 'webview',
        'data': '<!DOCTYPE html><html><body><input id="input1" type="text" value="123" /><div id="test"><font color="red" onclick="test();"><b>点我</b></font></div><script>window.inputset = function(res){document.getElementById("input1").value = res}; document.getElementById("input1").addEventListener("click", function() {    jsCallMethod("input",document.getElementById("input1").value,function(res){inputset(res);});  });function test(){jsCallMethod("test","time: ",function(a){document.getElementById("test").innerHTML = a;});}</script></body></html>',
        //or
        //'url': 'http://xxxxx'
    },
]
, {
    onchange: function (result) {
        console.log(result);
    },
    webviewcallback: function (result) {
        console.log(result);
        var result = JSON.parse(result);
        if (result.method == 'onload') {
             console.log('init');
        }if (result.method == 'test') {
            menu1.webviewcall(result.id, '<font color="red" onclick="test();"><b>点我后:' + result.args + new Date().getTime() + '</b></font>');
        } else if (result.method == 'input') {
            dialog.input('标题', {
                ok: function (res) {
                    console.log(res);
                    menu1.webviewcall(result.id, res);
                },
                cancel: function () { }
            }, result.args);
        }
    }
});

[实例].edgeHiden(enable)
        icon图标拖动到屏幕边缘自动隐藏,默认不开启
        参数:enable: boolean

modmenu.closeAll()
        关闭所有创建的mod菜单

注意事项

  • 每个webview组件网页加载完成都是触发webviewcallback result.method为onload的事件,可以在这里做一些脚本中的初始化处理,当webview中的input点击后触发脚本中的dialog.input等待用户输入,输入完成后回传给webview中在赋值到input中。

  • webview会注入jsCallMethod方法,为3个参数,method, args, callback,示例中,并没有在callback回调中直接操作input,因为回调是独立的,不能获取到任何外部局部变量,你需要将视图更新事件注册到window中,才可以在callback回调中使用。

storage

用于本地数据的存储,数据存在被hook的应用私有路径中,

storage.get(key)
        参数:key: string
        返回值: string

storage.set(key,data)
        参数:
        key: string
        data: string

storage.del(key)
        参数:key: string

storage.clear()

convert

用于常用的转换

convert.stringToByte(data)
        参数:data: string
        返回值: byte[]

convert.byteToString(data)
        参数:data: byte[]
        返回值: string

convert.hexStringToByte(data)
        参数:data: string
        返回值: byte[]

convert.byteToHexString(data)
        参数:data: byte[]
        返回值: string

convert.byteToBitmap(data)
        参数:data: byte[]
        返回值: Bitmap

convert.bitmapToByte(data)
        参数:data: Bitmap
        返回值: byte[]

convert.byteToDrawable(data)
        参数:data: byte[]
        返回值: Drawable

convert.drawableToByte(data)
        参数:data: Drawable
        返回值: byte[]

convert.byteToInputStream(data)
        参数:data: byte[]
        返回值: InputStream

convert.inputStreamToByte(data)
        参数:data: InputStream
        返回值: byte[]

convert.byteToOutputStream(data)
        参数:data: byte[]
        返回值: OutputStream

convert.outputStreamToByte(data)
        参数:data: OutputStream
        返回值: byte[]

dialog

常用的对话框

dialog.input(title,callback,content)
        参数:
        title: string
        callback: function
        content: string

dialog.input('标题', {
    ok: function (res) {
        console.log(res);
    },
    cancel: function () { }
}, '默认值');

view

视图层操作

view.findViewByText(text)
        查找当前activity所有包含字符的TextView
        参数:
        text: string
        返回值: List<View>

var time1 = setInterval(function () {
    var view1 = view.findViewByText('Text');
    if (view1.size() > 0) {
        clearInterval(time1);
        view1.get(0).setText('fuck');
        var p = view.getPosition(view1.get(0));
        keys.click(p.get(0), p.get(1));
        setTimeout(function () {
            keys.back();
        }, 2000);
    }
}, 100);

view.getPosition(view)
        获取view的位置
        参数:view: View
        返回值: List<int>

view.findViewPositionByText(text)
        通过查找所有包含字符的TextView的位置
        参数:text: string
        返回值: List<List<int>>

view.runOnUiThread(function)
        在ui线程中执行
        参数:function: function

view.runOnUiThread({
    run: function () {
        //...
    }
});

keys

模拟按键操作,需要安装Magisk模块才可以调用

keys.click(x,y)
        模拟点击x,y位置
        参数:
        x: int
        y: int

keys.swipe(x,y,tox,toy,time)
        模拟滑动,从x,y位置滑动到tox,toy位置,time持续时间
        参数:
        x: int
        y: int
        tox: int
        toy: int
        time: int

keys.back()
        返回上一页

keys.home()
        返回首页

keys.enter()
        回车

keys.del()
        删除

keys.text(text)
        模拟输入文本,请注意调用这个需要确定处于文本输入焦点状态

colors

用于颜色字符串与数值的转换

colors.toString(color)
        返回颜色值的字符串,格式为 "#AARRGGBB"
        参数:color: int rgb颜色值
        返回值: string

colors.red(color)
        返回颜色color的R通道的值,范围0 ~ 255
        参数:color: string 字符串颜色值
        返回值: int

colors.green(color)
        返回颜色color的G通道的值,范围0 ~ 255
        参数:color: string 字符串颜色值
        返回值: int

colors.blue(color)
        返回颜色color的B通道的值,范围0 ~ 255
        参数:color: string 字符串颜色值
        返回值: int

colors.alpha(color)
        返回颜色color的Alpha通道的值,范围0 ~ 255
        参数:color: string 字符串颜色值
        返回值: int

colors.rgb(red,green,blue)
        返回这些颜色通道构成的整数颜色值
        参数:
        red: int 颜色的R通道的值
        green: int 颜色的R通道的值
        blue: int 颜色的R通道的值
        返回值: int

colors.argb(alpha,red,green,blue)
        返回这些颜色通道构成的整数颜色值
        参数:
        alpha: int 颜色的Alpha通道的值
        red: int 颜色的R通道的值
        green: int 颜色的R通道的值
        blue: int 颜色的R通道的值
        返回值: int

colors.parseColor(color)
        返回这些颜色通道构成的整数颜色值
        参数:
        color: string 字符串颜色值
        返回值: int

canvas

创建全屏的画板,可以在画板中进行图形绘制,支持创建多个画板

canvas.create(function)
        创建画板实例
        参数:
        function: function
        返回值: 实例

[实例].close()
        关闭画板

[实例].maxfps(fps)
        调整绘制最大帧数,默认为30
        参数:fps: int 帧数整数值

canvas.closeAll()
        关闭所有创建的画板

frida 简单示例

var dw = device.getScreenWidth();
var dh = device.getScreenHeight();
var playerCount = 10;
var players = [];
var Canvas_Class = Java.use('android.graphics.Canvas');
var Paint_Class = Java.use('android.graphics.Paint');
var Paint_Style_Class = Java.use('android.graphics.Paint$Style');
var Bitmap_Class = Java.use('android.graphics.Bitmap');
var Bitmap_Config_Class = Java.use('android.graphics.Bitmap$Config');

var canvas1 = canvas.create({
    ondraw: function () {
        var bitmap = Bitmap_Class.createBitmap(dw, dh, Bitmap_Config_Class.ARGB_8888.value);
        var canvast = Canvas_Class.$new(bitmap);
        var paint = Paint_Class.$new();
        paint.setStyle(Paint_Style_Class.STROKE.value);
        paint.setStrokeWidth(3);
        for (var i = 0; i < players.length; i++) {
            paint.setColor(colors.parseColor(players[i][4]));
            canvast.drawRect(
                players[i][0],
                players[i][1],
                players[i][0] + players[i][2],
                players[i][1] + players[i][3],
                paint
            );
            canvast.drawLine(dw / 2, 0, players[i][0] + players[i][2] / 2, players[i][1], paint);
        }
        return bitmap;
    }
});

function random(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function playerdata() {
    var newPlayers = [];
    for (var i = 0; i < playerCount; i++) {
        var playerWidth = random(30, 100);
        var playerHeight = 2 * playerWidth;
        var color = random(0, 1) ? "#ff0000" : "#00ff00";
        var player = [
            random(0, dw),
            random(0, dh),
            playerWidth,
            playerHeight,
            color
        ];
        newPlayers.push(player);
    }
    players = newPlayers;
}

setInterval(function() {
    dw = device.getScreenWidth();
    dh = device.getScreenHeight();
}, 1000);

setInterval(function() {
    playerdata();
}, 100);

本文标签: 网页 JS vjstools hook 安卓上用