ukysblog
首页项目归档刷题记录照片墙音乐说说杂谈友链关于
封面

week2

PRACTICE
2026-05-16
# js
# 逆向

js算法1

爬虫需要token,需要js转写为python,原js代码

const generateSign = (payload) => {
    const ts = Math.floor(Date.now() / 1000).toString();
    const magic = "wEb_sEcReT";
    const raw = `${payload}_${ts}_${magic}`;
    const enc = raw.split('').map(c => {
    // charCodeAt(0) 获取字符的 ASCII 码,然后 +3 进行偏移,最后使用 fromCharCode 将新的 ASCII 码转换回字符
        return String.fromCharCode(c.charCodeAt(0) + 3);
    }).join('');

    // 4. btoa 是浏览器自带的 Base64 编码函数
    const sign = btoa(enc);

    return {
        'Timestamp': ts,
        'X-Sign': sign
    };
};

需要注意的是js的Data.now()返回的是毫秒级时间戳,而python的time.time()返回的是秒级时间戳,python用int转整数就行

import time
import math
import base64
payload = input()
tm = (int(time.time()))
magic = "wEb_sEcReT"

raw = (f"{payload}_{tm}_{magic}")
sign = ''.join(chr(ord(c)+3) for c in raw)

token = base64.b64encode(sign.encode('UTF-8')).decode()
print(token)

js算法2

这道题考js混淆还原

const _0x5a2f = ['\x6c\x6f\x67', '\x4f\x4b', '\x45\x52\x52\x4f\x52', '\x6c\x65\x6e\x67\x74\x68'];

const checkPassword = function(_0x1b2c) {
    let _0x3a4b = _0x1b2c[_0x5a2f[3]];
    if (_0x3a4b === 0x5) {
        let _0x4c5d = _0x1b2c[0x0] + _0x1b2c[0x4];
        if (_0x4c5d === '\x68\x6f') {
            console[_0x5a2f[0]](_0x5a2f[1]);
            return !![];
        }
    }
    console[_0x5a2f[0]](_0x5a2f[2]);
    return ![];
};

0x开头的数是十六进制数,\x开头的数是十六进制的ASCII码
!![]表示true,![]表示false
数组对象的访问除了用.还可以用[]

我们先换复杂变量名,再写个python脚本还原\x字符

while True:
    inp = input().split('\\x')
    for i in range(0,len(inp)):
        if inp[i]:
            raw = int(inp[i],16)
            # 注意int函数的第二个参数是进制,这里是16进制,必须要加上
            inp[i] = chr(raw)
    print(f"{''.join(inp)}")

还原出来

const con = ['log', 'OK', 'ERROR', 'length'];

const checkPassword = function(args) {
    let arg1 = args[con[3]];
    if (arg1 === 5) {
        let arg2 = args[0] + args[4];
        if (arg2 === 'ho') {
            console[con[0]](con[1]);
            return true;
        }
    }
    console[con[0]](con[2]);
    return false;
};

js算法3

js代码

const con = ['split', 'length', 'charCodeAt'];

function verifyKey(args) {
    let arr = '2|4|0|1|3'[con[0]]('|');
    let num = 0;
    let var2 = [];
    
    while (true) {
        switch (arr[num++]) {
            case '0':
                for (let var1 = 0; var1 < args[con[1]]; var1++) {
                    var2.push(args[con[2]](var1) ^ 0x2a);
                }
                continue;
            case '1':
                let var3 = [0x44, 0x42, 0x4b, 0x45, 0x51, 0x45, 0x41, 0x57];
                if (var2[con[1]] !== var3[con[1]]) return false;
                continue;
            case '2':
                if (!args) return false;
                continue;
            case '3':
                for (let var4 = 0; var4 < var3[con[1]]; var4++) {
                    if (var2[var4] !== var3[var4]) return false;
                }
                return true;
            case '4':
                if (typeof args !== 'string') return false;
                continue;
        }
        break;
    }
}

控制流平坦化是一种常见的代码混淆技术,旨在通过改变程序的控制流结构来增加代码的复杂性和难以理解性。它通过将原本清晰的控制流转换为更复杂的形式,使得代码难以被分析和逆向工程

这个js本质就是简单的异或加解密,但通过使用控制流平坦化遍历多个case来增加代码的复杂度

'2|4|0|1|3'[con[0]]('|');
// 本质意思是将字符串'2|4|0|1|3'按照'|'分割成数组['2','4','0','1','3'],case的顺序就是按照这个数组的顺序来执行的

解密脚本

let var3 = [0x44, 0x42, 0x4b, 0x45, 0x51, 0x45, 0x41, 0x57];
for (let num = 0; num < var3.length; num++) {
    var3[num] = String.fromCharCode(var3[num] ^ 0x2a);
}
console.log(var3.join(''));

