前言
GoogleCTF 2018 上的 Web 签到题,本质上是一道 JS 逆向,使用了 JS 里一些比较 trick 的技巧
分析
可以看到页面的输入框变化会触发 open_safe()
函数
<input id="keyhole" autofocus onchange="open_safe()" placeholder="🔑">
open_safe()
逻辑如下:
function open_safe() {
keyhole.disabled = true;
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
if (!password || !x(password[1])) return document.body.className = 'denied';
document.body.className = 'granted';
password = Array.from(password[1]).map(c => c.charCodeAt());
encrypted = JSON.parse(localStorage.content || '');
content.value = encrypted.map((c,i) => c ^ password[i % password.length]).map(String.fromCharCode).join('')
}
可以看到这里会先做一个正则匹配,我们输入的内容格式必须是 CTF{xxxxxx}
,其中 xxxxxx 的范围为 0-9a-zA-Z_@!?-
,囊括了大部分的可见字符
然后会将 xxxxxx
作为参数传入 x()
函数,所以我们这里需要 x()
函数返回一个 true 值(或者非空即可)
这里有三个要点需要注意:
-
x 和 х 的区别,前者是 ASCII 字符,后者是西里尔字母,而
x = h(str(x));
实际上的参数是函数自身字符串内容,并不是我们传入的参数 -
for (a = 0; a != 1000; a++) debugger;
运行后 a = 1000,所以当 h 函数被调用时,内部的 a 初始值为 1000
function h(s) {
for (i = 0; i != s.length; i++) {
a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521;
b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521
}
return chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF)
}
source = /Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤ ¢dÈ9&òªћ#³1᧨/;
是正则模板,其没有.length
属性,所以循环应该会无限进行,但由于使用了with
,则实际访问的则是source.source
,对应的是正则模板的字符串,具有.length
属性
Writeup
优化后的代码如下:
function x(х) {
ord = Function.prototype.call.bind(''.charCodeAt);
chr = String.fromCharCode;
str = String;
function h(s) {
for (i = 0; i != s.length; i++) {
a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521;
b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521
}
return chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF)
}
function c(a, b, c) {
for (i = 0; i != a.length; i++) c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])));
return c
}
var a = 1000;
x = h(str(`function x(х){ord=Function.prototype.call.bind(''.charCodeAt);chr=String.fromCharCode;str=String;function h(s){for(i=0;i!=s.length;i++){a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;b=((typeof b=='undefined'?0:b)+a)%65521}return chr(b>>8)+chr(b&0xFF)+chr(a>>8)+chr(a&0xFF)}function c(a,b,c){for(i=0;i!=a.length;i++)c=(c||'')+chr(ord(str(a[i]))^ord(str(b[i%b.length])));return c}for(a=0;a!=1000;a++)debugger;x=h(str(x));source=/Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤
¢dÈ9&òªћ#³1᧨/;source.toString=function(){return c(source,x)};try{console.log('debug',source);with(source)return eval('eval(c(source,x))')}catch(e){}}`));
source = `Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤
¢dÈ9&òªћ#³1᧨`;
try {
console.log(c(source,x));
with(source) return eval('eval(c(source,x))')
} catch (e) {
}
}
运行后可以得到如下表达式:х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х))//᧢
,写 python 脚本爆破即可
由于我们传入的参数对应的字符范围已知,并且加密密钥长度为 4,我们只需要根据加密函数爆破相应加密密钥:
function c(a, b, c) {
for (i = 0; i != a.length; i++) c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])));
return c
}
p = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_@!?-'
sec = '''¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ'''
for i in range(4):
for c in p:
j = i
k0 = ord(sec[j])^ord(c)
flag = True
while j < len(sec)-4:
j += 4
if chr(k0^ord(sec[j])) not in p:
flag = False
break
if flag:
print('%d: %d' % (i, k0))
根据结果可以得到 [253, 149, 21, 249]
\ [253, 153, 21, 249]
两种可能的 key 值,直接带入测试即可
直接带入可以解出真正的原文 _N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_
Overview
这题本质上是一道 reverse 题,使用了一种非常简单的加密,唯一的难度是有意无意使用了一些 trick 的技巧使得我们第一时间没有发现(比如我😭)
Source Code
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS safe v2.0 - the leading localStorage based safe solution with military grade JS anti-debug technology</title>
<!--
Advertisement:
Looking for a hand-crafted, browser based virtual safe to store your most
interesting secrets? Look no further, you have found it. You can order your own
by sending a mail to js_safe@example.com. When ordering, please specify the
password you'd like to use to open and close the safe. We'll hand craft a
unique safe just for you, that only works with your password of choice.
-->
<style>
body {
text-align: center;
}
input {
font-size: 200%;
margin-top: 5em;
text-align: center;
width: 26em;
}
#result {
margin-top: 8em;
font-size: 300%;
font-family: monospace;
font-weight: bold;
}
body.granted>#result::before {
content: "Access Granted";
color: green;
}
body.denied>#result::before {
content: "Access Denied";
color: red;
}
#content {
display: none;
}
body.granted #content {
display: initial;
}
.wrap {
display: inline-block;
margin-top: 50px;
perspective: 800px;
perspective-origin: 50% 100px;
}
.cube {
position: relative;
width: 200px;
transform-style: preserve-3d;
}
.back {
transform: translateZ(-100px) rotateY(180deg);
}
.right {
transform: rotateY(-270deg) translateX(100px);
transform-origin: top right;
}
.left {
transform: rotateY(270deg) translateX(-100px);
transform-origin: center left;
}
.top {
transform: rotateX(-90deg) translateY(-100px);
transform-origin: top center;
}
.bottom {
transform: rotateX(90deg) translateY(100px);
transform-origin: bottom center;
}
.front {
transform: translateZ(100px);
}
@keyframes spin {
from { transform: rotateY(0); }
to { transform: rotateY(360deg); }
}
.cube {
animation: spin 20s infinite linear;
}
.cube div {
position: absolute;
width: 200px;
height: 200px;
background: rgba(0, 0, 0, 0.51);
box-shadow: inset 0 0 60px white;
font-size: 20px;
text-align: center;
line-height: 200px;
color: rgba(0,0,0,0.5);
font-family: sans-serif;
text-transform: uppercase;
}
</style>
<script>
function x(х){ord=Function.prototype.call.bind(''.charCodeAt);chr=String.fromCharCode;str=String;function h(s){for(i=0;i!=s.length;i++){a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;b=((typeof b=='undefined'?0:b)+a)%65521}return chr(b>>8)+chr(b&0xFF)+chr(a>>8)+chr(a&0xFF)}function c(a,b,c){for(i=0;i!=a.length;i++)c=(c||'')+chr(ord(str(a[i]))^ord(str(b[i%b.length])));return c}for(a=0;a!=1000;a++)debugger;x=h(str(x));source=/Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤
¢dÈ9&òªћ#³1᧨/;source.toString=function(){return c(source,x)};try{console.log('debug',source);with(source)return eval('eval(c(source,x))')}catch(e){}}
</script>
<script>
function open_safe() {
keyhole.disabled = true;
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
if (!password || !x(password[1])) return document.body.className = 'denied';
document.body.className = 'granted';
password = Array.from(password[1]).map(c => c.charCodeAt());
encrypted = JSON.parse(localStorage.content || '');
content.value = encrypted.map((c,i) => c ^ password[i % password.length]).map(String.fromCharCode).join('')
}
function save() {
plaintext = Array.from(content.value).map(c => c.charCodeAt());
localStorage.content = JSON.stringify(plaintext.map((c,i) => c ^ password[i % password.length]));
}
</script>
</head>
<body>
<div>
<input id="keyhole" autofocus onchange="open_safe()" placeholder="🔑">
</div>
<div class="wrap">
<div class="cube">
<div class="front"></div>
<div class="back"></div>
<div class="top"></div>
<div class="bottom"></div>
<div class="left"></div>
<div class="right"></div>
</div>
</div>
<div id="result">
</div>
<div>
<input id="content" onchange="save()">
</div>
</body>
</html>