CTF Wiki
  • 简介
  • 如何使用 CTF Wiki
  • introduction
    • CTF 历史
    • CTF 竞赛模式简介
    • CTF 竞赛内容
    • 线下攻防经验小结
    • CGC 超级挑战赛
    • 学习资源
  • misc
    • 杂项简介
    • 取证隐写前置技术
    • 信息搜集技术
    • encode
      • 通信领域常用编码
      • 计算机相关的编码
      • 现实世界中常用的编码
    • picture
      • 图片分析简介
      • JPG
      • PNG
      • GIF
    • audio
      • 音频隐写
    • archive
      • RAR 格式
      • ZIP 格式
    • traffic
      • 流量包分析简介
      • 协议分析概述
        • Wireshark
        • DNS
        • HTTP
        • HTTPS
        • FTP
        • USB
        • WIFI
      • 数据提取
      • PCAP 文件修复
    • disk-memory
      • 磁盘内存分析
      • 题目
    • shellcode
    • other
      • pyc
  • web
    • Web 简介
    • XSS
    • php
      • PHP 代码审计
    • SQL 注入
      • sqlmap绕过脚本
      • 各版本数据库语句备忘
    • CSRF
    • SSRF
  • reverse
    • 软件逆向工程简介
    • identify-encode-encryption
      • 常见加密算法和编码识别
    • language
      • 简介
      • go
        • Golang 逆向入门
      • python
        • Python 逆向入门
      • rust
        • Rust 逆向入门
    • maze
      • 迷宫问题
    • obfuscate
      • 控制流平坦化
      • 花指令
      • movofuscator
      • Self-Modified Code
    • vm
      • 虚拟机分析
    • platform
      • linux
        • Detecting Breakpoints
        • Detecting debugging
        • False Disassembly
        • LD_PRELOAD
      • windows
        • anti-debug
          • CheckRemoteDebuggerPresent
          • 反调试技术例题
          • Heap Flags
          • Interrupt 3
          • IsDebuggerPresent
          • 花指令
          • NtGlobalFlag
          • NtQueryInformationProcess
          • The Heap
          • Thread Local Storage(TLS)
          • ZwSetInformationThread
        • unpack
          • 一步到达 OEP 法
          • ESP 定律法
          • DUMP 及 IAT 重建
          • 最后一次异常法
          • 手动查找 IAT 并使用 ImportREC 重建
          • 内存镜像法
          • 保护壳简介
          • SFX 法
          • 单步跟踪法
          • DLL 文件脱壳
    • tools
      • constraint
        • z3
      • debug
        • gdb
        • ollydbg
        • windbg
        • x64dbg/x32dbg
      • simulate-execution
        • angr
        • Unicorn Engine
      • static-analyze
        • dnspy
        • Ghidra
        • IDA Pro
        • jadx
  • crypto
    • 密码学简介
    • asymmetric
      • 介绍
      • discrete-log
        • 离散对数
        • ECC
        • ElGamal
      • knapsack
        • 背包加密
      • lattice
        • CVP
        • 基本介绍
        • 格基规约算法
        • 格概述
      • rsa
        • RSA 选择明密文攻击
        • RSA 复杂题目
        • Coppersmith 相关攻击
        • 公钥指数相关攻击
        • 模数相关攻击
        • Bleichenbacher's attack
        • RSA 侧信道攻击
        • RSA 介绍
        • d_attacks
          • 私钥 d 相关攻击
          • 扩展维纳攻击
    • attack-summary
      • 简介
      • 比特攻击
      • 中间相遇攻击 - MITM
    • basic
      • 基础数学知识
    • blockcipher
      • AES
      • ARX: Add-Rotate-Xor
      • DES
      • IDEA
      • 块加密
      • Simon and Speck Block Ciphers
      • mode
        • CBC
        • CFB
        • CTR
        • ECB
        • 分组模式
        • OFB
        • Padding Oracle Attack
        • 填充方式
        • PCBC
    • certificate
      • 证书格式
    • classical
      • 古典密码简介
      • 单表代换加密
      • 其它类型加密
      • 多表代换加密
      • 总结
    • hash
      • Hash Attack
      • 综合题目
      • Fowler–Noll–Vo hash function
      • 哈希函数
      • MD5
      • SHA1
    • signature
      • DSA
      • ElGamal
      • 数字签名
      • RSA 数字签名
    • streamcipher
      • 流密码
      • fsr
        • 反馈移位寄存器
        • 线性反馈移位寄存器 - LFSR
        • 非线性反馈移位寄存器
      • lcg
        • 题目
        • 线性同余生成器
      • prng
        • 密码安全伪随机数生成器
        • 伪随机数生成器介绍
        • 题目
      • special
        • RC4
  • pwn
    • MacOS
    • misc-os
    • 概述
      • stackoverflow
        • 执行 Shellcode
        • 栈介绍
        • 栈溢出原理
    • browser
      • Chrome
      • Firefox
      • Safari
    • hardware
      • 简介
        • side-channel
          • prefetch side-channel attack
      • trusted-computing
        • 可信执行环境
    • linux
      • kernel-mode
        • 基础知识
        • Introduction
          • DoS
          • Information Disclosure
          • Introduction
            • Change Others
            • Change Self
        • Introduction
          • Introduction
            • 信息泄漏
            • Misc
          • Introduction
            • Kernel Stack Canary
          • Introduction
            • inner-kernel
              • 内部隔离
            • Introduction
              • KPTI - Kernel Page Table Isolation
              • 用户代码不可执行
              • 用户数据不可访问
          • Introduction
            • FGKASLR
            • KASLR
        • Introduction
          • 编译内核驱动
          • 内核下载与编译
          • Qemu 模拟环境
          • Real Device
        • exploitation
          • heap
            • 内核堆概述
            • buddy
              • Cross-Cache Overflow & Page-level Heap Fengshui
              • Page-level UAF
            • slub
              • freelist 劫持
              • Heap Spray
              • kernel UAF
          • race
            • Double Fetch
            • userfaultfd 的使用
          • rop
            • bypass-smep
            • ret2dir
            • 利用 pt_regs 构造通用内核 ROP
            • ret2usr(已过时)
            • Kernel ROP
          • tricks
            • 在内存中直接搜索 flag
      • user-mode
        • environment
        • fmtstr
          • 检测
          • 例子
          • 利用
          • 原理介绍
        • integeroverflow
          • 整数溢出
        • io-file
          • glibc 2.24下 IO_FILE 的利用
          • 伪造vtable劫持程序流程
          • FSOP
          • FILE结构
        • mitigation
          • Canary
        • race-condition
          • introduction
          • 题目
        • summary
          • 获取地址
          • shell 获取小结
          • 控制程序执行流
        • Type Confusion
        • Uninitialized Memory
        • heap
          • mallocng
          • ptmalloc2
            • Chunk Extend and Overlapping
            • Fastbin Attack
            • 堆概述
            • 堆相关数据结构
            • 堆溢出
            • House Of Einherjar
            • House Of Force
            • House of Lore
            • House of Orange
            • House of Pig
            • House of Rabbit
            • House of Roman
            • 堆利用
            • Large Bin Attack
            • 通过堆进行信息泄漏
            • 堆中的 Off-By-One
            • 堆中的检查
            • tcache makes heap exploitation easy again
            • Unlink
            • Unsorted Bin Attack
            • Use After Free
            • implementation
              • 基础操作
              • 释放内存块
              • 堆初始化
              • malloc_state 相关函数
              • 申请内存块
              • 测试支持
              • 深入理解堆的实现
              • tcache
        • stackoverflow
          • arm
            • 环境搭建
            • Arm ROP
          • mips
            • mips - ROP
          • RISC-V
          • x86
            • 基本 ROP
            • 花式栈溢出技巧
            • 中级ROP
            • 栈介绍
            • 栈溢出原理
            • advanced-rop
              • 高级 ROP
              • ret2dlresolve
              • ret2VDSO
              • SROP
    • sandbox
      • Chroot
      • Docker
      • Namespace
      • python
        • Python 沙盒
      • seccomp
        • C 沙盒逃逸
      • Shell Sandbox
    • virtualization
      • basic-knowledge
        • 虚拟化技术简介
        • CPU 虚拟化
        • IO 虚拟化
        • 内存虚拟化
      • parallels
        • Parallels
      • VirtualBox
      • VMWare
      • qemu
        • basic-knowledge
          • QEMU 设备模拟
          • QEMU 内存管理
        • environment
          • 编写 QEMU 模拟设备
          • QEMU 下载与编译
        • exploitation
          • QEMU 逃逸入门
          • 越界读写
  • Android 安全
    • basic_develop
      • Android 开发基础
    • Android 应用运行机制简述
      • Android 中 Java 层的运行机制
        • dex
          • DEX文件
          • ODEX文件
        • smali
          • Smali
      • native_layer
        • so 介绍
    • basic_reverse
      • Android 关键代码定位
      • Android 逆向基本介绍
      • dynamic
        • Android 动态调试
        • IDA 动态调试原生层程序
        • IDA 动态调试 smali 代码
      • static
        • 静态分析综合题目
        • 静态分析 java 层例子
        • 静态分析原生层程序
  • blockchain
    • Blockchain Security Challenges
    • Blockchain Security Overview
    • ethereum
      • Ethereum Basics
      • Ethereum Overview
      • Ethereum Opcodes
      • 学习资源
      • Smart Contract Reverse
      • Function Selector and Argument Encoding
      • Ethereum Storage
      • attacks
        • Airdrop Hunting
        • Arbitrary Writing
        • CREATE2
        • Delegatecall
        • Introduction
        • Jump Oriented Programming
        • Integer Overflow and Underflow
        • Randomness
        • Re-Entrancy
        • Short Address Attack
        • Uninitialized Storage Pointer
    • publicblockchain
      • Public Blockchain Security Overview
      • Blockchain Weaknesses
  • assembly
    • ARM
    • MIPS
    • x86_x64
  • executable
    • elf
      • 程序加载
      • 程序执行流程
      • linking
        • 程序链接
        • Symbol Reslove
      • structure
        • ELF 文件
        • Code Section
        • Data Related Sections
        • Dynamic Sections
        • Misc Sections
        • Sections
        • String Sections
        • .symtab: Symbol Table
    • pe
      • PE 文件格式
      • 导出表
      • 导入表
      • 基址重定位表
  • ics
    • ICS_CTF 竞赛
    • ICS_CTF 发现
    • ICS_CTF 利用
    • ICS_CTF 学习资源
  • contribute
    • 贡献之前
    • 基本贡献方式
    • 贡献文档要求
    • 翻译
  • write up
    • 浙江工业大学CTF赛事
      • 2023第四届“安恒杯”CTF新生赛题解
