很有意思的一题逆向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!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" 5=}<^Y?ii,7('-$
ZH%aT=ws"kgLF$T :~mR9%OQ,w7BMdY
b}|/%67!xz&|I~N ^,/cG8Tnq;]96wT
g%l$!0Psg2S'dn% ########## cXUU19V{&>m*;>o
~Meepb"9ft"*E.D # ##### #### b=<V.s+m(x=:.5[
>CGqx0AvnhC"jMN # ########## z%#WY-v@kp;({]Z
ga+7yj:lPzD_ASb # # # # 38t>^J&YsAa}:>>
<D0uaBCl$H^;mj| # # # # /KZGA7%*"^!q0/]
_@~]fU@'RMyt*Z} # # # # ~"EO9Fxo+Y(d4l4
eX,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&wGYQx
IH;ZT/fm{C_A_:; # # # # On!M%A].7vhbiz:
lGl"LJ%M~.Sb6~) ########## # OW/@)mDwW$czfAZ
az0b-_u&#*^v@-[ #### ##### # P9n6LJiTB',j.2I
NU c6GH(ekyxHV, ########## [S?3Zn;p4k,YFXx
{RNy(zq]".#>]C< eQN''6H?X-oS*#R
eHG26u.HCZX!9!w c$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
</pre>
<script id="gemini's cube">
// --- Configuration ---
const canvas = document.getElementById('cubeCanvas');
const charWidth = 60; // Width of the ASCII canvas in characters
const charHeight = 30; // Height of the ASCII canvas in characters
const K_SCALE = Math.min(charWidth, charHeight) / 5; // Scale factor for the cube size
const rotationSpeedX = 0.02;
const rotationSpeedY = 0.015;
const frameInterval = 200;
const edgeChar = '#'; // Character used to draw edges
const 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 indices
const 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 line
function f(s) {
return s.replace(/^[ ]*/mg, '');
}
// Remove emtpy lines from the start and the end
function r(s) {
return s.replace(/^\n/, '').replace(/\n$/, '')
}
// Tagged template function to help define multiline strings
function 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,删去哈希串,当然,因为长度的因素,这里需要将后面加空格
这样就可以绕过有关长度校验
当然,有点随笔的感觉,接着就是几个坑
这里沿用的大量特殊字符混淆视听,其实不是空格,而是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;
}
如此如此
1 个帖子 - 1 位参与者