抽空玩了一下Google Ctf,发现自己太菜了,好好学习吧
安全脉搏
You stumbled upon someone's "JS Safe" on the web. It's a simple HTML file that can store secrets in the browser's localStorage. This means that you won't be able to extract any secret from it (the secrets are on the computer of the owner), but it looks like it was hand-crafted to work only with the password of the owner...
看了题目觉得非常阴吹斯汀,还有反调试。基本上就是JS没跑了,提供了一个txt文件,直接下载下来。
打开TXT文件,发现是其实一个个HTML文件。
直接本地浏览器打开,看到有一个炫酷的小盒子,然后让你输入Key,验证是否通关
我们提交表单触发验证,那么我们就来看看这个表单,这个一切开始地方
<input id="keyhole" autofocus onchange="open_safe()" placeholder="????">
安全脉搏
现在我们去看绑定onchange事件的open_safe,格式化一下看起来更方便
<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>
观察函数代码后我们可以获得一些信息
我们输入的应该是CTF{XXXXXX}这种类型的内容,不然页面会提示错误
我们输入的如果是CTF{XXXXXX}这种类型的,那么会进入x函数进行判断,函数返回值同样影响页面是否报错
这个题目的目的我们基本上已经摸清,算出可以验证成功可以通关的内容,目测这个内容就是Flag
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value); if (!password || !x(password[1])) return document.body.className = 'denied';
脉搏文章标题链接 Safe 2.0 Google-CTF-2018
https://www.secpulse.com/archives/73137.html
<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>
嗯,这一横条就是x函数,看起来很不方便,所以我们可以像之前一样格式化一下
<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>
看起来舒服多了 O(∩_∩)O~~
观察函数代码后我们可以获得一些信息
- 函数内部重命名了一些较长的JS内置函数,简化了代码
- 定义了h()和c()两个函数,看了眼这两个函数都不知道是干嘛的蛋疼
- 函数开始的地方为for (a = 0; a != 1000; a++) debugger;
- 决定函数返回值(是否通过验证)的位置是with (source) return eval('eval(c(source,x))')
谷歌大法好,直接拖进谷歌浏览器里面,打开F12。输入CTF{aaa},控制台直接跳到了Sources模块的断点。
for (a = 0; a != 1000; a++) debugger;这里进行了1000次循环的debugger。
肯定不能一个一个跳过,不然会死人。难道就是传说中的反调试??
这里的代码不重要,我们可以直接在控制台里面设置a的值为999就直接跳过了(不推荐包括删除,注释等修改代码)
继续看这一段x = h(str(x));
这里是第一次调用h(),开始认真理解下这个函数。
这个函数接受一个值,然后经过一些看不懂的操作,最终输出一个四字节的字符,有一种hash函数的感觉。
return chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF)
安全脉搏
这里有个大坑,就是这个传入函数的x参数,并不是我们调用x()时我们键入的内容,它们只是长得很相似,它们只是长得很相似,它们只是长得很相似!!!!
这里我们可以使用PhpStorm,010 Editor帮我们进行区分,我用异形X
在后文进行区分,方便阅读
- phpstorm说明的很详细,一个是未使用的变量(没错异形x确实没有直接在代码中被使用),还有个是不是ASCII字符
010 Editor查看两个字符的十六进制也不同
感觉这里的情况类似IDN homograph attack(国际域名同形异义字欺骗攻击),ORZ
详情参考:https://en.wikipedia.org/wiki/IDN_homograph_attack
这里传入的参数是x,不是异形x,所以这里的参数是函数本身的代码
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){}}
Safe 2.0 Google-CTF-2018
https://www.secpulse.com/archives/73137.html
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,然后重定义了它的toString方法。
然后就是console.log,这个看起来好像没什么用,还有return这个就是我们能否通过的依据吧
安全脉搏
然后我们发现都提到了c(),然后再仔细看看这个函数
嗯,看了好久,还是没看出什么东西,大概就是参数XOR,然后相加的操作吧,先记着
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 }
我们之前通过下断点调试到了x = h(str(x));这个位置
然后我们继续单步调试下去,然后你就会神奇的发现,进入try这个模块的时候,浏览器就进入假死状态了
WTF,真的反调试,然后我尝试在各种地方下断点,这地方花了至少四五个小时吧,我希望能弄清楚浏览器假死的原因,还有弄清变量数据的变化
console.log()经常假死,很蛋疼,幸运的最后还是弄清了,toString方法就是罪魁祸首,这个方法会调用c(),然后将source传入。
这个source其实是一个正则表达式的对象
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp)
一个正则表达式是没有length这个属性的,所以就会undefined,然后到for循环的里面就会陷入死循环,浏览器就假死了。
<script> var source = /test/; console.log(source.length); for (i = 0; i != source.length; i++) console.log('死循环'); </script>
可以发现陷入死循环,因此c()就是导致浏览器假死的元凶,果真是反调试,ORZ。
我们还得想办法解决/避开这个反调试,这就是另一个疑惑,就是我们要弄清到底是哪里触发了toSring,才能更好应对。
经过反复测试,发现并不是代码直接调用的toString。
我们在控制台调试界面有些模块会显示变量的值,就是控制台这时候触发的这个方法(console.log同理)。
验证方法:
随便输入,然后提示你失败,页面现在还是正常的,你F12,发现浏览器假死了,因为console.log('debug', source);,要把结果显示在控制台,这里触发函数了,导致假死。
注释掉console.log('debug', source);,随便输入,然后提示你失败,你F12,页面现在还是正常的没有进入假死,如果你在控制台输入source同样假死。
反调试就算弄懂了,修改toString为return 0,大胆调试就行了。
如果你非常仔细,你会发现其实在最后也是调用了c(source,x),连参数看起来都是一模一样的,为什么这里没有触发假死呢,其实都归咎于with。
with关键字的作用在于改变作用域
var qs = location.search.substring(1); var hostName = location.hostname; var url = location.href;
这几行代码都是访问location对象中的属性,如果使用with关键字的话,可以简化代码如下:
with (location){ var qs = search.substring(1); var hostName = hostname; var url = href; }
使用了with语句关联了location对象,在with代码块内部,每个变量首先被认为是一个局部变量,
如果局部变量与location对象的某个属性同名,则这个局部变量会指向location对象属性。
详细介绍:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with
回到题目中,source是一个正则表达式对象,然后正则表达式对象也有一个source的对象属性。
source属性用于返回模式匹配所用的文本。
该文本不包括正则表达式直接量使用的定界符,也不包括标志 g、i、m。
参考:http://www.w3school.com.cn/js/jsref_source_regexp.asp
当使用with (source)后,作用域就发生了改变,传入c()的参数其实发生了变化,其实是source.source。
参数其实是一个字符串,并且长度为55,那么自然就不会无线循环陷入假死。
Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤ ¢dÈ9&òªћ#³1᧨
然后看最后的语句return eval('eval(c(source,x))'),这里就是验证的关键,先看c()
这里还有个坑点,就是x的值需要我们自己覆盖,因为x是本身的函数代码,我们格式化或者修改代码调试的时候会导致x发生变化
然后我们会发现c()的返回值很有意思
这是一个通过==判断的表达式字符串,这会传给第一个eval,得到一个Bool的返回值,然后这个返回值决定外部函数流程的是否提示通过
х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х))
这里还有一个坑,表达式中其实是都是异形x,复制出来用上面的方法可以验证,也就是说这里终于用到了我们键入的内容,ORZ
现在我们要想办法键入一个值让这个表达式成立,这个值应该就是FLAG。
然后我们仔细观察发现很多信息:
- 两边相等,FLAG就是异形x,因此c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х))这边的结果同样表示FLAG
- 我们键入的内容也就是FLAG符合正则表达式/^CTF{([0-9a-zA-Z_@!?-]+)}$/
h(x)的这个类似hash的函数输出的密钥就只有4位,长度很弱,然后用于生成密钥的a的范围是(2714~65521),b的范围(33310~65521),密钥种子范围也很小。
我们可以直接在范围内爆破密钥,然后观察生成的结果Flag是否符合正则表达式/^CTF{([0-9a-zA-Z_@!?-]+)}$/
直接写入Python脚本即可,顺便入了python3的坑,这里推荐python3,字符处理很方便chr()等函数对unicode支持更好,真心比Python2好多了
# coding=utf-8 import string import sys def c(a, b): for i in range(len(a)): xor = chr(ord(str(a[i])) ^ ord(str(b[i % len(b)]))) if not (xor in reg_str): return False return True if __name__ == '__main__': reg_str = string.ascii_uppercase + string.ascii_lowercase + string.digits + '_@!?-' text = u'¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ' for a in range(5625, 65521): for b in range(33310, 65521): sys.stdout.write('a = %d b = %d \r' % (a, b)) key = chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF) if c(text, key): Flag = '' for i in range(len(text)): Flag = Flag + chr(ord(str(text[i])) ^ ord(str(key[i % len(key)]))) print('(a = %d - b = %d) | %s' % (a, b, Flag))
CTF{_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_}
通关成功
我真是太菜了,花了十几个小时,ORZ