Powered by GitBook
On this page
  • 2017 34c3 Software_update
  • 2019 36c3 SaV-ls-l-aaS
  1. crypto
  2. hash

综合题目

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)(F={0,1},F256,⊕,⋅)

是一个 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!!!! 字眼,如下

➜  software_update git:(master) python3 installer.py now.zip
Preparing to copy data...
!!!!come here!!!!
Software update installed successfully.

参考文献

  • 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服务,题目描述给了连接方法:

url='http://78.47.240.226:60601' && ip=$(curl -s "$url/ip") && sig=$(curl -s -d "cmd=ls -l&ip=$ip" "$url/sign") && curl --data-urlencode "signature=$sig" "$url/exec"

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

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

package main

import (
	"bytes"
	"crypto/sha1"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"strings"
	"time"
)

func main() {
	m := http.NewServeMux()

	m.HandleFunc("/ip", func(w http.ResponseWriter, r *http.Request) {
		ip, _, err := net.SplitHostPort(r.RemoteAddr)
		if err != nil {
			return
		}
		fmt.Fprint(w, ip)
	})

	m.HandleFunc("/sign", func(w http.ResponseWriter, r *http.Request) {
		ip, _, err := net.SplitHostPort(r.RemoteAddr)
		if err != nil {
			return
		}
		remoteAddr := net.ParseIP(ip)
		if remoteAddr == nil {
			return
		}

		ip = r.PostFormValue("ip")
		signIP := net.ParseIP(ip)
		if signIP == nil || !signIP.Equal(remoteAddr) {
			fmt.Fprintln(w, "lol, not ip :>")
			return
		}

		cmd := r.PostFormValue("cmd")
		if cmd != "ls -l" {
			fmt.Fprintln(w, "lol, nope :>")
			return
		}

		msg := ip + "|" + cmd
		digest := sha1.Sum([]byte(msg))

		b := new(bytes.Buffer)
		err = json.NewEncoder(b).Encode(string(digest[:]))
		if err != nil {
			return
		}

		resp, err := http.Post("http://127.0.0.1/index.php?action=sign", "application/json; charset=utf-8", b)
		if err != nil || resp.StatusCode != 200 {
			fmt.Fprintln(w, "oops, hsm is down")
			return
		}

		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			fmt.Fprintln(w, "oops, hsm is bodyless?")
			return
		}

		var signature string
		err = json.Unmarshal(body, &signature)
		if err != nil {
			fmt.Fprintln(w, "oops, hsm is jsonless?")
			return
		}

		fmt.Fprint(w, signature+msg)
	})

	m.HandleFunc("/exec", func(w http.ResponseWriter, r *http.Request) {
		ip, _, err := net.SplitHostPort(r.RemoteAddr)
		if err != nil {
			return
		}
		remoteAddr := net.ParseIP(ip)
		if remoteAddr == nil {
			return
		}

		signature := r.PostFormValue("signature")
		digest := sha1.Sum([]byte(signature[172:]))

		b := new(bytes.Buffer)
		err = json.NewEncoder(b).Encode(signature[:172] + string(digest[:]))
		if err != nil {
			fmt.Fprintln(w, "oops, json encode")
			return
		}

		resp, err := http.Post("http://127.0.0.1/index.php?action=verify", "application/json; charset=utf-8", b)
		if err != nil || resp.StatusCode != 200 {
			fmt.Fprintln(w, "oops, hsm is down?")
			return
		}

		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			fmt.Fprintln(w, "oops, hsm is bodyless?")
			return
		}

		var valid bool
		err = json.Unmarshal(body, &valid)
		if err != nil {
			fmt.Fprintln(w, "oops, json unmarshal")
			return
		}

		if valid {
			t := strings.Split(signature[172:], "|")
			if len(t) != 2 {
				fmt.Fprintln(w, "oops, split")
			}

			signIP := net.ParseIP(t[0])
			if signIP == nil || !signIP.Equal(remoteAddr) {
				fmt.Fprintln(w, "lol, not ip :>")
				return
			}

			conn, err := net.DialTimeout("tcp", "127.0.0.1:1024", 1*time.Second)
			if err != nil {
				fmt.Fprintln(w, "oops, dial")
				return
			}
			fmt.Fprintf(conn, t[1]+"\n")
			conn.(*net.TCPConn).CloseWrite()
			io.Copy(w, conn)
		}
	})

	s := &http.Server{
		Addr:           ":60601",
		Handler:        m,
		ReadTimeout:    5 * time.Second,
		WriteTimeout:   5 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	log.Fatal(s.ListenAndServe())
}

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

