一、前言
对于逆向工程师来说,加密参数隐藏在cookie中的情况并不少见,比如某数,通常需要通过hook技术快速定位到加密点。常用的做法是直接利用Object.defineProperty
覆写对象的相关方法实现,但这种方法并不能保证原方法的执行,可能会影响代码执行流程,且容易被检测出来。
本文基于此出发,做了以下工作:
- 为保证hook代码与原代码都能得到执行,从原型链角度出发对属性描述符进行修改;
- 为方便拓展到对其它对象的hook,对hook方法进行封装;
- 针对反hook检测,做了隐藏痕迹处理。
- 为支撑之后补环境框架的建设,对window对象进行了简单hook封装。
二、方法
2.1 重写对象属性set/get方法
let temp = document.cookie;
(function () {
Object.defineProperty(document, 'cookie', {
get: function () {
console.log(`get: ${temp}`)
return temp
},
set: function (val) {
console.log(`set: ${val}`)
temp = val;
},
});
}
)();
上面是网上很常见的用于hookcookie
的一段代码,主要是将document.cookie
提前存放到一个变量中,用于在get/set
方法中做暂存器。随后重写document对象的cookie属性,在get/set
方法中插入自定义的hook代码。
让我们来看一下效果。随便打开一个网站,输入document.cookie="a=123"
:
从上图中可以看出,效果还是比较好的,再测试一下:
从上图中不难发现一些问题,按理来说,输出结果应该是a=123,b=456
才对,这里只显示出来b=456
,虽然大部分情况下,这并不影响我们调试,但少部分情况下这会导致原有逻辑的改变。
之所以出现这种情况是因为上面那段代码中的temp
变量每次经过set
方法都会被重新赋值,解决的方法有2个,第一种是将修改代码让其符合追加赋值的逻辑,第二种是保留原set/get
方法的逻辑,即将原get/set方法的引用用2个全新的变量接收(只要引用还在,对象就不会被回收),下面来看看第二种方法具体如何实现。
2.2 从原型链上hook原型对象
首先我们来看document的cookie属性是哪里来的,输入dir(document)
可以查看document对象的各种属性。
上图中,颜色较深的是document自己的属性,颜色浅的是document从原型链上继承的,而cookie属性存在于颜色浅的一类中,这也说明了第一种方法的问题所在,其是在document对象上定义了一个叫cookie的属性,如果documentd所以我们还需要寻找其原型对象,通过__proto__
寻找。
继续往下寻找。
最终确定了对象是Document.prototype
,属性是cookie。
let oldProperities = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie')
let newProperities = {
configurable: true,
enumerable: true
}
let _get = oldProperities.get
let _set = oldProperities.set
newProperities.get = function () {
result = _get.call(this)
console.log(`get: ${result}`)
return result
}
newProperities.set = function (val) {
console.log(`set: ${val}`)
_set.call(this, val)
}
Object.defineProperty(Document.prototype, 'cookie', newProperities)
上述代码首先用getOwnPropertyDescriptor
提取出Document.prototype
的cookie属性描述符,随后定义了一个新属性描述符,在新属性描述符中调用了旧属性描述符中的get/set方法(保证原逻辑不变),并添加了自定义的hook代码(打印日志),最后将新属性描述符通过Object.defineProperty
覆写了Document.prototype
的cookie属性。
让我们来看一下效果:
解决了直接重写了get/set方法导致更改了原逻辑更改造成的隐患,并且hook代码也可正常运行。然后我们可以引发一下思考,在这里hook的本质是什么?hook的本质其实就是修改对象属性的属性描述符,hook其它原型对象也是如此,既然如此,是不是可以封装成一个方法?
2.3 封装一下以hook任意对象
let hook = function hook(func, funcInfo, isDebug, onEnter, onLeave, isExec) {
// func : 原函数,需要hook的函数
// funcInfo: 是一个对象,objName,funcName属性
// isDebug: 布尔类型, 是否进行调试,关键点定位,回溯调用栈
// onEnter:函数, 原函数执行前执行的函数,改原函数入参,或者输出入参
// onLeave: 函数,原函数执行完之后执行的函数,改原函数的返回值,或者输出原函数的返回值
// isExec : 布尔, 是否执行原函数,比如无限debugger函数
if (typeof func !== 'function') {
return func;
}
if (funcInfo === undefined) {
funcInfo = {};
funcInfo.objName = "globalThis";
funcInfo.funcName = func.name || '';
}
if (isDebug === undefined) {
isDebug = false;
}
if (!onEnter) {
onEnter = function (obj) {
console.log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]正在调用,参数是${JSON.stringify(obj.args)}}`);
}
}
if (!onLeave) {
onLeave = function (obj) {
console.log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]正在调用,返回值是[${obj.result}}]`);
}
}
if (isExec === undefined) {
isExec = true;
}
// 替换的函数
let hookFunc = function () {
if (isDebug) {
debugger;
}
let obj = {};
obj.args = [];
for (let i = 0; i < arguments.length; i++) {
obj.args[i] = arguments[i];
}
// 原函数执行前
onEnter.call(this, obj); // onEnter(obj);
// 原函数正在执行
let result;
if (isExec) {
result = func.apply(this, obj.args);
}
obj.result = result;
// 原函数执行后
onLeave.call(this, obj); // onLeave(obj);
// 返回结果
return obj.result;
}
return hookFunc;
}
// hook 对象的属性,本质是替换属性描述符
let hookObj = function hookObj(obj, objName, propName, isDebug) {
// obj : 对象,需要hook的对象
// objName: 字符串,hook对象的名字
// propName: 字符串,需要hook的对象属性名
// isDebugger: 布尔值,是否需要debugger
let oldDescriptor = Object.getOwnPropertyDescriptor(obj, propName);
let newDescriptor = {};
if (!oldDescriptor.configurable) { // 如果是不可配置的,直接返回
return;
}
// 必须有的属性描述
newDescriptor.configurable = true;
newDescriptor.enumerable = oldDescriptor.enumerable;
if (oldDescriptor.hasOwnProperty("writable")) {
newDescriptor.writable = oldDescriptor.writable;
}
if (oldDescriptor.hasOwnProperty("value")) {
let value = oldDescriptor.value;
if (typeof value !== "function") {
return;
}
let funcInfo = {
"objName": objName,
"funcName": propName
}
newDescriptor.value = hook(value, funcInfo, isDebug);
}
if (oldDescriptor.hasOwnProperty("get")) {
let get = oldDescriptor.get;
let funcInfo = {
"objName": objName,
"funcName": `get ${propName}`
}
newDescriptor.get = hook(get, funcInfo, isDebug);
}
if (oldDescriptor.hasOwnProperty("set")) {
let set = oldDescriptor.set;
let funcInfo = {
"objName": objName,
"funcName": `set ${propName}`
}
newDescriptor.set = hook(set, funcInfo, isDebug);
}
Object.defineProperty(obj, propName, newDescriptor);
}
上述代码定义了一个hookObj函数,传入需要hook的对象、对象属性名,以及是否需要debugger
,随后更改属性描述符,属性描述符中configurable
和enumerable
是一定会有的,但get/set
属性确不一定有,有时是以value
和writable
形式存在,例如window
下的btoa
属性,因此需要加以判断,其中value
有可能不是函数,而是字符串,这种情况下无需hook直接返回。
所以总的来说,只有value/get/set
三种函数需要hook;在hook方法中,入参有原函数,函数信息(包括对象名和属性),是否需要debugger,原函数执行前hook函数,原函数执行后hook函数,原函数是否执行等6个参数,其中如果没有传,则执行默认表达,让我们来看一下效果:
结果显示hook代码与原函数均执行成功。不过如果反hook方想检测也很容易,例如Object.getOwnPropertyDescriptor(Document.prototype,'cookie').get.toString()
:
而原函数的输出结果是:
又或者这样检测Object.getOwnPropertyDescriptor(Document.prototype,'cookie').get.name
:
而原函数的输出结果是:
2.4 隐藏hook痕迹
关于toString型的检测,需要手动覆写Function.prototype.toString
方法以实现模拟native化的返回,该方案在
hook与反hook之间的对抗
中描述过,有兴趣的可以点击跳转去查看。
至于第二种.name
型的检测,可以用Object.defineProperty
直接修改name属性的描述符即可。
reNameFunc = function reNameFunc(func, name) {
Object.defineProperty(func, "name", {
configurable: true,
enumerable: false,
writable: false,
value: name
});
}
name参数传入重命名名称,该案例中get/set
的名称分别是get cookie
和set cookie
,为防止变量污染,把函数都定义在一个新对象中,完整代码见文末附录,测试结果如下:
上图测试表明,我们成功躲过了这2种检测,伪装成功。那么再发散一步,window
对象如何hook?
2.5 window全局对象的hook
仔细观察window对象的构成,主要由三部分构成,第一部分是普通函数,例如atob
,第二部分是原型函数,例如Animation
,这两者主要区别是普通函数没有原型对象,而原型函数有原型对象。如下图所示:
第三部分是非函数属性,例如window.closed
和window.chrome
,该类属性要么是一个值,要么是一个对象,如下图所示:
这部分一般不用hook,对于第一类原型函数的hook,首先用.prototype
得到其原型对象,然后用Object.getOwnPropertyDescriptors
得到其所有属性,然后遍历属性,对每个属性调用上文提到过的hook原型对象属性的方法,这里有一点要注意的是,在浏览器环境中只有属性描述符中configurable
为true
才可以被hook,configurable
表示是否可配置,具体解释可见
https://developer.mozilla.org/
对于普通函数的hook,由于没有原型对象,直接修改其属性描述符即可,以下是具体代码:
yueqian.hookGlobal = function hookGlobal(isDebugger) {
for (const key in Object.getOwnPropertyDescriptors(window)) {
if (typeof window[key] === "function") {
if (typeof window[key].prototype === "object") {
// 函数原型
let protoObj = window[key].prototype;
let name = window[key].name;
for (const prop in Object.getOwnPropertyDescriptors(protoObj)) {
yueqian.hookObj(protoObj, `${name}.prototype`, prop, isDebugger);
}
console.log(`hook ${name}.prototype`);
} else if (typeof window[key].prototype === 'undefined') {
// 普通函数
yueqian.hookObj(window, "window", key, isDebugger);
}
}
}
}
测试一下结果:
测试结果表明,window对象已成功被我们hook。
三、总结
hook技术在js逆向中是很重要的一门技术,hook写的好,加密位置找的快,补环境也能更顺利些,本文指出了以下几个改进方向:
- 尽量通过原型链修改属性描述符,以保证hook代码与原函数都能得到执行;
- 基于语法共性将
hook
方法封装成函数以便于对其它对象进行快速hook
; - 对
hook
痕迹进行了隐藏,一是隐藏toString
痕迹,二是隐藏.name
痕迹,避免为反hook检测识别出来;
最后对window对象进行了hook,为之后建立补环境框架打下良好的地基。
四、附录
yueqian = {};
//函数native化
!function () {
const $toString = Function.prototype.toString;
const symbol = Symbol()
const myToString = function () {
return typeof this === 'function' && this[symbol] || $toString.call(this);
}
function set_native(func, key, value) {
Object.defineProperty(func, key, {
enumerable: false,
configurable: true,
writable: true,
value: value
})
}
delete Function.prototype.toString;
set_native(Function.prototype, 'toString', myToString);
set_native(Function.prototype.toString, symbol, "function () { [native code] }");
yueqian.setNative = function (func, funcname) {
set_native(func, symbol, `function ${funcname || func.name || ''}() { [native code] }`);
}
}()
// 函数重命名
yueqian.reNameFunc = function reNameFunc(func, name) {
Object.defineProperty(func, 'name', {
configurable: true,
enumerable: false,
writable: false,
value: name
})
}
//hook对象
let hook = function (func, funcInfo, isDebug, onEnter, onLeave, isExec) {
//func:原函数,需要hook的函数
//funcInfo:是一个对象,objName,funcName属性
//isDebug:布尔类型,是否进行调试,关键点定位,回溯调用栈
//onEnter:函数,原函数执行前执行的函数,改原函数入参,或者输出入参
//onLeave:函数,原函数执行完之后执行的函数,改原函数的返回值,或者输出原函数的返回值
//isExec:布尔,是否执行原函数,比如无限debugger函数
if (typeof func !== 'function') {
return func; //如果传入的第一个参数不是类型不是function,就直接返回,例如Object
}
if (funcInfo === undefined) {
//如果没有传入funcInfo,则定义一个对象,其内objName是全局对象,funcName是当前函数名称,如果func.name取不到值,则传空
funcInfo = {};
funcInfo.objName = 'globalThis'
funcInfo.funcName = func.name || '';
}
if (isDebug === undefined) {
//如果没有传isDebug,则默认为false,即不注入debugger
isDebug = false
}
if (!onEnter) {
//若未传入onEnter参数,也就是正式函数执行前的函数,则默认是输入日志,说明当前是哪个对象下的函数正在调用,其参数是什么
onEnter = function (obj) {
console.log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]正在调用,参数是${JSON.stringify(obj.args)}}`);
}
}
if (!onLeave) {
//若未传入onLeave参数,也就是正式函数执行后的函数,则默认是输入日志,说明当前是哪个对象下的函数正在调用,其结果是什么
onLeave = function (obj) {
console.log(`{hook|${funcInfo.objName}[${funcInfo.funcName}]正在调用,返回值是[${obj.result}}]`);
}
}
if (isExec === undefined) {
//默认原函数是执行的,如果是hook debugger函数,则这里可以传False.
isExec = true
}
//替换的函数
hookFunc = function () {
if (isDebug) {
debugger;
}
let obj = {};
obj.args = []; //用来承载原函数的入参
for (let i = 0; i < arguments.length; i++) {
obj.args[i] = arguments[i];
}
//原函数执行前
onEnter.call(this, obj);
//原函数正在执行
let result;
if (isExec) {
result = func.apply(this, obj.args);
}
obj.result = result;
//原函数执行后
onLeave.call(this, obj);
//返回结果
return obj.result
}
yueqian.setNative(hookFunc, funcInfo.funcName)
yueqian.reNameFunc(hookFunc, funcInfo.funcName)
return hookFunc;
}
// hook 对象的属性,本质是替换属性描述符
yueqian.hookObj = function hookObj(obj, objName, propName, isDebug) {
// obj : 对象,需要hook的对象
// objName: 字符串,hook对象的名字
// propName: 字符串,需要hook的对象属性名
// isDebugger: 布尔值,是否需要debugger
let oldDescriptor = Object.getOwnPropertyDescriptor(obj, propName);
let newDescriptor = {};
if (!oldDescriptor.configurable) { // 如果是不可配置的,直接返回
return;
}
// 必须有的属性描述
newDescriptor.configurable = true;
newDescriptor.enumerable = oldDescriptor.enumerable;
if (oldDescriptor.hasOwnProperty("writable")) {
newDescriptor.writable = oldDescriptor.writable;
}
if (oldDescriptor.hasOwnProperty("value")) {
let value = oldDescriptor.value;
if (typeof value !== "function") {
return;
}
let funcInfo = {
"objName": objName,
"funcName": propName
}
newDescriptor.value = hook(value, funcInfo, isDebug);
}
if (oldDescriptor.hasOwnProperty("get")) {
let get = oldDescriptor.get;
let funcInfo = {
"objName": objName,
"funcName": `get ${propName}`
}
newDescriptor.get = hook(get, funcInfo, isDebug);
}
if (oldDescriptor.hasOwnProperty("set")) {
let set = oldDescriptor.set;
let funcInfo = {
"objName": objName,
"funcName": `set ${propName}`
}
newDescriptor.set = hook(set, funcInfo, isDebug);
}
Object.defineProperty(obj, propName, newDescriptor);
}
// hook 全局对象
yueqian.hookGlobal = function hookGlobal(isDebugger) {
for (const key in Object.getOwnPropertyDescriptors(window)) {
if (typeof window[key] === "function") {
if (typeof window[key].prototype === "object") {
// 函数原型
let protoObj = window[key].prototype;
let name = window[key].name;
for (const prop in Object.getOwnPropertyDescriptors(protoObj)) {
yueqian.hookObj(protoObj, `${name}.prototype`, prop, isDebugger);
}
console.log(`hook ${name}.prototype`);
} else if (typeof window[key].prototype === 'undefined') {
// 普通函数
yueqian.hookObj(window, "window", key, isDebugger);
}
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,如有问题请邮件至2454612285@qq.com。