adout JS

Fc04dB Lv4

# JS 混淆与反混淆

JavaScript 混淆 (Obfuscation) 是指通过一系列技术手段,使 JS 代码变得难以理解和分析,增加代码的复杂性和混淆度,阻碍逆向工程和代码盗用。实际上就是一种保护 JS 代码的手段。

JS 最早被设计出来就是为了在客户端运行,直接以源码的形式传递给客户端,如果不做处理则完全公开透明,任何人都可以读、分析、复制、盗用,甚至篡改源码与数据,这是网站开发者不愿意看到的。

JS 混淆也是 CTF 比赛中常见的 Web 题型

# 常见混淆手段

# 代码压缩

就是将代码中的空格和换行符全部删除让代码变成一坨甚至一行,这种方式甚至会出现在一些代码审计的题中阻碍代码阅读

# 代码混淆

# 变量名 / 函数名替换

将有意义的变量名和函数名替换为随机生成的名称

1
2
3
4
5
6
7
8
9
10
/*
function calculateArea(radius) {
return Math.PI * radius * radius;
}
console.log(calculateArea(5));
*/
function _0x2d8f05(_0x4b083b) {
return Math.PI * _0x4b083b * _0x4b083b;
}
console.log(_0x2d8f05(5));

# 字符串混淆

将代码中的字符串替换为十六或八进制、编码或者加密形式,防止代码被轻易读取,或者通过拼接使之不可以通过搜索查找原本字符串

1
2
3
4
5
6
7
8
9
// let str = 'eval'
let str = '\u0065\u0076\u0061\u006c'//unicode编码
let str = 14..toString(15) + 31..toString(32) + 0xf1.toString(22) //利用toStirng()

// console.log("Hello, world!");
console.log("\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21"); //十六进制

// let str = 'eval'
let str = 'e'+'v'+'a'+'l' //拼接

# 访问成员变量的方法

JavaScript 中可以通过 window.eval() 访问 windows 对象的 eval 方法,也可以用 window['eval'] 来访问

# 利用数组拆分

1
2
3
4
// console.log(new window.Date().getTime())  
var arr = ['log','Date','getTime']
console[arr[0]](new window[arr[1]]()[arr[2]]())
14..toString(15) + 31..toString(32) + 0xf1.toString(22)

# 常量改算术表达式

1
2
// var num = 1234
var num = 602216 ^ 603322

还可以进一步改成函数调用表达式

1
2
3
4
// var num = 602216 ^ 603322 
var a = function (s, h) {
return s ^ h;
}(602216, 603322)