<?php
define('ALGO', 'md5WithRSAEncryption');
$d = json_decode(file_get_contents('php://input'), JSON_THROW_ON_ERROR);

if ($_GET['action'] === 'sign'){
    $pkeyid = openssl_pkey_get_private("file:///var/www/private_key.pem");
    openssl_sign($d, $signature, $pkeyid, ALGO);
	echo json_encode(base64_encode($signature));
    openssl_free_key($pkeyid);
}
elseif ($_GET['action'] === 'verify') {
    $pkeyid = openssl_pkey_get_public("file:///var/www/public_key.pem");
    echo json_encode(openssl_verify(substr($d, 172), base64_decode(substr($d,0, 172)), $pkeyid, ALGO) === 1);
    openssl_free_key($pkeyid);
}

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

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

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

msg := ip + "|" + cmd
digest := sha1.Sum([]byte(msg))

b := new(bytes.Buffer)
err = json.NewEncoder(b).Encode(string(digest[:]))

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

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

fmt.Fprintln(w,string(body))

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

string(38) "	��.���?-�KC��@�"
"K4FEmxz4yuTsjDAbRZQmHJ+MBiCSGaOnpZTLbThXpCkDYe3siAIPfihX6ppjN2Tz6XqOr4tF\/u1\/+ccfhj8NNLIL+2hknyDXbosmMBV8mEGYsMqQHAE0f+3OhDWlzN5RnteSMYNZbTipFErB8ZOWCiXmynWxsqJhyaN9J6\/\/h6I="
oops, hsm is jsonless?

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

