js逆向-加解密定位

艺帆风顺 发布于 2025-04-10 50 次阅读


声明

本文仅作学习参考,如有侵权请联系作者删除,切勿用于其他途径。若认为本文对您有所帮助,欢迎关注或收藏!

加解密定位

当我们从某个网站的接口抓取数据时,往往会需要携带加密参数或者需要解密返回回来的密文。但是网站会加载大量JavaScript文件,要在这么多JavaScript文件快速找到关键的加解密位置,显得尤为重要。

接口分析

1、加密:

1)找到我们所需要抓取数据的接口;

2)观察请求头、cookies、请求参数、请求体中是否有加密参数;

3)分析加密参数的值是变化的还是固定不变的,可不可以直接复制;

(通过多次请求可以观察到有些加密参数的值虽然是个密文,但是每次请求中,这个加密参数的值却是不变的,那么我们就可以直接使用这个密文,不用去逆向它如何生成而来的)

4)分析加密参数的值是本地生成的还是服务器返回的。

(有些网站的加密参数的值就是在之前的请求中返回到了给我们,我们则不需要去逆向,直接找到返回密文的请求,对它进行请求然后将它返回的响应结果添加到我们数据接口的请求中来。当我们在前端页面中只执行了一个操作,不一定就是只对应一个请求,可能会连带多个请求,那么我们的加密参数的值可能就在前面的请求中返回给我们的)

2、解密:

1)找到我们所需要抓取数据的接口;

2)观察该请求返回回来的数据是否是密文。

