• 上篇

    Eucalypt Forest

    题目给出了一段 Python 代码,表示 Cookie 是用 AES CBC 加密的,我们使用用户名 xdmin 登录,然后经过测试,密文的第 14 字节对应的是字母 x ,只需要暴力修改第 14 字节 (0-256),判断返回内容中是否有 admin 关键词,即可伪造 admin 登录,获得 Flag。

    Wolf Spider ( By swordfeng )

    这题用上了几乎一半 Coursera Stanford Cryptography I 的知识吧……
    Signup 输入一个用户名,之后会根据用户名生成一个 hash+ciphertext 的cookie ,如果用户名是 admin 就能点开 admin 拿到flag
    不多说,贴代码

    @staticmethod
    def encode(t):
        # because len(s) may not be a multiple of the key size, we need to pad it
        # https://tools.ietf.org/html/rfc2315#section-10.3 has a way of
        # clearly indicating how padding should be performed, and how it
        # should be removed.
    
        pad = (16 - (len(t) % 16))
        t += chr(pad) * pad
    
        iv=os.urandom(16)
    
        algo = AES.new(Storage.aes_key,
            AES.MODE_CBC,
            IV=iv)
        crypttext = algo.encrypt(t)
        return (iv + crypttext)
    

    encode是普通的 iv || AES_CBC(key, iv, data),这没什么问题。

    @staticmethod
    def make(dct):
        tcd = urllib.urlencode(dct)
    
        # Use RFC1321 to hash our data, so it can't be tampered with.
        h = SHA.new()
        h.update(Storage.mac_key)
        h.update(tcd)
        s = h.digest()
    
        coded = CookieCutter.encode(tcd)
    
        return s.encode('hex') + "." + coded.encode('hex')
    

    这里问题来了:最后的 cookie 是 SHA1(mac_key || tcd) || iv || AES_CBC(key, iv, tcd)

    @staticmethod
    def unmake(st):
        pieces = st.split(".")
        if len(pieces) != 2:
            return None
    
        s = CookieCutter.decode(pieces[1].decode('hex'))
        if s == None:
            return None
    
        h = SHA.new()
        h.update(Storage.mac_key)
        h.update(s)
        f = h.hexdigest()
    
        if pieces[0] != f:
            # print "hash comparasion failed :("
            return None
    
        kv = urlparse.parse_qsl(s)
        ret = {}
        for k, v in kv:
            ret[k] = v
        return ret
    

    这里还有一个问题,返回时的for循环,后面的值会把前面的覆盖。
    构成这个攻击的条件有3个:

    1. 在用CBC模式的情况下对明文tcd使用mac,并且区分了pad error(raise)和mac error(return None)
    2. 使用自己构造的MAC方法,特别是用SHA1时把key放在开头
    3. unmake时后面的username会覆盖掉前面的 (而且还把算法暴露出来了)

    下面分别解释这三个问题如何构造出攻击来。

    1. 关于unmake 比如原来的cookie明文是username=aaa,这时候只有在后面追加成username=aaa........&username=admin就能实现把用户名替换成admin。
    2. 关于明文MAC 这种模式是典型的可以构造出padding oracle attack的形式。我们知道AES是一个(K, M) -> C的映射,
      CBC模式对于每一块都进行这样的循环:C <- AES(K, M xor IV); IV <- C;
      因为必须要一整块才能进行AES的计算,因此明文会进行padding,具体操作是补上1-16个字节到末尾,
      并且字节内容等于字节数。
      下面构造有C的情况下的AES的逆函数C -> (M xor IV) 任意给一个IV。修改IV的最后一个字节,把IV||C传给服务器做验证。
      如果明文M的最后一个字节是0x01那么结果会是mac错(跳转),否则一般是pad错(返回500)。 对于有多个可能的情况,如果修改倒数第二个字节不受影响,那么可以确认pad是0x01。
      这样试验约256次后,我们得到了使M最后一个字节是0x01的IV。把IV的最后一字节异或0x01就得到这个块(M xor IV)的最后一个字节。
      我们再让M最后一个字节变成0x02,并且对倒数第二个字节进行相同的攻击,就可以得到这个块的倒数第二个字节。
      反复进行就能把整个M xor IV恢复出来。
      有了M xor IV之后,我们就可以通过控制IV来控制M的内容。并且,由于IV正好就是前一个C,这么做可以构造任意合法的密文。
    3. 关于SHA1 SHA1可以被length extension attack,也就是知道一个sha1的值和它的message长度,可以构造一个更长的含有人工构造的信息的合法的sha1。
      SHA1也有padding。假设message长度是l字节(l<56),pad后就是 message || 0x80 0x00 0x00 0x00 ... 0x00 || uint64be(l * 8) 总长度64字节。
      这里有个方便的库hashpump,只要知道最前面key的长度,后面加上的数据的内容,sha1的结果,要追加的内容,就能构造出追加后的(sha1, message)。

    最后说一下整体的操作。先输入一个用户名比如aaa得到hash。追加数据,得到一个username=aaa\x80\x00\x00...\x01\x60&username=admin的message和新的合法hash。把message加上padding(刚好一个\x01),分成3个block,从最后一个密文块全0开始往前计算IV,构造出合法的密文。最后修改cookie,刷新,用户就变成admin了。
    贴一下使用的padding oracle attack代码:

    'use strict';
    var co = require('co');
    var request = require('request');
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
    
    var mac = 'a2a1ceee1953540238446536d7ad58c56f8f23e5';
    
    function postwith(cookie) {
        return new Promise((resolve) => {
            request({
                method: 'GET',
                url: 'https://wolf-spider.ctfcompetition.com/qwerty',
                headers: {
                    'Cookie': 'UID=' + cookie,
                    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.7 Safari/537.36'
                }
            }, function (err, response) {
                if (err) console.log(err);
                resolve(response.statusCode);
            });
        });
    }
    
    co(function* () {
        var b = Buffer(process.argv[2], 'hex');
        yield bruteplain(b);
    });
    
    function* bruteplain(block) {
        var iv = Buffer(16);
        iv.fill(0);
        for (let pos = 15; pos >= 0; pos--) {
            for (var p = pos; p < 16; p++) iv[p] ^= 16 - pos;
            let tt = [];
            for (let test = 0; test < 256; test++) {
                iv[pos] ^= test;
                tt[test] = postwith(mac + '.' + iv.toString('hex') + block.toString('hex'));
                iv[pos] ^= test;
            }
            var result = yield new Promise(co.wrap(function* (resolve) {
                for (let test = 0; test < 256; test++) {
                    var code = yield tt[test];
                    if (code !== 500) {
                        // maybe right
                        if (pos === 0) resolve(test);
                        else {
                            iv[pos] ^= test;
                            iv[pos-1] ^= 32;
                            code = yield postwith(mac + '.' + iv.toString('hex') + block.toString('hex'));
                            iv[pos-1] ^= 32;
                            iv[pos] ^= test;
                            if (code !== 500) resolve(test);
                        }
                    }
                }
            }));
            console.log(pos, 'got:', result);
            iv[pos] ^= result;
            for (var p = pos; p < 16; p++) iv[p] ^= 16 - pos;
            console.log('plain: ', iv.toString('hex'));
        }
        console.log('cipher:', block.toString('hex'));
        console.log('plain: ', iv.toString('hex'));
        return iv;
    }
    

    执行node file cipher,大概7秒能出一个字节。

    For2

    题目给出了一个 Wireshark 抓包 ,打开之后发现是 USB 鼠标的抓包。

    USB 鼠标的数据为 4 个字节,第四个字节一般不会用到。

    第一个字节为鼠标的状态,前 5 个 bit 不用管,第 6 个 bit 是鼠标中键按下的状态,第 7 个 Bit 是鼠标右键的状态,最后一个是左键

    第二个字节为 X 的移动,signed int ,第三个字节为 Y 的移动, signed int

    题目中的第一个字节不是 0 就是 1,说明只用到了鼠标左键,我们写脚本将鼠标移动的图片画出来即可获得 Flag

    代码如下

    import struct
    from PIL import Image
    import dpkt
    import binascii
    
    INIT_X, INIT_Y = 1000, 1000
    
    def compliment(h):
        i = int(h, 16)
        return i - ((0x80 & i) << 1)
    
    def print_map(pcap, device):
        picture = Image.new("RGB", (2200, 2500), "white")
        pixels = picture.load()
    
        x, y = INIT_X, INIT_Y
    
        for ts, buf in pcap:
            mouse_data = buf[27:32]
            mdstr = binascii.hexlify(mouse_data);
            ml = len(mdstr)
            if ml != 8:
                continue
            bits = int(mdstr[0] + mdstr[1],base=16)
            bx = compliment(mdstr[2] + mdstr[3])
            by = compliment(mdstr[4] + mdstr[5])
    
            if bx > 20:
                continue
            if by > 20:
                continue
    
            status = bits
            x = x + bx
            y = y + by
    
            if (status == 1):
                for i in range(-5, 5):
                    for j in range(-5, 5):
                        pixels[x + i , y + j] = (0, 0, 0, 0)
            else:
                pixels[x, y] = (255, 0, 0, 0)
        picture.save("out.png", "PNG")
    
    if __name__ == "__main__":
    
        f = open("1.pcap", "rb")
        pcap = dpkt.pcap.Reader(f)
    
        print_map(pcap, 3)
        f.close()
    

    No Big Deal

    题目给出了一个 Wireshark 抓包

    里面数据实在是太多了,咋办呢?我们 grep 一波试试

    strings *.pcap | grep ctf
    

    不行,这下咋办?我们把 CTF 关键词 base64 编码一下,再 grep 一下

    strigns *.pcap | grep Q1RG
    
    Q1RGe2JldHRlcmZzLnRoYW4ueW91cnN9
    

    其实 NBD 第二个也很简单,先 binwalk 提取再 grep ctf 就好了,当时脑抽了没想到,还去提取了别的数据。。。

    BonaFortuna (443)

    同样的,题目给出了一个 Wireshark 抓包

    我们可以看到前面进行了一次 git clone,然后后面一串 SSL 加密的数据。

    对着 git clone 进行 Follow TCP Stream, 然后写一个脚本从 TCP Stream 里面按路径提取文件

    var fs = require('fs');
    var bsplit = require('buffer-split');
    
    
    var p1 = fs.readFileSync('./part1');
    
    write(p1);
    
    function write(px) {
      var pkts = bsplit(px,new Buffer('GET '));
      for (var i = 1; i < pkts.length; i++) {
        var dta = bsplit(pkts[i], new Buffer('\r\n\r\n'));
        var url = bsplit(dta[0], new Buffer(' HTTP/1.1'))[0];
        var path = url.toString();
        ensureDirectoryExistence(path);
        var data = dta[2];
        fs.writeFileSync(path,data);
      }
    }
    
    function ensureDirectoryExistence(filePath) {
      var path = require('path');
      var dirname = path.dirname(filePath);
      if (directoryExists(dirname)) {
        return true;
      }
      ensureDirectoryExistence(dirname);
      fs.mkdirSync(dirname);
    }
    
    function directoryExists(path) {
      try {
        return fs.statSync(path).isDirectory();
      }
      catch (err) {
        return false;
      }
    }
    

    把提取出来的文件放到 .git 文件夹下,然后进行 git fsck ,即可获得一个生成 SSL 私钥和证书的 python 脚本,但是发现随机数生成器有些问题。

    class SecureRand(object):
        """Implement a better PRNG by wrapping Fortuna."""
    
        def __init__(self, seed=None):
            self.state = hashlib.sha256(seed or self._newSeed()).digest()
            self.prng = FortunaGenerator.AESGenerator()
            self.prng.reseed(self.state)
    
        def _newSeed(self):
            return (os.name + str(int(time.time())))
    
        def read(self, n):
            return self.prng.pseudo_random_data(n)
    

    随机数竟然是用时间戳生成的。

    我们回到 Wireshark 包,提取出 SSL 握手阶段的证书,然后提取出 Public Key 和 n 的值

    对生成的脚本进行一下修改,从参数读取 timestamp,然后如果找到相等的 n 值,则把生成的私钥和证书保存到本地

    通过 git log --format="%at" 命令得知最后一次提交的时间戳为 1454713929

    写一段小脚本暴力执行

    for (var i = 1454713929; i < 1454713999; i++) {
        console.log(i);
        console.log(exec('python2 keygen.py flagserver ' + i).toString());
    }
    

    即可获得 flagserver.crt 和 flagserver.key ,将 flagserver.key 导入 Wireshark 内置的 RSA 解密插件内,即可获得加密传输的 Flag

    Fin

    Writeup 就到这里啦,由于篇幅和时间有限,有些简单/常规的题没有写出来。

    还有什么 PPC 的 pwn 题。。实在太变态,没有解出来。

    这次 CTF 各种不按套路出牌,感觉学到了很多东西。

    另外:我计划在下个月重写 Blog 系统,带上最新的前端技术,并使用 C++ 构建后端。

    再吐槽一句:苹果的 Network Extension 申请了十几次了,一点回复都没有,没说成功也没说拒绝,本来还想着写 iOS ** 工具,结果。。。。。