package main

import (
	"bytes"
	"crypto/sha1"
	"encoding/json"
	"fmt"
)
func main() {
	msg := "172.17.0.1|ls -l"
	digest := sha1.Sum([]byte(msg))

	b := new(bytes.Buffer)
	json.NewEncoder(b).Encode(string(digest[:]))
	fmt.Print(string(b.Bytes()));
}

运行一下:

"\u000e\t\u001d\ufffd\u0012\ufffd.\ufffd\ufffd\ufffd?-\ufffdKC\ufffd\u0005\ufffd@\ufffd"

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

Python 2.7.16 (default, Sep  2 2019, 11:59:44)
[GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> "\u000e\t\u001d\ufffd\u0012\ufffd.\ufffd\ufffd\ufffd?-\ufffdKC\ufffd\u0005\ufffd@\ufffd"
'\\u000e\t\\u001d\\ufffd\\u0012\\ufffd.\\ufffd\\ufffd\\ufffd?-\\ufffdKC\\ufffd\\u0005\\ufffd@\\ufffd'
>>> from hashlib import *
>>> sha1('172.17.0.1|ls -l').digest()
'\x0e\t\x1d\xbd\x12\x90.\xca\xf0\xd9?-\x98KC\xeb\x05\xa1@\xd1'

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

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

$d = json_decode(file_get_contents('php://input'), JSON_THROW_ON_ERROR);
var_dump(file_get_contents('php://input'));
var_dump($d);
var_dump(bin2hex($d));

结果:

string(89) ""\u000e\t\u001d\ufffd\u0012\ufffd.\ufffd\ufffd\ufffd?-\ufffdKC\ufffd\u0005\ufffd@\ufffd"
"
string(38) "	��.���?-�KC��@�"
string(76) "0e091defbfbd12efbfbd2eefbfbdefbfbdefbfbd3f2defbfbd4b43efbfbd05efbfbd40efbfbd"
"K4FEmxz4yuTsjDAbRZQmHJ+MBiCSGaOnpZTLbThXpCkDYe3siAIPfihX6ppjN2Tz6XqOr4tF\/u1\/+ccfhj8NNLIL+2hknyDXbosmMBV8mEGYsMqQHAE0f+3OhDWlzN5RnteSMYNZbTipFErB8ZOWCiXmynWxsqJhyaN9J6\/\/h6I="
oops, hsm is jsonless?

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++ 碰撞脚本我本地编译的有点问题,加了一些引入的头文件:

// g++ -std=c++17 -march=native -O3 -lcrypto -lpthread gewalt.cpp -o gewalt

#include <cassert>
#include <iomanip>
#include <string>
#include <sstream>
#include <iostream>
#include <functional>
#include <random>
#include <unordered_map>
#include <algorithm>
#include <thread>
#include <atomic>
#include <mutex>
#include <array>
#include <openssl/sha.h>

const unsigned num_threads = std::thread::hardware_concurrency();



static std::string hash(std::string const& s)
{
    SHA_CTX ctx;
    if (!SHA1_Init(&ctx)) throw;
    if (!SHA1_Update(&ctx, s.data(), s.length())) throw;
    std::string d(SHA_DIGEST_LENGTH, 0);
    if (!SHA1_Final((uint8_t *) &d[0], &ctx)) throw;
    return d;
}

static std::u32string kapot(std::string const& s)
{
    std::u32string r(s.size(), 0);
    size_t o = 0;

    for (size_t i = 0; i < s.length(); ) {

        auto T = [](uint8_t c) {
            return (c < 0x80)         ? 1   /* ASCII */
                 : (c & 0xc0) == 0x80 ? 0   /* continuation */
                 : (c & 0xe0) == 0xc0 ? 2   /* 2-byte chunk */
                 : (c & 0xf0) == 0xe0 ? 3   /* 3-byte chunk */
                 : (c & 0xf8) == 0xf0 ? 4   /* 4-byte chunk */
                 : -1;
        };

        uint32_t c = s[i++];
        auto cont = [&]() { c = (c << 6) | (s[i++] & 0x3f); };

        switch (T(c)) {

        case -1:
        case  0:
        invalid: c = 0xfffd; /* fall through */

        case  1:
        valid:   r[o++] = c; break;

        case  2:
                 if (c &= 0x1f, i+0 >= s.size() || T(s[i+0]))
                     goto invalid;
                 goto one;

        case  3:
                 if (c &= 0x1f, i+1 >= s.size() || T(s[i+0]) || T(s[i+1]))
                     goto invalid;
                 goto two;

        case  4:
                 if (c &= 0x1f, i+2 >= s.size() || T(s[i+0]) || T(s[i+1]) || T(s[i+2]))
                     goto invalid;
                 cont();
        two:     cont();
        one:     cont();
                 goto valid;

        }

    }

    r.resize(o);

    return r;
}

std::atomic<uint64_t> hcount = 0, kcount = 0;
typedef std::unordered_map<std::u32string, std::string> tab_t;
tab_t tab0, tab1;
std::mutex mtx;

std::array<uint8_t,4> ip;
std::string cmd0, cmd1;

class stuffer_t
{
    private:
        std::array<size_t,4> cnts;
        size_t step;
        std::string cmd;
    public:
        stuffer_t(size_t t, size_t s, std::string c) : cnts{t}, step(s), cmd(c) {}
        std::string operator()()
        {
            //XXX this is by far not the most efficient way of doing this, but yeah
            if (++cnts[3] >= cnts[0]) {
                cnts[3] = 0;
                if (++cnts[2] >= cnts[0]) {
                    cnts[2] = 0;
                    if (++cnts[1] >= cnts[0]) {
                        cnts[1] = 0;
                        cnts[0] += step;
                    }
                }
            }
            std::stringstream o;
            for (size_t i = 0; i < 4; ++i)
                o << (i ? "." : "")
                  << std::string(cnts[i], '0')
                  << (unsigned) ip[i];
            o << "|" << cmd;
            return o.str();
        }
};

void go(size_t tid)
{
    //XXX tid stuff is a hack, but YOLO

    bool one = tid & 1;

    stuffer_t next(tid >> 1, (num_threads + 1) >> 1, one ? cmd1 : cmd0);

    tab_t& mytab = one ? tab1 : tab0;
    tab_t& thtab = one ? tab0 : tab1;

    uint64_t myhcount = 0, mykcount = 0;

    while (1) {

        std::string r = next();

        {

            ++myhcount;

            auto h = hash(r);
            if ((h.size()+3)/4 < (size_t) std::count_if(h.begin(), h.end(),
                                            [](unsigned char c) { return c < 0x80; }))
                continue;

            ++mykcount;

            auto k = kapot(h);
            if (k.size() > 3 + (size_t) std::count(k.begin(), k.end(), 0xfffd))
                continue;

            std::lock_guard<std::mutex> lck(mtx);

            hcount += myhcount, myhcount = 0;
            kcount += mykcount, mykcount = 0;

            if (thtab.find(k) != thtab.end()) {

                mytab[k] = r;

                std::cerr << "\r\x1b[K"
                          << "\x1b[32m";
                std::cout << tab0[k] << std::endl
                          << tab1[k] << std::endl;
                std::cerr << "\x1b[0m";

                std::cerr << std::hex;
                bool first = true;
                for (uint32_t c: k)
                    std::cerr << (first ? first = false, "" : " ") << c;
                std::cerr << std::endl;

                std::cerr << std::dec << "hash count:  \x1b[35m" << hcount << "\x1b[0m";
                {
                    std::stringstream s;
                    s << std::fixed << std::setprecision(2) << log(hcount|1)/log(2);
                    std::cerr << " (2^\x1b[35m" << std::setw(5) << s.str() << "\x1b[0m" << ")" << std::endl;
                }
                std::cerr << "kapot count: " << "\x1b[35m" << kcount << "\x1b[0m";
                {
                    std::stringstream s;
                    s << std::fixed << std::setprecision(2) << log(kcount|1)/log(2);
                    std::cerr << " (2^\x1b[35m" << std::setw(5) << s.str() << "\x1b[0m)" << std::endl;
                }
                std::cerr << "table sizes: \x1b[35m"
                          << tab0.size() << "\x1b[0m \x1b[35m"
                          << tab1.size() << "\x1b[0m" << std::endl;

                exit(0);

            }

            if (mytab.size() < (1 << 20))
                mytab[k] = r;

        }

        hcount += myhcount;
        kcount += mykcount;

    }
}

void status()
{
    while (1) {

        {
            std::lock_guard<std::mutex> lck(mtx);

            std::cerr << "\r\x1b[K";
            std::cerr << "hash count: \x1b[35m" << std::setw(12) << hcount << "\x1b[0m ";
            {
                std::stringstream s;
                s << std::fixed << std::setprecision(2) << log(hcount|1)/log(2);
                std::cerr << "(2^\x1b[35m" << std::setw(5) << s.str() << "\x1b[0m) | ";
            }
            std::cerr << "kapot count: \x1b[35m" << std::setw(12) << kcount << "\x1b[0m ";
            {
                std::stringstream s;
                s << std::fixed << std::setprecision(2) << log(kcount|1)/log(2);
                std::cerr << "(2^\x1b[35m" << std::setw(5) << s.str() << "\x1b[0m) | ";
            }
            std::cerr << "tables: \x1b[35m"
                      << std::setw(9) << tab0.size() << " "
                      << std::setw(9) << tab1.size() << "\x1b[0m "
                      << std::flush;
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main(int argc, char **argv)
{

    if (argc < 2) {
        std::cerr << "\x1b[31mneed IPv4 in argv[1]\x1b[0m" << std::endl;
        exit(1);
    }
    {
        std::stringstream ss(argv[1]);
        for (auto& v: ip) {
            std::string s;
            std::getline(ss, s, '.');
            int n = std::atoi(s.c_str());
            if (n < std::numeric_limits<uint8_t>::min() || n > std::numeric_limits<uint8_t>::max())
                goto bad_ip;
            v = n;
        }
        if (!ss) {
bad_ip:
            std::cerr << "\x1b[31mbad IPv4 given?\x1b[0m" << std::endl;
            exit(2);
        }
    }


    if (argc < 4) {
        std::cerr << "\x1b[31mneed commands in argv[2] and argv[3]\x1b[0m" << std::endl;
        exit(2);
    }
    cmd0 = argv[2];
    cmd1 = argv[3];


    std::thread status_thread(status);
    std::vector<std::thread> ts;
    for (unsigned i = 0; i < num_threads; ++i)
        ts.push_back(std::thread(go, i));
    for (auto& t: ts)
        t.join();

}

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

g++ -std=c++17 -march=native -O3 -lcrypto -lpthread gewalt.cpp -o gewalt -L/usr/local/opt/openssl/lib

与 go 交互的脚本:

#!/usr/bin/env python3
import sys, requests, subprocess

benign_cmd = 'ls -l'
exploit_cmd = 'cat *'

ip, port = sys.argv[1], sys.argv[2]
url = 'http://{}:{}'.format(ip, port)

my_ip = requests.get(url + '/ip').text
print('[+] IP: ' + my_ip)

o = subprocess.check_output(['./gewalt', my_ip, benign_cmd, exploit_cmd])
print('[+] gewalt:' + o.decode())

payload = {}
for l in o.decode().splitlines():
    ip, cmd = l.split('|')
    payload['benign' if cmd == benign_cmd else 'pwn'] = ip, cmd

print(payload)

sig  = requests.post(url + '/sign', data={'ip': payload['benign'][0], 'cmd': payload['benign'][1]}).text
print('[+] sig: ' + sig)

r = requests.post(url + '/exec', data={'signature': sig[:172] + payload['pwn'][0]  + '|' + payload['pwn'][1]})
print(r.text)
 ⚙  SaV-ls-l-aaS  python solve.py 127.0.0.1 60601
[+] IP: 172.17.0.1
fffd fffd fffd fffd fffd fffd 55 fffd fffd fffd fffd c fffd fffd fffd fffd fffd fffd fffd fffd
hash count:  168104875 (2^27.32)
kapot count: 3477222 (2^21.73)
table sizes: 8745 8856
[+] gewalt:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017.000000000000000000000000000000000000000000000000000000000000000000000000000000000.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001|ls -l
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.17.000000000000000000000000.0000000000000000000000000000000000000001|cat *

{'pwn': (u'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.17.000000000000000000000000.0000000000000000000000000000000000000001', u'cat *'), 'benign': (u'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017.000000000000000000000000000000000000000000000000000000000000000000000000000000000.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', u'ls -l')}
[+] sig: ODxSukwtu4rHICBpzT23WGD7DCJNawhA0DUN/tcyv1AgwNmS8OPUnO5FnBBDgiaVx5OTYd4OjH8LVbKiXUBUBuFx1OHDgKBKG5umkKMLt+350SlgMWY5qWny9tPIU3I+X0A9FcADCBCi6f0PkXfc0CSCZXuFu9rAKnVGsbmaUwY=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017.000000000000000000000000000000000000000000000000000000000000000000000000000000000.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001|ls -l
hxp{FLAG}

参考:

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

PreviousHash AttackNextFowler–Noll–Vo hash function

Last updated 1 year ago