定位技巧
1、关键字搜索
前面的文章我们都是用的关键字搜索,想必这个方法也不会很陌生。但是关键字并非只可以搜索参数名。
比如参数是sign,可以是sign、sign:、sign =、sign :、sign = 等等
(加密)encrypt(、(解密)decrypt(、
headers[
JSON.parse(、JSON.stringify(
interceptors
(请求拦截器interceptors.request)、(响应拦截器interceptors.response)
等等
2、正常堆栈分析
堆栈是程序执行时记录函数调用关系的数据结构,遵循"后进先出"(LIFO)原则。每当函数被调用时,其上下文会被压入堆栈;函数执行完毕时弹出堆栈。
function funcA() {  console.log("进入 funcA");  funcB();                      // 调用 funcB  console.log("离开 funcA");}function funcB() {  console.log("进入 funcB");  funcC();                      // 调用 funcC  console.log("离开 funcB");}function funcC() {  console.log("进入 funcC");  console.trace("当前堆栈");     //打印堆栈  console.log("离开 funcC");}// 执行入口funcA();// ===== 输出结果 =====// 进入 funcA// 进入 funcB// 进入 funcC// 当前堆栈//   funcC (test.js:15:11)//   funcB (test.js:9:3)//   funcA (test.js:3:3)//    (test.js:20:1)   所有不在任何具体名字的函数内(匿名函数)以及全局作用域内的代码执行,都会被标记为 (anonymous)。// 离开 funcC// 离开 funcB// 离开 funcA// 相当于全局作用域下执行程序,(anonymous)被压入堆栈// 调用funcA,funcA被压入堆栈// funcA内调用funcB,funcB被压入堆栈// funcB内调用funcC,funcC被压住堆栈// 然后funcC执行完弹出堆栈,// funcB执行完弹出堆栈,// funcA执行完弹出堆栈,// 最后全局作用域下的调用执行结束也弹出堆栈。
3、异步堆栈分析
1)堆栈、任务队列以及事件循环
// 堆栈:JavaScript 引擎使用调用堆栈来管理函数调用。当一个函数被调用时,它会被压入调用堆栈中,执行完成后,它会被弹出堆栈。// 任务队列:用于存储待执行的异步任务,分为宏任务队列和微任务队列。当异步操作完成时,其回调函数会被推入任务队列中,等待事件循环处理。// 事件循环:事件循环负责协调调用堆栈和任务队列。当调用堆栈为空时,事件循环会从任务队列中取出任务并执行。// 事件循环的工作方式:// 1、执行堆栈中的同步代码;// 2、同步代码执行完毕后,检查微任务队列。// 3、执行所有微任务,直到队列为空。// 4、检查宏任务队列,执行一个宏任务。// 5、重复上述过程。
2)宏任务、微任务
// 宏任务:// 定义:较大的异步任务,执行时间较长。// 常见来源:setTimeout、setInterval、I/O 操作、UI 渲染// 执行时机:微任务队列清空后,执行一个宏任务。// 微任务:// 定义:较小的异步任务,执行时间较短。// 常见来源:Promise的 .then、.catch、.finally// 执行时机:在当前宏任务结束后,立即执行所有微任务。
3)同步代码
console.log("1");console.log("2");  // 输出顺序:1 → 2// 特点:简单直观,1执行完了才执行2
4)Promise(微任务):

Promise 是 JavaScript 中的一种异步编程解决方案,用于处理异步操作的结果(成功或失败)。

Promise有三种状态

pending(进行中):初始状态,既没有成功也没有失败。

fulfilled(已成功):操作成功完成。

rejected(已失败):操作失败。

状态一旦从pending变为fulfilled或rejected,就不可更改(不可逆)。

可以通过 .then()(成功执行) 和 .catch()(失败执行) 以及.finally ()(无论成功还是失败都执行)方法处理结果。

基本语法:

const promise = new Promise((resolve, reject) => {  const success = true// 模拟操作结果  if (success) {    resolve("操作成功"); // 状态变为 fulfilled  } else {    reject("操作失败");   }});// 使用 .then() 和 .catch() 处理结果promise  .then(result => {    console.log(result); // 输出:操作成功  })  .catch(error => {    console.error(error);   })  .finally(() => {    console.log("我就要执行")  // 输出我就要执行  });

Promise的链式调用:

.then()返回一个新的Promise,可以连续调用,形成链式结构。

.catch()用于捕获链中任何地方抛出的错误。

new Promise((resolve, reject) => {  resolve(1);})  .then(result => {    console.log(result); // 输出:1    return result + 1;  })  .then(result => {    console.log(result); // 输出:2    throw new Error('发生错误'); // 抛出错误  })  .then(result => {    console.log(result); // 不会被执行  })  .catch(error => {    console.error(error.message); // 捕获错误并输出:发生错误  });

Promise 的静态方法:

Promise.resolve(value):返回一个状态为fulfilled的Promise,结果为value。

Promise.resolve('成功').then(result => console.log(result)); // 输出:成功

Promise.reject(reason):返回一个状态为rejected的Promise,结果为reason。

Promise.reject('失败').catch(error => console.error(error)); // 输出:失败

Promise.all(iterable):接收一个Promise数组,返回一个新的Promise。如果所有Promise都为成功,则新Promise的结果为所有结果的数组。如果有一个Promise失败则新Promise立即失败。

const p1 = Promise.resolve(1);        // p1成功const p2 = Promise.resolve(2);        // p2成功const p3 = Promise.reject('错误');    // p3失败Promise.all([p1, p2, p3])  .then(results => console.log(results))  .catch(error => console.error(error)); // 输出:错误
还有很多Promise 的静态方法,这里就不一一做赘述。
Promise实现异步代码
console.log('1');const myPromise = new Promise((resolve, reject) => {  console.log("2");  resolve("成功");  // 如果失败,可以使用 reject("失败");  console.log("3")});myPromise.then((res) => console.log("4" + res));console.log("5");// 输出顺序:1 → 2 → 3 → 5 → 4成功// 1执行完了执行构造函数中的代码执行2,然后执行mypromise的状态从Pending转到Fulfilled(成功),执行3,.then()、.catch()以及.finally()里面的回调函数是异步执行的,它们会被放入微任务队列中,然后执行5.此时同步代码已经执行完,执行微任务的代码输出4.// 微任务优先于宏任务执行,适合处理高优先级异步操作。
5)async/await(基于 Promise 的语法糖)
async用于声明一个异步函数。异步函数会隐式返回一个Promise对象。如果手动设
置函数的返回值,那么也会自动变成Promise.resolve(返回值)。
await用于等待一个Promise的结果。await只能在async 函数中使用。等待它会暂停当前 async 函数的执行,直到Promise完成(resolve 或 reject)。
await会暂停 async 函数的执行,直到 Promise 完成。
Promise完成后,await 后面的代码会被安排在微任务队列中执行。
console.log("1");async function example() {  console.log("2");  await new Promise(function (resolve) {    resolve(); // 立即将 Promise 状态变为 fulfilled    console.log("3"); // 这是 Promise 内部的同步代码,会立即执行  });  console.log("4"); // 这段代码会作为微任务执行,相当于.then()回调}example();console.log("5");// 输出顺序// 1// 2// 3// 5// 4
6)setTimeout(宏任务)
setTimeout和setInterval都是JavaScript 提供的定时器函数,前者用于在指定的时间后执行某段代码(回调函数),后者用于按照指定的时间间隔重复执行某段代码(回调函数)。如果相同延迟的setTimeout:严格按注册顺序执行,当延迟不同时,较短的延迟会先执行。
console.log("1");setTimeout(() => console.log("2"), 0);console.log("3");  // 输出顺序:1 → 3 → 2// 1执行完了执行3,setTimeout里面的回调函数加入宏任务队列,等待同步任务执行完再执行。// 即使延迟设为 0,实际执行可能因同步代码阻塞而延迟。// 属于异步中的宏任务,优先级低于微任务(如 Promise)。
7)组合
console.log("1");setTimeout(() => {  console.log("2");                    // 宏任务}, 0);Promise.resolve().then(() => {  console.log("3");                    // 微任务 });async function main() {  await 0;  console.log("4");                    // 微任务}main();console.log("5");// 执行顺序:// 1// 5// 3// 4// 2// console.log("1") → 输出 1//setTimeout 的回调(输出2)被放入宏任务队列。// Promise.resolve().then 的回调(输出3)被放入微任务队列。// 调用 main() 函数,遇到 await 0(等价于 await Promise.resolve(0)),函数暂停,console.log("4") 被包装成微任务,放入微任务队列。// console.log("5") → 输出 5。// 处理微任务队列(先进先出):// 先执行 Promise.resolve().then 的回调 → 输出 3。// 再执行 main() 中 await 后的代码 → 输出 4。// 处理宏任务队列://setTimeout 的回调 → 输出 2。
8)堆栈先执行同步任务接着微任务然后再是宏任务
任务类型
执行优先级
典型实例
同步任务
最高(阻塞主线程)
console.log()、变量赋值、普通函数调用等
微任务
次高(宏任务之前)
Promise.then()、await等
宏任务
最低
setTimeout、setInterval和DOM事件回调等
说白了就一句话,咱们逆向加解密的位置可能在异步的回调函数中。
4、hook
hook函数:在js逆向中,我们通常把替换原函数的过程都称为Hook。(重写会被调用的函数,然后使其debugger方便回溯调试,并不要改变原执行流程调用原函数)

1)Hook Eval

(function () { // 保存原始的 eval 方法 window.__cr_eval = window.eval;    // 自定义 eval 方法    var myEval = function (src) {    // 打印被传递到 eval 的代码    console.log("执行的 eval 代码:");    console.log(src);    console.log("=============== eval end ===============");    // 触发调试器,便于调试    debugger;    // 调用原始的 eval 方法    return window.__cr_eval(src);};    // 绑定上下文并保留原始 toString 方法    var _myEval = myEval.bind(null);    _myEval.toString = window.__cr_eval.toString;    // 使用 Object.defineProperty 重写 window.eval    Object.defineProperty(window'eval', {    value: _myEval,    writablefalse// 禁止重新赋值    configurablefalse // 禁止重新配置    });    console.log("window.eval 已被挂钩");})();
2)Hook JSON.stringfy
(function() {   // 保存原始的 JSON.stringify 方法  var originalStringify = JSON.stringify; // 重写 JSON.stringify 方法   JSON.stringify = function(params) {    // 打印日志,方便调试  console.log("Hook JSON.stringify ->", params);    // 触发调试器     debugger;   // 调用原始的 JSON.stringify 方法     return originalStringify(params);    };   console.log("JSON.stringify 已被挂钩");})();
3)Hook JSON.parse
(function() {   // 保存原始的 JSON.parse 方法  var originalParse = JSON.parse; // 重写 JSON.parse 方法   JSON.parse = function(params) {     // 打印日志,便于调试     console.log("Hook JSON.parse ->", params);     // 触发调试器     debugger;    // 调用原始的 JSON.parse 方法    return originalParse(params);  };  console.log("JSON.parse 已被挂钩");})();
4)Hook Header
(function () {  // 保存原始的 XMLHttpRequest.prototype.setRequestHeader 方法   var originalSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader // 重写 setRequestHeader 方法    window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {       // 检查是否拦截到 Authorization 头        if (key === 'Authorization') {         console.log("拦截到 Authorization 请求头:", value);          // 触发调试器         debugger;       }      // 调用原始 setRequestHeader 方法       return originalSetRequestHeader.apply(thisarguments);     };    console.log("XMLHttpRequest.setRequestHeader 已被挂钩");})();
5)Hook Cookie
(function () {  'use strict' // 临时存储 cookie 的变量   var cookieTemp = '' // 使用 Object.defineProperty 重写 document.cookie 的 setter 和 getter   Object.defineProperty(document'cookie', {     setfunction (val) {      // 如果设置的 cookie 包含特定关键字 '__dfp',触发调试器      if (val.indexOf('__dfp') !== -1) {       console.log('拦截到包含 "__dfp" 的 cookie 设置:', val);      debugger;      }      // 打印捕获到的 cookie 设置      console.log('Hook 捕获到 cookie 设置 ->', val);      // 更新临时存储变量      cookieTemp = val;       return val;     },    getfunction () {       // 返回临时存储的 cookie``       return cookieTemp;     }); console.log('document.cookie 的 setter 和 getter 已被挂钩');})();
断点技巧
1)普通断点:普通断点就是直接点击js代码左边打上断点,程序在代码的指定行暂停执行,用于观察变量状态和调用堆栈。
2)条件断点:当指定条件为真时暂停执行。过滤循环中的特定迭代或特定参数值的触发。
3)日志断点:不暂停执行,仅在控制台输出日志。跟踪高频触发事件或记录变量变化。
4)dom断点:在某个特定的DOM元素上设置了一个断点。一旦该元素被修改、删除或者属性发生变化,断点就会触发,浏览器会暂停执行JavaScript 代码,使你能够检查变化的内容、调试代码或者分析程序行为。
5)xhr断点(俗称小黄人断点):xhr全称 XMLHttpRequest 。前面说过我们可能在前端页面中只操作了一下,但是不一定只发送一个请求,所以需要使用到xhr断点精准断到我们需要的请求。(注:主要针对xhr类型的请求)
6)事件监听断点:在事件触发时暂停。(如点击、键盘、脚本等事件)
例一、关键字搜索 + 普通断点
1、目标URL
aHR0cHM6Ly9wYXNzcG9ydDIuY2hhb3hpbmcuY29tL2xvZ2luP2ZpZD0mbmV3dmVyc2lvbj10cnVlJnJlZmVyPWh0dHBzJTNBJTJGJTJGaS5jaGFveGluZy5jb20=
2、接口分析
复制链接到浏览器上打开网站,打开开发者工具,随便输入手机号和密码点击登录,去网络面板中找到触发的登录请求。
这里面有uname和password这两个参数加密了的,分别对应的是我们的手机号和密码。我们就去定位一下它在何处进行的加密。
3、加密定位
先试试关键字搜索,按Ctrl+Shift+F打开全局搜索,直接搜索password:
只有一行,但是从外面看这个明显不是正确的地方,键为password,但是值为!0,也就是true,明显不对。那我们直接搜索password试试。
发现6个文件中共有26行,其实也不算多,可以一个一个简单分析一下。关键字搜索搜索参数名,搜到的结果必须是参数名单独存在的,然后它要么是变量赋值的形式存在,要么是键值对的形式存在,也可以是以键名的形式存在(比如参数名为password,不能是hpassword、passwords这种带有前缀或者后缀的,只能是password = 、password : 、data["password"] = 、'password'  : 等等)
最终发现如图所框的几个可疑点,点击去打上断点。
像这种在‘password’ : pwd处是打不上断点的,断点会自动落在最近的下一步有操作的代码处。这样我们需要观察的代码都已经执行结束了,所以可以在$.ajax这里打上断点,也可以找到最近出现pwd变量的地方打上断点。
其它几处有这种情况同样的方法。都打上断点之后,重新登录触发一下登录请求。
发现断点就断在此处,且是个密文内容。释放断点继续执行,看看其他几个断点是否会断住。(这样做的目的是判断其他断点是否也进行了操作,来个二次加密什么的)最终都没在其他断点断住,将其他断点给取消了。但这也不能说明这就一定是加密的最终结果。最终确定是不是此处加密还得与所对应的请求包实际携带的参数密文对比。
将断点打下来,让两个加密执行完。(这样做的目的是为了让pwd和phone的值固定下来。如果它是变化的密文,就连你控制台第一次输出的值和第二次输出的值都对不上,更不用说和最终的请求携带的参数密文对比了。例如非对称加密不就是每次加密的密文不一样吗,就算不是非对称加密,可能也会有什么时间戳和随机数的影响导致每次加密的密文不同)
其实图中就可以看到两个变量的值了。我们可以赋值这两个值到本地记录着,也可以去控制台输出一下两个变量的值记录在控制台。
pwd和phone分别对应的就是我们的密码(password)和电话号码(uname)。释放断点查看对应的请求。
像这种较短的密文,其实肉眼既可以分辨出是否一致。如果密文特别长,我们一个一个字母去对比,显然不合适。所以可以去控制台去判断,前面几篇文章已经阐述,所以在这里就不再过多去赘述。密文一致,那么加密位置就是在此处。
例二、正常堆栈分析+ 普通断点 + xhr断点
1、目标URL
aHR0cHM6Ly93d3cuaml6aHkuY29tLzQ0L3Jhbmsvc2Nob29s
2、接口分析
还是这个网址还是这个请求,找到对应的请求接口。
还是逆向uname和password这两个加密参数。
3、加密定位
点击启动器查看此请求的调用堆栈也就是该程序执行时记录函数调用关系的数据结构。
这是一个非异步的调用堆栈,从下往上的执行顺序,一个函数调用一个函数这样的关系。我们进入最后一个函数,也就是发送请求前的最后一步,打上断点。但是我们会发现我还没有重新登录,程序就自动断在此处。
正常情况下是我们重新触发登录请求,断点才会生效。说明此时断住的并非我们的登录请求,还有其他请求会经过我们这个断点。去查看下网络面板。
我们需要跟栈登录的请求,那么就得断住对应的请求才对。也可以发现这请求类型都是xhr的,那么我们就可以通过xhr断点断住我们想要的请求。复制该请求网址的路径。
前往源代码面板,右侧有个XHR/提取断点,添加一个xhr断点。
将前面那个普通断点给取消了。此时再去重新触发一下登录请求。
发现成功断住,而且可以发现断住的是当前的登录请求。断住的位置也是请求刚准备发送的位置。我们可以通过右侧的调用堆栈溯源。
其实通过函数名就能发现大概位置在第三或者第四个中。但是我们也可以一层一层的来,首先点击到ajax中。
堆栈分析配合着作用域来看。我们要明确我们需要找的是什么?我们要找到密文数据,跟踪密文数据,找到何时从明文到密文的时候。可以去右侧作用域中看是否有咱们想要找的密文数据。
可以发现此时的电话和密码都是密文的状态。说明程序在ajax中运行到调用send这句代码时,我们的明文电话和密码都已经被加密了。我们向上跟栈。
发现执行到在loginByPhoneAndPwdSubmit执行到调用ajax这句代码时,明文电话和密码也是密文。再次向上跟栈。
发现电话是明文的,密码是密文的?电话在loginByPhoneAndPwd内调用loginByPhoneAndPwdSubmit时都是明文的,但是又在loginByPhoneAndPwdSubmit执行到调用ajax这句代码时是密文的。说明电话是在loginByPhoneAndPwdSubmit内部且调用ajax这句代码之前加密的。所以我们回到loginByPhoneAndPwdSubmit中。
观察代码,给pwd出现的位置前后一步打上断点,释放程序,让程序重新执行。
此时pwd和电话都是明文的。看到这里肯定有人疑问,为什么前面的密码不是已经加密了吗,怎么这里又是明文状态。是因为前面加密了,但是并没有把密文传到loginByPhoneAndPwdSubmit中也没有设置到全局变量中,所以对做逆向的我们来说他那个加密完全没有意义。让程序执行下来。
此时电话和密码都还是明文的,让程序走到下一个断点。
可以发现电话和密码就是密文了。程序在195行的时候都还是明文,在199行的时候就成了密文。那不用多说了,肯定是195、196、197、198这四行的原因。这就是一个AES加密,195行是密钥、196是密码加密、197是电话加密,198反大括号。接下来就是验证此处加密的结果和彻底释放断点对应的请求的加密参数对比是否一致。这个就不多说了。
例三、异步堆栈分析+ 普通断点
1、目标URL
aHR0cHM6Ly93d3cuaml6aHkuY29tLzQ0L3Jhbmsvc2Nob29s
2、接口分析
复制链接到浏览器上打开网站,打开开发者工具,滑到页面最底端,点击查看更多。去网络面板中找到触发的获取数据请求。
其他参数先不管,咱们去找sign参数是怎么生成的。
3、加密定位
点击启动器查看此请求的调用堆栈也就是该程序执行时记录函数调用关系的数据结构。
中间有一处Promise.then,这是一个异步的调用堆栈。还是点击最后一个堆栈,并打上断点,再次触发数据请求。
发现程序被断在了这里,也就是发送请求的最后一步。那么我们向上跟栈。
跟到这个栈的时候咱们的密码都还是密文,然后继续向上跟。
但是我们点击h.request,跟不上来。这就是因为异步的原因,我们需要在所高亮的这一行打上一个断点。然后释放程序,再次触发请求。
可以发现成功断住了,可是params里的sign字段没有。如果我们按正常的堆栈分析,就是去后面的那个函数中去找加密了。我们可以去看看,给后面的函数第一行就打上断点,让程序执行过去。
可以发现第一句代码都还没执行,params里又有sign字段了。那就奇怪了,加密跟丢了,在哪儿添加进去的都找不到了。这就是异步栈,跟着跟着发现找不到。别忘了中间Promise.then,从字面简单理解这个就是处理异步回调的。回到h.request中。
简单解释下这段代码n = n.then(e.shift(), e.shift()),
e为一个16长度的数组,里面存储着多个函数。
e.shift()取出前数组中第一个元素,e取出一个就减少一个。
then方法接受两个回调函数onFulfilled(成功的回调)和 onRejected(失败的回调)。
n在上方等于Promise.resolve(t),这不就是Promise的静态方法吗,n为成功状态且结果为t。
查看t的值:
t正好是无sign字段的对象。
所以整段代码的意思就是从一个函数数组中取出函数,绑定回调函数。数组中第一个、第二个元素分别绑定为Promise.resolve(t)的成功回调和失败回调并接收t的值。.then方法会返回一个新的Promise对象。此时又给新的Promise对象绑定数组中第三个、第四个元素作为成功回调和失败回调并接收上一个回调函数的返回值,依次类推,直到e里面的函数绑定完。
可以将这段代码改成Promise的链式调用:
Promise.resolve(t)  .then(e[0], e[1])    // 使用索引01(成功回调为函数,失败回调为undefined)  .then(e[2], e[3])    // 使用索引23(成功回调为undefined,失败回调为函数)  .then(e[4], e[5])    // 使用索引45(成功回调为函数,失败回调为undefined)  .then(e[6], e[7])    // 使用索引67(成功回调为函数,失败回调为undefined)  .then(e[8], e[9])    // 使用索引89(成功回调为函数,失败回调为undefined)  .then(e[10], e[11])  // 使用索引1011(成功回调为函数,失败回调为undefined)  .then(e[12], e[13])  // 使用索引1213(成功回调为undefined,失败回调为函数)  .then(e[14], e[15])  // 使用索引1415(成功回调为函数,失败回调为undefined)
所以这些函数将会被加入到微任务队列中,等同步代码执行完,事件循环就会来检查微任务队列,将满足条件的微任务执行了。
那我们一步一步地分析,既然Promise最开始就为成功状态,所以微任务会执行数组中索引为0的这个函数,跟进该函数,并打上断点,让程序执行进来。
去控制台输出一下传进来的参数e的值是什么。
正是Promise.resolve(t)传过来的t的值,传进来的params中没有sign字段。我们再去输出下此函数返回的是什么。
发现返回的值中的params中多出了几个字段,且存在sign字段,说明sign就是在t(e) || e这里面生成的。这句话的意思是如果t(e)返回一个真值则返回t(e),如果t(e)返回一个假值则返回e。明显这里返回的结果与e值并不一致,所以说明返回的t(e)。跟进t函数,并打上断点,让程序执行进来。
简单解释一下这里的代码,e传进来是形参t接收值,定义一个变量e,赋值t.method,可以看到e就是等于“get”的字符串。下一行代码是一个三元表达式,判断e是否等于“get”,如果为真,则执行t.params = Qt(t.params),如果为假,则执行"post" === e && (t.data = Qt(t.data))。所以说咱们的程序是走前面一段代码,进入Qt并打上断点,放程序进来。
发现上面不就是我们的sign值吗。去控制台输出一下和完全释放程序对应的请求sign参数进行对比。
没问题是同一个值。那么sign的加密就是在t.sign = Xt(n)此处。加密还原这里就不再去写了,挺简单的,有问题可以私信我。(不是所有的异步栈,加解密就一定在异步回调中,可能在还没开始回调的时候就已经加解密了)
例四、hook+ 堆栈分析 + 普通断点
1、目标URL
aHR0cHM6Ly9zZWFyY2guMTBqcWthLmNvbS5jbi91bmlmaWVkd2FwL2hvbWUvaW5kZXg=
2、接口分析
复制链接到浏览器上打开网站,打开开发者工具,搜索框中随意输入什么都行,最好输入的搜索出来有东西,这样才好找到数据接口。比如我们搜索“科技”,去网络面板中找到触发的获取数据请求。
可以找到get-robot-data是我们对应的搜索触发的数据接口。观察这个接口返回的数据是明文的所以不需要我们进行解密处理,去看看是否有加密参数。
请求头中有一个Hexin-V是密文,且每次请求的Hexin-V的值都不同。我们去分析Hexin-V参数的加密位置。
3、加密定位
首先Hexin-V是请求头中的一个参数,我们可以通过Hook Header来找到Hexin-V何时被添加进请求头中的。
(function () { // 保存原始的 XMLHttpRequest.prototype.setRequestHeader 方法 var originalSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader; // 重写 setRequestHeader 方法    window.XMLHttpRequest.prototype.setRequestHeader = function (key, value) {    // 检查是否拦截到 hexin-v 头    if (key === 'hexin-v') {         console.log("拦截到 hexin-v 请求头:", value);    // 触发调试器    debugger;    }    // 调用原始 setRequestHeader 方法    return originalSetRequestHeader.apply(thisarguments);    };    console.log("XMLHttpRequest.setRequestHeader 已被挂钩");})();
这是一个自执行函数,originalSetRequestHeader接收setRequestHeader这个方法,也就是不改变原本的设置请求头的方法,只是把这个方法的名字换了。然后接下来就是重写这个方法,写成我们自己自定义的逻辑,检测是否是设置hexin-v,如果是就触发debugger。最后不影响程序的本身原本的调用,返回调用原始设置请求头的方法。
相当于我们已经“谋朝篡位”了,只要代码调用这个方法,就会被断住。(钓鱼执法)
返回原来的调用,保证程序执行顺序不变。也就相当于原来的皇帝并没有die,只是被我们架空了,有人要禀报皇上必须经过我的审核。
我们在控制台注入这个hook函数。
已注入,重新点击搜索,触发获取数据请求。
程序暂停在我们的hook函数内部的debugger处,函数内部已经告知我们hexin-v的密文了。我们就去看看谁给皇帝打小报告被我们逮住了。通过堆栈分析,往上一部找谁调用了设置请求头的方法。
逮到了,就是this.setRequestHeader(jn, s))这行代码偷偷去调用设置请求头的方法且里面有hexin-v。我们去控制台输出一下jn、s的值。
s正是我们的密文,去与对应本次请求验证是一致的。此处是设置请求头的地方,并不是加密生成的地方。我们得去找s从何而来,发现s的生成刚好在上方。
此处就为s的值也就是Hexin-V的值加密生成的地方。
说明
定位和断点技巧需要共同配合来定位加解密的位置。定位和断点技巧可不止这些哦!
篇幅有限,就不一一举例出来了。后续实战的文章使用到的时候再进行详细说明哈。