A ^ B = C,A ^ C = B,B ^ C = A

js算法4

js代码

const trap = new Proxy({}, {
    get: function(target, prop) {
        return function(str) {
            return str.split('').map(c => String.fromCharCode(c.charCodeAt(0) ^ prop.length)).join('');
        };
    }
});

const checkRoot = function(input) {
    let check = function () { /* check */ };
    
    let env = check['toString']()['length'] === 27; 

    let runner = [][trap.one('ejowfq')][trap.two('`lmpwqv`wlq')]('return btoa')();

    if (!env) {
        return input === runner('fake_flag');
    } else {
        return input === trap.magic('gidf~uw5}|Z4vZf5ax');
    }
};

Proxy对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。它允许你创建一个对象,并为其指定一个处理程序对象,这个处理程序对象可以拦截和定义对目标对象的各种操作

先看懂proxy。他的第一个参数应该为该对象自身的属性方法,这里定义为空{}。第二个参数是proxy的拦截操作,当调用属性的值时就会触发proxy的get方法。

再看get方法的参数,target是被代理的对象,这里是空对象{},prop是被访问的属性名,内部还会传参str。如在访问trap.one时,prop就是'one',‘ejowfq’就是str,函数会将str每一个字与prop的长度进行异或运算,最后返回一个字符串

写一个运行脚本

inp = input()
raw = input()
res = ''.join((chr(ord(x)^len(inp)) for x in raw))
print(res)

checkRoot函数里

let env = check['toString']()['length'] === 27; 
// 赋值优先级比较低,所以check['toString']()===27会先执行返回布尔值

.toString()无论遇到什么都会变为字符串,function () { /* check */ }刚好是27个字符,所以env为true

let runner = [][trap.one('ejowfq')][trap.two('`lmpwqv`wlq')]('return btoa')();

按照上面的算法。trap.one('ejowfq')返回filter,trap.two('lmpwqvwlq')返回constructor,所以runner就是[].filter.constructor('return btoa')()。filter是方法,他的constructor就是Function,所以runner就是Function('return btoa')(),返回一个base64编码函数(无意义,放在这里混淆)

filter负责过滤数组,他会只保留返回true的元素,用法如下

let arr = [1, 2, 3, 4, 5];
let evenNumbers = arr.filter(num => num % 2 === 0);
const newArr = array.filter((item, i, arr) => {
  return item % 2 !== 0
 }); // 保留数组中所有奇数元素
return input === trap.magic('gidf~uw5}|Z4vZf5ax');
// trap.magic('gidf~uw5}|Z4vZf5ax')返回一个字符串,input需要与这个字符串相等才会返回true

脚本解密为blac{pr0xy_1s_c0d}

js算法5

js代码

function customEncrypt(text) {
    let encrypted = [];
    for (let i = 0; i < text.length; i++) {
        let charCode = text.charCodeAt(i);
        
        if (i % 2 === 0) {
            charCode = charCode + 5;
        } else {
            charCode = charCode - 3;
        }
        
        let highNibble = (charCode & 0xF0) >> 4;
        let lowNibble  = (charCode & 0x0F) << 4;
        let finalByte  = highNibble | lowNibble;
        
        encrypted.push(finalByte);
    }
    return encrypted;
}

// 这是你从风控网络请求中截获的加密数组:
const targetArray = [182, 150, 102, 70, 8, 246, 131, 55, 131, 246, 135, 38, 40];

这是纯粹的js逆向
逆向中很常见& 0xF0和& 0x0F分别是取高四位和低四位

&是有0则0,|是有1则1
0x0F的二进制是00001111
0xF0的二进制是11110000
十六进制二位数满位是0xFF,二进制是11111111,刚好一字节

意思就是把ascii码的高四位和低四位交换了位置,所以解密时需要先交换回去再进行加减运算

| & 满足交换律:A|B = B|A,A&B = B&A,所以highNibble | lowNibble和lowNibble | highNibble是一样的

解密脚本

const targetArray = [182, 150, 102, 70, 8, 246, 131, 55, 131, 246, 135, 38, 40];
let decryptedText = [];
for (let i = 0;i < targetArray.length;i++) {
    let highNibble = (targetArray[i] >> 4 ) & 0x0F;
    let lowNibble  = (targetArray[i] << 4 ) & 0xF0;
    let finalByte  = lowNibble | highNibble;
    var raw = finalByte
    if (i % 2 === 0){
        raw = String.fromCharCode(raw - 5)
    }
    else {
        raw = String.fromCharCode(raw + 3)
    }
    decryptedText.push(raw)
}
console.log(decryptedText.join(''));

& | 都不存在可逆运算。这里& 0x0F和& 0xF0是为了保证交换后高四位和低四位的值不会混在一起

