综合题目

2017 34c3 Software_update

可以看出,程序的大概意思是上传一个 zip 压缩包,然后对 signed_data 目录下的文件进行签名验证。其中,最后验证的手法是大概是将每一个文件进行 sha256 哈希,然后异或起来作为输入传递给 rsa 进行签名。如果通过验证的话,就会执行对应的 pre-copy.py 和 post-copy.py 文件。

很自然的想法是我们修改 pre-copy.py 或者 post-copy.py 文件,使其可以读取 flag,然后再次绕过签名即可。主要有两种思路

  1. 根据给定的公钥文件获取对应的私钥,进而再修改文件后伪造签名,然后大概看了看公钥文件几乎不可破,所以这一点,基本上可以放弃。

  2. 修改对应文件后,利用异或的特性使得其哈希值仍然与原来相同,从而绕过签名检测。即使得 signed_data 目录下包含多个文件,使得这些文件的哈希值最后异或起来可以抵消修改 pre-copy.py 或者 post-copy.py文件所造成的哈希值的不同。

这里,我们选择第二种方法,这里我们选择修改 pre-copy.py 文件,具体思路如下

  1. 计算 pre-copy.py 的原 hash 值。

  2. 修改 pre-copy.py 文件,使其可以读取 flag。与此同时,计算新的 hash 值。将两者异或,求得异或差值 delta。

  3. 寻找一系列的文件,使其 hash 值异或起来正好为 delta。

关键的步骤在于第三步,而其实这个文件可以看做是一个线性组合的问题,即寻找若干个 256 维01向量使其异或值为 delta。而

(F={0,1},F256,,)(F=\{0,1\},F^{256},\oplus ,\cdot)

是一个 256 维的向量空间。如果我们可以求得该向量空间的一个基,那么我们就可以求得该空间中任意指定值的所需要的向量。

我们可以使用 sage 来辅助我们求,如下

# generage the base of <{0,1},F^256,xor,*>
def gen_gf2_256_base():
    v = VectorSpace(GF(2), 256)
    tmphash = compute_file_hash("0.py", "")
    tmphash_bin = hash2bin(tmphash)
    base = [tmphash_bin]
    filelist = ['0.py']
    print base
    s = v.subspace(base)
    dim = s.dimension()
    cnt = 1
    while dim != 256:
        tmpfile = str(cnt) + ".py"
        tmphash = compute_file_hash(tmpfile, "")
        tmphash_bin = hash2bin(tmphash)
        old_dim = dim
        s = v.subspace(base + [tmphash_bin])
        dim = s.dimension()
        if dim > old_dim:
            base += [tmphash_bin]
            filelist.append(tmpfile)
            print("dimension " + str(s.dimension()))
        cnt += 1
        print(cnt)
    m = matrix(GF(2), 256, 256, base)
    m = m.transpose()
    return m, filelist

关于更加详细的解答,请参考 exp.py

这里我修改 pre-copy 多输出 !!!!come here!!!! 字眼,如下

参考文献

  • https://sectt.github.io/writeups/34C3CTF/crypto_182_software_update/Readme

  • https://github.com/OOTS/34c3ctf/blob/master/software_update/solution/exploit.py

2019 36c3 SaV-ls-l-aaS

这个题的分类是 Crypto&Web,捋一下流程:

60601端口开着一个Web服务,题目描述给了连接方法:

可以看到,先是访问 /ip 得到 ip,再向 /sign post 过去 ip 和我们要执行的命令,得到签名,最后向 /exec post signature 来执行命令。我们执行这一行可以发现回显了ls -l执行的结果,发现有个 flag.txt。

看源码,Web 服务是由 go 起的:

代码很容易看,限制了 cmd 只能是ls -l,其余不给签名,看样子我们是要伪造其他命令的签名来读flag,这里注意到签名和验签的过程是传给本地起的一个 php 来完成的,看一下这部分源码:

采用的是md5WithRSAEncryption的方式签名,本地试了一下,是把我们传入的 $d md5 后转为hex,填充到0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003020300c06082a864886f70d020505000410后面,组成数字然后用RSA签名。

看样子整个逻辑找不到一点问题,用的都是标准库,基本无法攻击。有个思路是通过代理更换 ip,可以拿到两个 ip|ls -l 的签名,这样我们就拥有了两组 RSA 的 m 和 c,因为题目给了 dockerfile 给了生成公私钥的方法,使用 openssl 默认生成,e为65537,那么我们可以通过求公因数的方式来求出 n。

在得到两组签名后,我们要得到 RSA 的m,就是填充后的数,所以按照代码逻辑,在 go 里面先是 sha1:

再 php 里的 md5,得到两组 m 和 c,但是总是求不出公因数 n,怀疑求的 m 不对。看代码发现 go 里把 sha1的结果用 json 编码,然后传到 php里 json 解码。这部分非常可疑,为何要用 json 编码(用 hex 传过去它不香么),本地搭一下环境跟一下。(题目给了dockerfile)

起个docker,改一下 index.php,加一个var_dump($d);,再改一下 go,返回一下 php 的结果:

现在让程序签名,返回结果:

$d 竟然是长度为 38 的字符串,看来果然是这里编码有问题,我们需要看一下每个步骤的结果,先看一下 go 里 json编码后的 sha1 结果是什么:

运行一下:

和正常的sha1的结果来比较一下:

由于 go 的 json 编码,很多不可见字符都被转为了 U+fffd,丢失了很多信息。

再经过 php 接口的接收,我们来看一下结果:

结果:

U+fffd变成了\xef\xbf\xbd。所以由于 go 的 json 编码问题,丢失了很多信息,造成了 md5 前的数据有很多相同字符。当时做题时往下并没有细想,得到 n 后总是想构造出任意命令的签名,也很疑惑如果构造出岂不是这种签名就不安全了?其实是无法得到的。

正解是 go 的这种问题 ,为碰撞创造了条件。我们可以碰撞出在这种编码情况下与 ls -l 有相同结果的cat * 此类命令。但是问题是我们需要非常大量 ip 来提供碰撞的数据。

可以发现,go 取 ip 的时候,是先用net.ParseIP解析了 ip,我们在 ip 每个数字前面加 0 ,解析后还是原来的 ip 结果,每个数字最多添加 256 个 0,四个数字就已经产生了 2^32种不同的组合,足以碰撞出 ls -l cat *之间的冲突。

官方题解的 c++ 碰撞脚本我本地编译的有点问题,加了一些引入的头文件:

编译可能会找不到 lcrypto,编译命令加上 lcrypto 路径(我本地是 /usr/local/opt/openssl/lib)

与 go 交互的脚本:

参考:

  • https://ctftime.org/writeup/17966

Last updated