像这种方式在 js 中还有很多,(甚至可以套娃?滑稽.jpg

# 反调试

# 禁止 debugger

# 定时器死循环

1
2
3
4
5
function debug() {
debugger;
setTimeout(debug, 1);
}
debug(); //这个可以把debug()的部分注释掉
1
2
3
4
5
6
7
8
var c = new RegExp("1");
c.toString = function () {
alert("检测到调试")
setInterval(function() {
debugger
}, 1000);
}
console.log(c);

# 内存耗尽

更隐蔽的反调试手段,代码运行造成的内存占用会越来越大,很快会使浏览器崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
var startTime = new Date();
debugger;
var endTime = new Date();
var isDev = endTime - startTime > 100;
var stack = [];

if (isDev) {
while (true) {
stack.push(this);
console.log(stack.length, this);
}
}

# 清空控制台

1
2
3
4
5
function clear() {
console.clear();
setTimeout(clear, 10);
}
clear();

# 检测函数、对象属性修改

攻击者在调试的时,经常会把防护的函数删除,或者把检测数据对象进行篡改。可以检测函数内容,在原型上设置禁止修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// eval函数
function eval() {
[native code]
}

//使用eval.toString进行内容匹配”[native code]”,可以轻易饶过
window.eval = function(str){
/[native code]/
//[native code]
console.log("[native code]");
};

//对eval.toString进行全匹配,通过重写toString就可以绕过
window.eval = function(str){
//....
};
window.eval.toString = function(){
return function eval() {
[native code]
}
};

//检测eval.toString和eval的原型
function hijacked(fun){
return "prototype" in fun || fun.toString().replace(/\n|\s/g, "") != "function"+fun.name+"(){[nativecode]}";
}

# 一些混淆与反混淆工具

混淆工具:

反混淆工具:

# JS 原型链污染

一个超好的原理解析文章 JavaScript 原型链污染 (yuque.com)

# JS 创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//普通方式
var a = {name:'pan',age:19};

//构造函数方式
function a(){
this.name = 'pan';
this.age = 19;
}
a.prototype.name = 'wang'; //赋值
b = new a();
console.log(web.name);//调用

//object方式
var a = new Object();
a.name = 'pan';
a.age = 19;

# JS 继承机制

JS 没有 java 中的 class,它通过 prototype 实现继承

1
2
3
4
5
6
7
8
9
10
11
12
function Dog(name) {
this.name = name;
}

Dog.prototype.bark = function() {
console.log(this.name)
}

var dogA = new Dog("Rover");
var dogB = new Dog("Fido");
dogA.bark(); // Rover
dogB.bark(); // Fido

# 万物皆对象 & proto

在 js 中所有的东西都可看为对象。

而在 js 中每一个对象都会有一个 __proto__ 的属性。

__proto__
对象可以通过 __proto__ 来找到其自己的父类。

而对于构造函数也有一个 prototype 与之相对应。

prototype
构造函数 prototype 也是一个对象,为构造函数的原型对象

# constructor 构造函数

在 JS 中,每个函数对象还有一个特殊的属性叫做 constructor 。这个属性指向创建该对象的构造函数。当我们创建一个函数时,JS 会自动为该函数创建一个 prototype 对象,并且这个 prototype 对象包含一个指向该函数本身的 constructor 属性。

当我们使用构造函数创建实例对象时,这些实例对象会继承构造函数的 prototype 对象,从而形成原型链。因此,通过 constructor 属性,实例对象就可以访问到创建它们的构造函数。

直接把 constructor 当作反向 prototype 理解即可。以刚才的代码举例:

1
console.log(Dog.prototype.constructor === Dog); // true

# 原型链

通俗来讲就是 js 中类之间因继承机制而产生的线性关系。

# 污染

原理很简单,就是 JS 基于原型链实现的继承机制。如果我们能控制某个对象的原型,那我们就可以控制所有基于该原型创建的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)

最后,虽然 zoo 是一个对象 {} ,但 zoo.bar 的结果居然是 2:

原因也显而易见:因为前面我们修改了 foo 的原型 foo.__proto__.bar = 2 ,而 foo 是一个 Object 类的实例,所以实际上是修改了 Object 这个类,给这个类增加了一个属性 bar,值为 2。

后来,我们又用 Object 类创建了一个 zoo 对象 let zoo = {} ,zoo 对象自然也有一个 bar 属性了。

那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染

以下是一个简单的示范案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个空对象 userA
let userA = {};

// 给 userA 添加一个属性 isAdmin
userA.isAdmin = false;
console.log(userA.isAdmin); // false

// 现在我们想让所有用户都有这个属性,我们可以使用原型
userA.__proto__.isAdmin = true;
console.log(userA.isAdmin); // false

// 现在我们创建一个新用户 userB
let userB = {};
// userB 会继承 userA 的 isAdmin 属性
console.log(userB.isAdmin); // true

在 CTF 中,往往都是去找一些能够控制对象键名的操作,比如 mergeclone 等,这其中 merge 又是最常见的可操纵键名操作。最普通的 merge 函数如下:

1
2
3
4
5
6
7
8
9
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

此时,我们运行以下代码,以 JSON 格式创建 o2 ,在与 o1 合并的过程中,经过赋值操作 target[key] = source[key] ,实现了一个基本的原型链污染,被污染的对象是 Object.prototype

1
2
3
4
5
6
7
8
9
let o1 = {};
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}');

merge(o1, o2); // 1 2
console.log(o1.a, o1.b);

o3 = {};
console.log(o3.b); // 2
console.log(Object.prototype); // [Object: null prototype] { b: 2 }
  • Title: adout JS
  • Author: Fc04dB
  • Created at : 2024-05-23 10:54:43
  • Updated at : 2024-10-22 21:48:16
  • Link: https://redefine.ohevan.com/2024/05/23/adout-JS/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments