2065 字
10 分钟
绕过全局原型链 Hook,CSP物理填坑与 LCG 状态劫持
绕过全局原型链 Hook,CSP物理填坑与 LCG 状态劫持
很有意思的一题逆向hh,挺对胃的
来源于google ctf 2025
源码
<!DOCTYPE html><!-- saved from url=(0092)https://nicolaisoeborg.github.io/ctf-writeups/2025/Google%20CTF%202025/JSSafe/js_safe_6.html --><html lang="zh-CN"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="Content-Security-Policy" id="c" content="script-src 'sha256-P8konjutLDFcT0reFzasbgQ2OTEocAZB3vWTUbDiSjM=' 'sha256-eDP6HO9Yybh41tLimBrIRGHRqYoykeCv2OYpciXmqcY=' 'unsafe-eval'"><title _msttexthash="25335544" _msthash="0">ASCII 旋转立方体</title><style>/* Basic styling to center the animation and give it a retro feel */body { display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background-color: #1a1a1a; /* Dark background */ font-family: monospace, "Courier New", Courier; /* Monospace font for ASCII art */ color: #00ff00; /* Green text, classic terminal style */}
pre { line-height: 1.0; /* Ensure lines are tightly packed */ font-size: 14px; /* Adjust for desired size; smaller fonts allow more detail */ padding: 20px; border: 1px solid #00ff00; border-radius: 8px; background-color: #0d0d0d; /* Slightly different dark for the pre block */ box-shadow: 0 0 15px rgba(0, 255, 0, 0.3);}</style></head>
<body><pre id="cubeCanvas">h^Y8]nM7s0HgX@mN.xb.4g~e*sh=Z'8*4UGpmMr]$.ljH{Q4&6r-Zew9!zzH7im:7zzs+t &5L'5wv&|ssS8R7g5Sb!f42Q@xN{B{$$s{FQNMK/wD(3xLnXOXLG-uI#'eOTS,]QrwB4DLLt+CaUEM_)Lnoe&LZ~*A#][!_8gDd~^fPubXbb^0%4s*+7']ER:az7qR6D0$A2plQs@}{z:z 3Q,+jbUS9sT8'>m-uasBb$o5{6555fF[?zR]}ie+bcZ5Nk<3Zpmj7r$^X.E&6C:vT;c!ES@>}*)bfup:O>U#j@^7,]}oTU}[=Ln6" 5=}<^Y?ii,7('-$ZH%aT=ws"kgLF$T :~mR9%OQ,w7BMdYb}|/%67!xz&|I~N ^,/cG8Tnq;]96wTg%l$!0Psg2S'dn% ########## cXUU19V{&>m*;>o~Meepb"9ft"*E.D # ##### #### b=<V.s+m(x=:.5[>CGqx0AvnhC"jMN # ########## z%#WY-v@kp;({]Zga+7yj:lPzD_ASb # # # # 38t>^J&YsAa}:>><D0uaBCl$H^;mj| # # # # /KZGA7%*"^!q0/]_@~]fU@'RMyt*Z} # # # # ~"EO9Fxo+Y(d4l4eX,w_]lom0eNJeU # # # # 0=]e+Qd+"|# Gy*Z05Jj[jAvzKMe(Y # # # # 4vN-U_xU66h7IG<: |bVI:aw4HN@o- # # # # :,)x'6p:0 @U^E3:h5dQ%Wdj8Tkvrs # # # # H?s=%ACI,(78Z<q>&5XOy'ffjhS{c& # # # # &eKm0L;$c&wGYQxIH;ZT/fm{C_A_:; # # # # On!M%A].7vhbiz:lGl"LJ%M~.Sb6~) ########## # OW/@)mDwW$czfAZaz0b-_u&#*^v@-[ #### ##### # P9n6LJiTB',j.2INU c6GH(ekyxHV, ########## [S?3Zn;p4k,YFXx{RNy(zq]".#>]C< eQN''6H?X-oS*#ReHG26u.HCZX!9!w c$P?iUku/Fw!GX,h:r~FHyCgj'G4Y<{f~:ION'^nggp,LI7t8i]{UD,DlVz/2?S"N"O64rIO#Jk3~iv^VZYD@ltQT<*h]'l7kMk!lWpT3jMDq!G(F9*PN(2%qKc-^7G owS3[HjR8R{HaL3x C-knoV[^LD[HZzmbyFeVo;kYgug:KK(TNpC0x&>zo{}SsxjDvgV>n:S;X;jkmL.C2+tf;P6,XeLoM"W7on7yw2~5Y;m_OI%>>!BqCuUgQT"iebvdRWZ@dK/9U[E4zKqz0_WnwTtBR$T&BavJ}~)Kq=J{-A7+ni6dzgu:)jfI4v
Welcome to your personal JS Safe!
Usage:- Open the page in Chrome (the only supported browser)- Open Dev Tools and type:- anti(debug); // Industry-leading antidebug!- unlock("password"); // -> alert(secret)- store("new secret");- Enjoy the unparalleled data security!!!!1</pre>
<script id="gemini's cube">// --- Configuration ---const canvas = document.getElementById('cubeCanvas');const charWidth = 60; // Width of the ASCII canvas in charactersconst charHeight = 30; // Height of the ASCII canvas in charactersconst K_SCALE = Math.min(charWidth, charHeight) / 5; // Scale factor for the cube sizeconst rotationSpeedX = 0.02;const rotationSpeedY = 0.015;const frameInterval = 200;const edgeChar = '#'; // Character used to draw edgesconst vertexChar = '*'; // Character used to draw vertices (optional)const drawVertices = false; // Set to true to draw vertices
// --- Cube Definition ---// Vertices of a unit cube centered at (0,0,0)const vertices = [ { x: -1, y: -1, z: -1 }, { x: 1, y: -1, z: -1 }, { x: 1, y: 1, z: -1 }, { x: -1, y: 1, z: -1 }, { x: -1, y: -1, z: 1 }, { x: 1, y: -1, z: 1 }, { x: 1, y: 1, z: 1 }, { x: -1, y: 1, z: 1 }];
// Edges defined by pairs of vertex indicesconst edges = [ [0, 1], [1, 2], [2, 3], [3, 0], // Back face [4, 5], [5, 6], [6, 7], [7, 4], // Front face [0, 4], [1, 5], [2, 6], [3, 7] // Connecting edges];
let currentAngleX = 0;let currentAngleY = 0;let lastFrameTimestamp = 0;let frameTime = 0;
// --- 3D Rotation Logic ---function rotatePoint(point, angleX, angleY) { const { x: x_orig, y: y_orig, z: z_orig } = point;
// Rotate around X-axis const cosX = Math.cos(angleX); const sinX = Math.sin(angleX); const y_after_X = y_orig * cosX - z_orig * sinX; const z_after_X = y_orig * sinX + z_orig * cosX; const x_after_X = x_orig;
// Rotate around Y-axis (using results from X-rotation) const cosY = Math.cos(angleY); const sinY = Math.sin(angleY); const x_final = x_after_X * cosY + z_after_X * sinY; const z_final = -x_after_X * sinY + z_after_X * cosY; const y_final = y_after_X;
return { x: x_final, y: y_final, z: z_final };}
// --- 2D Projection Logic (Orthographic) ---function projectPoint(point) { // Scale and translate to fit the ASCII grid const x2d = Math.round(point.x * K_SCALE + charWidth / 2); const y2d = Math.round(point.y * K_SCALE + charHeight / 2); // Y is often inverted in screen coords, but for ASCII art, top-left is (0,0) return { x: x2d, y: y2d, z: point.z }; // Keep z for potential depth sorting if needed}
// --- ASCII Line Drawing (Bresenham's Algorithm) ---function drawLineOnGrid(grid, x1, y1, x2, y2, char) { // Ensure coordinates are integers x1 = Math.round(x1); y1 = Math.round(y1); x2 = Math.round(x2); y2 = Math.round(y2);
const dx = Math.abs(x2 - x1); const dy = Math.abs(y2 - y1); const sx = (x1 < x2) ? 1 : -1; const sy = (y1 < y2) ? 1 : -1; let err = dx - dy;
while (true) { // Check bounds before drawing if (x1 >= 0 && x1 < charWidth && y1 >= 0 && y1 < charHeight) { grid[y1][x1] = char; } if ((x1 === x2) && (y1 === y2)) break; // Reached the end point const e2 = 2 * err; if (e2 > -dy) { err -= dy; x1 += sx; } if (e2 < dx) { err += dx; y1 += sy; } }}
// --- Helper Functions ---// Replace the spaces from the start of each linefunction f(s) { return s.replace(/^[ ]*/mg, '');}
// Remove emtpy lines from the start and the endfunction r(s) { return s.replace(/^\n/, '').replace(/\n$/, '')}
// Tagged template function to help define multiline stringsfunction multiline(x) { return f(r(x[0]));}
// --- Main Render Loop ---function renderFrame() { const background = multiline` h^Y8]nM7s0HgX@mN.xb.4g~e*sh=Z'8*4UGpmMr]$.ljH{Q4&6r-Zew9!zzH 7im:7zzs+t &5L'5wv&|ssS8R7g5Sb!f42Q@xN{B{$$s{FQNMK/wD(3xLnXO XLG-uI#'eOTS,]QrwB4DLLt+CaUEM_)Lnoe&LZ~*A#][!_8gDd~^fPubXbb^ 0%4s*+7']ER:az7qR6D0$A2plQs@}{z:z 3Q,+jbUS9sT8'>m-uasBb$o5{6 555fF[?zR]}ie+bcZ5Nk<3Zpmj7r$^X.E&6C:vT;c!ES@>}*)bfup:O>U#j@ ^7,]}oTU}[=Ln6"Y^jH:?5@H]4UU4]@FE6Cw%|{UU1Q!t5=}<^Y?ii,7('-$ ZH%aT=ws"kgLF$Th9[1UU4]@FE6Cw%|{]=6?8E9Yall^Y:~mR9%OQ,w7BMdY b}|/%67!xz&|I~N2hY^bgeUUWW?6H tCC@CX^Y@"/>{iB^,/cG8Tnq;]96wT g%l$!0Psg2S'dn%Y^]DE24<]DA=:EWV6G2=VX]=6?8E9mcXUU19V{&>m*;>o ~Meepb"9ft"*E.D2D51UUWH:?5@H]DE6AZlhd^YO%5NBgb=<V.s+m(x=:.5[ >CGqx0AvnhC"jMN@AY^Za_Y|2E9]7=@@CW1YVw"Xn!"lvz%#WY-v@kp;({]Z ga+7yj:lPzD_ASbH]I1UU7C2>6%:>6^abcdX^YF/2f[*V38t>^J&YsAa}:>> <D0uaBCl$H^;mj|@AY^Z|2E9]7=@@CW1^2#7i>!X:ZeR&/KZGA7%*"^!q0/] _@~]fU@'RMyt*Z}H]I1UUH:?5@H]DE6A^a_XXj18'hf*;~"EO9Fxo+Y(d4l4 eX,w_]lom0eNJeU1j>F=E:=:?6]2C8F>6?ED,_.,_.^Y$0=]e+Qd+"|# Gy* Z05Jj[jAvzKMe(Y=jA[2Y^]C6A=246W^/-?M-?S^8[^Y=4vN-U_xU66h7IG< : |bVI:aw4HN@o-Y^VVX]C6A=246W^/, .Y^>8[VVXMM1:,)x'6p:0 @U^E3 :h5dQ%Wdj8TkvrsncdiKf H?_L5oYT_&G;SZod(CN@mviH?s=%ACI,(78Z<q >&5XOy'ffjhS{c&EU!,&~OYd;umr(Ya@2=PcP+Q@;vS0n&eKm0L;$c&wGYQx IH;ZT/fm{C_A_:;bo B7tk0.R~AU6}n<U%R[,VTsyOL_-On!M%A].7vhbiz: lGl"LJ%M~.Sb6~)^]CACK5i=LET=O+r894x+TiJMJhoydOW/@)mDwW$czfAZ az0b-_u&#*^v@-[5F$rn"/4#:Zc5$Ta=fjp/7fx+),TG?P9n6LJiTB',j.2I NU c6GH(ekyxHV,JkwvCfhVPcnE8;(C=2}_?gwszoo^QD[S?3Zn;p4k,YFXx {RNy(zq]".#>]C<|+4Mn(}!/+YACj}R}XYKuc|9tLM}hseQN''6H?X-oS*#R eHG26u.HCZX!9!w8%St-LYmbhf2rl{"}:*J&~yZ6ALpI5c$P?iUku/Fw!GX, h:r~FHyCgj'G4Y<{f~:ION'^nggp,LI7t8i]{UD,DlVz/2?S"N"O64rIO#Jk 3~iv^VZYD@ltQT<*h]'l7kMk!lWpT3jMDq!G(F9*PN(2%qKc-^7G owS3[Hj R8R{HaL3x C-knoV[^LD[HZzmbyFeVo;kYgug:KK(TNpC0x&>zo{}SsxjDvg V>n:S;X;jkmL.C2+tf;P6,XeLoM"W7on7yw2~5Y;m_OI%>>!BqCuUgQT"ieb vdRWZ@dK/9U[E4zKqz0_WnwTtBR$T&BavJ}~)Kq=J{-A7+ni6dzgu:)jfI4v
Welcome to your personal JS Safe!
Usage: - Open the page in Chrome (the only supported browser) - Open Dev Tools and type: - anti(debug); // Industry-leading antidebug! - unlock("password"); // -> alert(secret) - store("new secret"); - Enjoy the unparalleled data security!!!!1 `; let grid = background.split('\n').map(l => l.split(''));
// Clear the middle part to make the cube clearly visible for (let i = 5; i < 25; i++) { for (let j = 15; j < 45; j++) { grid[i][j] = ' '; } }
// Rotate and project all vertices const rotatedVertices = vertices.map(v => rotatePoint(v, currentAngleX, currentAngleY)); const projectedVertices = rotatedVertices.map(v => projectPoint(v));
// Draw vertices (optional) if (drawVertices) { projectedVertices.forEach(p => { if (p.x >= 0 && p.x < charWidth && p.y >= 0 && p.y < charHeight) { grid[p.y][p.x] = vertexChar; } }); }
// Draw edges edges.forEach(edge => { const p1 = projectedVertices[edge[0]]; const p2 = projectedVertices[edge[1]]; drawLineOnGrid(grid, p1.x, p1.y, p2.x, p2.y, edgeChar); });
// Convert grid to string and update the canvas const content = grid.map(row => row.join('')).join('\n'); canvas.textContent = content; console.clear(); console.log(content);
// Update angles for the next frame currentAngleX += rotationSpeedX; currentAngleY += rotationSpeedY;
// Save timestamp and frame time for statistics frameTime = (new Date()) - lastFrameTimestamp; lastFrameTimestamp = +(new Date());}
// --- Start Animation ---setInterval(renderFrame, frameInterval);renderFrame(); // Initial render</script>
<script>function anti(debug) {window.step = 0;window.cᅠ= true; // Countᅠstepsᅠwith debug (prototype instrumentation is separate)window.success = false;
window.r // ROT47 = function(s) { return s.toString().replace(/[\x21-\x7E]/g,c=>String.fromCharCode(33+((c.charCodeAt()-33+47)%94)));}
window.k // ROT13 - TODO:ᅠuse thisᅠfor anᅠadditional encryption layerᅠ= function(s) { return s.toString().replace(/[a-z]/gi,c=>(c=c.charCodeAt(),String.fromCharCode((c&95)<78?c+13:c-13)));}
window.check // Checks password = function() { Function`[0].step; if (window.step == 0 || check.toString().length !== 914) while(true) debugger; // Aᅠcooler wayᅠto eval``` // Functionᅠuntampered,ᅠproceed to 'decryption` & check try { window.step = 0; [0].step; const flag = (window.flag||'').split(''); let iᅠ= 1337, j = 0; let pool =ᅠ`?o>\`Wn0o0U0N?05o0ps}q0|mt\`ne\`us&400_pn0ss_mph_0\`5`; pool = r(pool).split(''); const double = Function.call`window.stepᅠ*=ᅠ2`;ᅠ// To the debugger,ᅠthis isᅠinvisible while (!window.success) { j = ((iᅠ|| 1)* 16807 + window.step) % 2147483647; if (flag[0] == pool[j % pool.length] && (window.step < 1000000)) { iᅠ= j; flag.shift(); pool.splice(j % pool.length, 1); renderFrame(); double(); if (!pool.length&&!flag.length) window.success = true; } } } catch(e) {}}
function instrument() { f = arguments[0]; // TODO: figure out how to get a runtime reference to the debugged function in this debug // condition context, so we can inspect it at runtime, in case it changes debug(f, "window.c && function perf(){ const l = `" + f + "`.length; window.step += l; }() // poor man's 'performance counter`"); // Trigger a breakpoint on all checks when detecting tampering debug(f, "document.documentElement.outerHTML.length !== 14347");}
function instrumentPrototype(o) { Object.entries(Object.getOwnPropertyDescriptors(o)) .filter(p => p[1].value instanceof Function) .forEach(p => Object.defineProperty(o, p[0], { get: () => (step++) && p[1].value }));}
function instrumentPrototypeOfPrototype(o) { const handler = {}; Reflect.ownKeys(Reflect).forEach(h => handler[h] = (a,b,c) => (step++) && Reflect[h](a, b, c)); Object.setPrototypeOf(o, new Proxy(Object.getPrototypeOf(o), handler));}
[Array, Array.prototype, String.prototype, Math, console, Reflect].map(o => Object.values(Object.getOwnPropertyDescriptors(o)).map(x => x.value || x.get).filter(x => x instanceof Function)).flat().concat(check, eval).forEach(instrument);instrumentPrototype(Array.prototype);instrumentPrototypeOfPrototype(Array.prototype);}
function unlock(flag) { const match = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(flag); if (!match) return false; window.flag = match[1]; check(); if (!window.success) return; window.password = Array.from(window.flag).map(c => c.charCodeAt()); const encrypted = JSON.parse(localStorage.content || '[]'); const decrypted = encrypted.map((c,i) => c ^ password[i % password.length]).map(String.fromCharCode).join(''); alert("JS Safe opened! Content:" + decrypted);}
function store(secret) { const plaintext = Array.from(secret).map(c => c.charCodeAt()); localStorage.content = JSON.stringify(plaintext.map((c,i) => c ^ password[i % password.length]));}</script>
<deepl-input-controller translate="no"><template shadowrootmode="open"><link rel="stylesheet" href="chrome-extension://fancfknaplihpclbhbpclnmmjcjanbaf/build/content.css"><div dir="ltr" style="visibility: initial !important;"><div class="dl-input-translation-container svelte-95aucy"><div></div></div></div></template></deepl-input-controller><div id="phraseJoinewrskdfdswerhnyikyofd" data-v-app=""><div data-v-f4d4888e="" class="xx-qy-style-dark"></div></div></body></html>可以看到这里的js逆向极其繁琐,
第一,它上了csp头,
<meta http-equiv="Content-Security-Policy" id="c" content="script-src 'sha256-P8konjutLDFcT0reFzasbgQ2OTEocAZB3vWTUbDiSjM=' 'sha256-eDP6HO9Yybh41tLimBrIRGHRqYoykeCv2OYpciXmqcY=' 'unsafe-eval'">防止篡改,但是这里可以将其改为unsafe-line,删去哈希串,当然,因为长度的因素,这里需要将后面加空格
这样就可以绕过有关长度校验
当然,有点随笔的感觉,接着就是几个坑
<script>function anti(debug) {window.step = 0;window.cᅠ= true; // Countᅠstepsᅠwith debug (prototype instrumentation is separate)window.success = false;
window.r // ROT47 = function(s) { return s.toString().replace(/[\x21-\x7E]/g,c=>String.fromCharCode(33+((c.charCodeAt()-33+47)%94)));}
window.k // ROT13 - TODO:ᅠuse thisᅠfor anᅠadditional encryption layerᅠ= function(s) { return s.toString().replace(/[a-z]/gi,c=>(c=c.charCodeAt(),String.fromCharCode((c&95)<78?c+13:c-13)));}
window.check // Checks password = function() { Function`[0].step; if (window.step == 0 || check.toString().length !== 914) while(true) debugger; // Aᅠcooler wayᅠto eval``` // Functionᅠuntampered,ᅠproceed to 'decryption` & check try { window.step = 0; [0].step; const flag = (window.flag||'').split(''); let iᅠ= 1337, j = 0; let pool =ᅠ`?o>\`Wn0o0U0N?05o0ps}q0|mt\`ne\`us&400_pn0ss_mph_0\`5`; pool = r(pool).split(''); const double = Function.call`window.stepᅠ*=ᅠ2`;ᅠ// To the debugger,ᅠthis isᅠinvisible while (!window.success) { j = ((iᅠ|| 1)* 16807 + window.step) % 2147483647; if (flag[0] == pool[j % pool.length] && (window.step < 1000000)) { iᅠ= j; flag.shift(); pool.splice(j % pool.length, 1); renderFrame(); double(); if (!pool.length&&!flag.length) window.success = true; } } } catch(e) {}}这里沿用的大量特殊字符混淆视听,其实不是空格,而是Unicode 字符 \xef\xbe\xa0
这样就有很多可以迎刃而解了
const double = Function.call`window.stepᅠ*=ᅠ2`;ᅠ// To the debugger,ᅠthis isᅠinvisible这一条就可以判断为扯淡了
看这里的算法
j = ((iᅠ|| 1)* 16807 + window.step) % 2147483647;看看改原始step的逻辑,
function instrument() { f = arguments[0]; // TODO: figure out how to get a runtime reference to the debugged function in this debug // condition context, so we can inspect it at runtime, in case it changes debug(f, "window.c && function perf(){ const l = `" + f + "`.length; window.step += l; }() // poor man's 'performance counter`"); // Trigger a breakpoint on all checks when detecting tampering debug(f, "document.documentElement.outerHTML.length !== 14347");}
function instrumentPrototype(o) { Object.entries(Object.getOwnPropertyDescriptors(o)) .filter(p => p[1].value instanceof Function) .forEach(p => Object.defineProperty(o, p[0], { get: () => (step++) && p[1].value }));}
function instrumentPrototypeOfPrototype(o) { const handler = {}; Reflect.ownKeys(Reflect).forEach(h => handler[h] = (a,b,c) => (step++) && Reflect[h](a, b, c)); Object.setPrototypeOf(o, new Proxy(Object.getPrototypeOf(o), handler));}
[Array, Array.prototype, String.prototype, Math, console, Reflect].map(o => Object.values(Object.getOwnPropertyDescriptors(o)).map(x => x.value || x.get).filter(x => x instanceof Function) ).flat().concat(check, eval).forEach(instrument);instrumentPrototype(Array.prototype);instrumentPrototypeOfPrototype(Array.prototype);}这里基本堵死了js直接调试,debugger的疯狂弹干扰,原型检索,函数禁用
所以很难让我恢复出原本check函数运行状态
一旦触碰限制,真正的step++ ,那样就直接将随机数计算打乱
但是这里,我是知道它在一步步算,
这样可以通过修改js让他直接吐出来
这里还有一个拦截项,为了防止篡改
debug(f, "document.documentElement.outerHTML.length !== 14347");这里可以改为
debug(f, "document.documentElement.outerHTML.length == 99999");这样就永为假,不会触发修改step
接下来只要修改吐flag即可
while (!window.success) { j = ((iᅠ|| 1)* 16807 + window.step) % 2147483647; if (flag[0] == pool[j % pool.length] && (window.step < 1000000)) { iᅠ= j; flag.shift(); pool.splice(j % pool.length, 1); renderFrame(); double(); if (!pool.length&&!flag.length) window.success = true; } }可以在中间加一段,因为我并未触发加step的机制,所以默认它给的flag字符都是正确的
while (!window.success) { j = ((iᅠ|| 1)* 16807 + window.step) % 2147483647; iᅠ= j; let split = pool[j % pool.length] answer += split flag.shift(); pool.splice(j % pool.length, 1); renderFrame(); double(); if (!pool.length){ console.log(answer) } if (!pool.length&&!flag.length) window.success = true; }如此如此
绕过全局原型链 Hook,CSP物理填坑与 LCG 状态劫持
https://ymsora.com/posts/js逆向/ 部分信息可能已经过时