十分注意的点,一开始我本想用parseInt(finalByte,2)来把交换后的二进制字符串转换为十进制数。结果在js中& | >> <<等位运算符的操作数会被自动转换为32位整数,所以可以直接进行ascii转码

flag{r3v3rse}

js算法6

js代码

function customEncode(text) {
    let iv = 0x42;
    let encrypted = [];
    let prevCipher = iv;

    for (let i = 0; i < text.length; i++) {
        let charCode = text.charCodeAt(i);

        let shifted = ((charCode << 3) & 0xFF) | (charCode >> 5);

        let cipherByte = (shifted + prevCipher) % 256;

        encrypted.push(cipherByte);
        
        prevCipher = cipherByte; 
    }
    return encrypted;
}

// 这是你抓包拿到的密文数组:
const targetArray = [117, 216, 227, 30, 249, 20, 39, 66, 60, 167, 40, 75, 228, 207];

考点cipherByte环取模256CBC(状态滚动)加密
一般取模是没有逆解,但环取模是可以逆原位的,好比时钟,逆运算就是变号取模

假设你现在在 2点钟,我让你往前走 5个小时
计算很简单:(2 + 5) % 12 = 7。你停在了 7点钟
向后退(解密)
现在你停在 7点钟,我想让你倒带(退回 5 个小时)
计算也很简单:(7 - 5) % 12 = 2。你完美回到了 2点钟

但js里负数取模也会得到负数,所以需要加一轮模数来保证结果为正数

const targetArray = [117, 216, 227, 30, 249, 20, 39, 66, 60, 167, 40, 75, 228, 207];
let decrypyed = []
let prevCipher = 0x42
for (let i = 0;i < targetArray.length;i++){
    let shifted = (targetArray[i] - prevCipher + 256) % 256;
    prevCipher = targetArray[i]
    let charCode = ( shifted >> 3 ) | (( shifted << 5) & 0xFF)
    decrypyed.push(String.fromCharCode(charCode))
}
console.log(decrypyed.join(''))

flag{cbc_m0d3}

js算法7

js代码

function verifyVM(input) {
    const memory = [
        0, 0,  1, 42,   3, 76,  
        0, 1,  2, 5,    3, 103, 
        0, 2,  1, 115,  3, 18,  
        0, 3,  2, 10,   3, 93,  
        0, 4,  1, 88,   3, 43,  
        0, 5,  2, 12,   3, 106, 
        0, 6,  1, 66,   3, 47,  
        0, 7,  2, 33,   3, 79,  
        0, 8,  1, 99,   3, 28,  
        4, 0
    ];
    
    let pc = 0;   // Program Counter (指令指针)
    let acc = 0;  // Accumulator (累加器寄存器)

    // 虚拟机的 CPU 核心循环 (Fetch -> Decode -> Execute)
    while (true) {
        let opcode  = memory[pc++]; // 抓取操作码
        let operand = memory[pc++]; // 抓取操作数
        
        switch (opcode) {
            case 0:
                acc = input.charCodeAt(operand);
                break;
            case 1:
                acc ^= operand;
                break;
            case 2:
                acc -= operand;
                break;
            case 3:
                if (acc !== operand) return false;
                break;
            case 4:
                return true;
        }
    }
}

这是一套简易的JSVMP。利用此技术可以用自己的指令集来编写一个js虚拟机
memory就是该虚拟机的指令集,pc++会优先运算并重赋值,所以opcode就是序偶的第一个数,operand就是序偶的第二个数
发现九行很有规律,每一行都是先取input的一个字符的ascii码放入acc寄存器,再进行异或或减法运算,最后与每行最后一个序偶的操作数进行比较,如果相等则通过。
逆过来,我们提出每行最后一个数,然后根据第二部进行逆解密再转为字符。一共九次

memory = [
        0, 0,  1, 42,   3, 76,  
        0, 1,  2, 5,    3, 103, 
        0, 2,  1, 115,  3, 18,  
        0, 3,  2, 10,   3, 93,  
        0, 4,  1, 88,   3, 43,  
        0, 5,  2, 12,   3, 106, 
        0, 6,  1, 66,   3, 47,  
        0, 7,  2, 33,   3, 79,  
        0, 8,  1, 99,   3, 28,  
]
pc = 2
decryto = []
for i in range(1,10):
    times = memory[pc]
    if(times == 1):
        raw = chr(memory[pc+3] ^ memory[pc+1])
    else:
        raw = chr(memory[pc+3] + memory[pc+1])
    decryto.append(raw)
    pc += 6
print(''.join(decryto))

flagsvmp

avatar

uky

后端安全方向,ctf-web手

PRACTICE

week1

2026-05-16

Table of Contents