有趣的一个系列。
Tap Into Mr. Mxyzinjin’s Toy Safe
输出login
,可以看到:
function check(pw) {
return new global.RegExp(`^${pw}$`).test(passwd);
}
容易想到二分区间[0x4e00,0x9fff]
,用^(.{i}[l-r])|\$
求出第i+1
个字符即可。
Tap Into Mr. Mxyzinjin’s Safe
输出login
,可以看到:
function check(pw) {
return void new RegExp(`^${pw}$`).test(passwd);
}
注意到void
标识符使得login
只能返回undefined
,考虑用时序攻击的方法求出串。
继续二分,容易写出正则表达式^.{i}[l-r].{length-i-1}$
来第i+1
个字符是否位于区间[l,r]
内,此时当区间正确时,login
会比另一半区间运行更多的时间,因为引擎还会继续匹配.{length-i-1}
。但当i
增大时,这个正则表达式的效果迅速减弱。
考虑用|
运算符来处理这个问题,可以写出正则表达式^(.{i}[l-r])|(\\D{4})$
,此时当区间错误时,login
一定会比另一半区间运行更多的时间,因为引擎还会继续匹配(\\D{4})
,而且花费的时间足够让我们将两者区分开来。
至于字符串长度,我们设一个足够小的值,当两段区间的login
耗时之差不大于这个值时,判定字符串结束即可。(其实我就随便设了几个,跑了几发就过了
const {performance}=require('perf_hooks');
function sgett(l,r,dt,login){
var i,j,k,t,gc=e=>e.toString(16),reg=`(.{${dt}}[\\u${gc(l)}-\\u${gc(r)}])|(\\D{4})`;
var run=_=>{var dk=performance.now();login(reg);return performance.now()-dk;}
var ar=[run(),run(),run(),run()];ar.sort();
return ar[1]+ar[2];
}
function getc(login,dt){
var mx=0,ts,i,j,k;
var b=0x4e00,e=0x9fff,m;while(b<e){
m=(b+e)>>1;var x=sgett(b,m,dt,login),y=sgett(m+1,e,dt,login);
if(Math.abs(x-y)<=0.0019)return null;
x<y?e=m:b=m+1;
}
return String.fromCharCode(b);
}
function crack(login) {
var i,t,rs='';for(i=0;;++i){
t=getc(login,i);if(!t)break;rs+=t;
}
return rs;
}
Tap Into Mr. Mxyzinjin’s Matrix
输出login
,可以看到:
function check(pw) {
return !(Array.isArray(pw) || pw.length>passwd.length) && [...passwd].every((c,i)=>c===pw[i]);
}
观察到对pw
的类型检验不严,直接用proxy
伪造一个对象传进去记录被比较的字符即可。
Tap Into Mr. Mxyzinjin’s Brain
输出login
,可以看到:
function check(pw) {
return typeof pw==='string' && passwd.length>=pw.length && passwd.every((e,i)=>e===pw[i]);
}
显然此时我们只能用时序攻击去求出串。
这题倒不用拐弯抹角的,直接写就好了,注意运行多次login
减少performance.now()
的误差。我是运行了1000
次,排序之后取中间100
次的值作为结果,效果还不错。
const {performance}=require('perf_hooks');
function crack(login) {
console.log(login.toString());
while(1){
var pw='',i,j,k;for(i=0;i<30;++i){
var mw=0,ps='';for(j=0;j<10;++j){
var t=pw+j,ar=new Array(1000);
for(var d=0;d<1000;++d){
var a=performance.now();
login(t);
var b=performance.now();
ar[d]=b-a;
}
ar.sort();var s=0;for(var z=450;z<550;s+=ar[z++]);
if(s>mw)mw=s,ps=''+j;
}
pw+=ps;
}
for(i=0;i<10;++i)for(j=0;j<10;++j)if(login(pw+i+j))return pw+i+j;
}
}
Tap into Mr. Mxyzinjin’s Stream
输出login
,可以看到:
function check(pw) {
if(tries++>=32) throw new Error('The stream becomes too unstable, it exploded and killed you in the process');
return typeof pw==='string' && pw.includes(passwd);
}
容易想到找到一个包含所有
x
<
2
25
x< 2^{25}
x<225的串
S
S
S,通过login
判断目标是否在
S
S
S中来二分,容易在25次调用下解决问题。注意到如果我们从一个24位的01串开始,在其后添加0或1,得到的数可以用
2
s
+
0
∣
1
m
o
d
2
25
2s+0|1\mod 2^{25}
2s+0∣1mod225表示,那么我们尝试直接完全随机地生成这个串,在线下测试发现可以覆盖
80
%
80\%
80%的数,优化一下,我们直接不用随机,每次
O
(
1
)
O(1)
O(1)在表中查询一下新的
s
s
s是否已经出现过了,测试发现这么做可以使得
S
S
S覆盖
99
%
99\%
99%的数,虽然我不会证(ε=ε=ε=┏(゜ロ゜;)┛
注意JavaScript
的随机取值速度非常缓慢,如果直接生成一个
2
25
2^{25}
225的表是一定会T的。我是直接生成了一个
2
20
2^{20}
220的Uint32Array
用做BitSet
来记录每个数的状态。
var keys=25,st=new Uint32Array(1<<20);
function gensm(){
var i,j,k,d,dt,t,rs='',hmd=(1<<5)-1,n=1<<(keys-3),md=(1<<25)-1;
for(d=i=0;i<24;++i){
k=0,d=((d<<1)+k)&md;rs+=k;
}
for(i=0;i<n;++i){
var ts='',dn=1<<3;for(j=0;j<dn;++j){
k=0,dt=((d<<1)+k)&md;if(st[dt>>5]&(1<<(dt&hmd)))k^=1;
d=((d<<1)+k)&md;if(dt==d||!(st[d>>5]&(1<<(d&hmd))))st[d>>5]^=(1<<(d&hmd));
ts+=k;
}
rs+=ts;
}
return rs;
}
function _cl(login,n,len){
if(len==1)return n;
var mi=24+(len>>1),l=n.slice(0,mi);
if(login(l))return _cl(login,l,len>>1);
return _cl(login,n.slice(mi-24),len>>1);
}
var sm=gensm();
function crack(login) {
return _cl(login,sm,1<<keys);
}