【技术分享】区块链CTF OJ平台ChainFlag -EVMEnc Writeup

VSole2021-10-21 16:21:52

ChainFlag是一个区块链主题的CTF OJ平台,个人感觉现有题目质量很高,值得一做,这里分享下自己做题的过程。

EVMEnc

题目简介

题目提示简单的EVM加密,给了两个附件,info.txt与source.sol,附件如下图所示:

info.txt

transaction1.0x81200224..................2.0xffdd8131000000000000000000000000000000000000000000003100e35e552c1273c959sload(0) = 2086453827893285425773093.0xffdd8131000000000000000000000000000000000000000000001ac3243c9e81ba850045sload(0) = 293410643427570933331044.0xffdd8131000000000000000000000000000000000000000000005ce6a91010e307946b87sload(0) = 2271039174494515057851925.0xe6dc28ae..................sload(3) = 1970527074032043059410457910532573615730510348629701619382

source.sol

pragma solidity ^0.5.10;
contract EVMEnc {
    uint public result;    string public key;
    uint private delta;    uint public output;
    uint32 public sum;    uint224 private tmp_sum=0;
    uint32 private key0;    uint224 private t0=0;    uint32 private key1;    uint224  private t1=0;    uint32 private key2;    uint224 private  t2=0;    uint32  private key3;    uint224 private  t3=0;
    constructor() public {        delta = 0xb3c6ef3720;    }
    function Convert(string memory source) public pure returns (uint result) {        bytes32 tmp;        assembly {            tmp := mload(add(source, 32))        }        result = uint(tmp) / 0x10000000000000000;    }
    function set_key(string memory tmp) public {        key = tmp;    }
    function cal_(uint x) public {        uint tmp = Convert(key) / 0x10000000000000000;        result = tmp % x;    }
    function Encrypt(string memory flag) public {        uint tmp = Convert(flag);        uint key_tmp = Convert(key) / 0x10000000000000000;        assembly {            let first,second            sstore(5, and(shr(96, key_tmp), 0xffffffff))            sstore(6, and(shr(64, key_tmp), 0xffffffff))            sstore(7, and(shr(32, key_tmp), 0xffffffff))            sstore(8, and(key_tmp, 0xffffffff))
            let step := 1            for { let i := 1 } lt(i, 4) { i := add(i, 1) } {
                first := and(shr(mul(add(sub(24, mul(i, 8)), 4), 8), tmp), 0xffffffff)                second := and(shr(mul(sub(24, mul(i, 8)), 8), tmp), 0xffffffff)
                sstore(4, 0)
                for {let j := 0 } lt(j, 32) { j := add(j, 1) } {
                    sstore(4, and(add(and(sload(4), 0xffffffff), shr(5, sload(2))), 0xffffffff))
                    let tmp11 := and(add(and(mul(second, 16), 0xffffffff), and(sload(5), 0xffffffff)), 0xffffffff)                    let tmp12 := and(add(second, and(sload(4),0xffffffff)), 0xffffffff)                    let tmp13 := and(add(div(second, 32), and(sload(6),0xffffffff)), 0xffffffff)
                    first := and(add(first, xor(xor(tmp11, tmp12), tmp13)), 0xffffffff)
                    let tmp21 := and(add(and(mul(first, 16), 0xffffffff), and(sload(7),0xffffffff)), 0xffffffff)                    let tmp22 := and(add(first, and(sload(4),0xffffffff)), 0xffffffff)                    let tmp23 := and(add(div(first, 32), and(sload(8),0xffffffff)), 0xffffffff)                    second := and(add(second, xor(xor(tmp21, tmp22), tmp23)), 0xffffffff)
                }
                sstore(3, add(sload(3), add(shl(sub(192, mul(step, 32)), first), shl(sub(192, mul(i, 64)), second))))                step := add(step, 2)            }
        }    }}

题目分析

题目提供的两个附件source.sol为智能合约源码,info.txt为具体的交易序列,包含了交易的输入数据以及执行后部分存储storage的状态。利用remix编译智能合约,在Compilation Details中查看Functionhashes如下图所示:

分析info.txt文件,文件给出了5条交易的部分输入信息以及部分执行后的状态。以第一条为例:

1.0x81200224..................

给出了交易发生的输入的前4个字节为0x81200224,交易输入的前4个字节一般对应了调用方法的哈希,后面的输入为调用方法使用的参数,这里调用了方法set_key(string),但是隐去了string参数的的具体内容。

分析第二条交易信息:

2.
0xffdd8131000000000000000000000000000000000000000000003100e35e552c1273c959
sload(0) = 208645382789328542577309

对应交易的方法为cal_(unit256),参数为0x3100e35e552c1273c959。sload(0)返回的是storage[0]的信息,根据合约对应的是全局变量result,意思是执行完交易后,result=208645382789328542577309

对info.txt中的信息继续处理,结果如下所示:

transaction1.set_key(string memory tmp)2.cal_(uint 0x3100e35e552c1273c959)  result = 0x2c2eb0597447608b329d3.cal_(uint 0x1ac3243c9e81ba850045)  result = 0x6369510a41dbcbed8704.cal_(uint 0x5ce6a91010e307946b87)  result = 0x301753fa0827117d19685.Encrypt(string memory flag)output =0x505d433947f27742f60b06f350f2583450a1f7221380eeb6

到这里题目的基本要求和大题思路算是比较清楚了,题目算是一个利用solidity语言构造的一个加解密题目,题目设置未知的key值,然后告知调用三次cal_函数的结果,之后要求通过flag加密后的输出求出flag。

下面的重点在于分析cal和encrypt函数,这两个函数的编写加入了内联汇编,汇编指令的理解可以参考文档,总体而言都是对sotrage、memory以及stack的操作。本人作为一个菜狗子主要通过以下这样的方式来帮助理解,一是通过本地动态调试,题目给出了源码,可以利用remix本地部署并对相关函数进行调试,二是将用python重写函数,这样也方便了后续的解密程序编写。

通过调试可知cal函数可以理解为取余,已知:

0x2c2eb0597447608b329d = tmp % 0x3100e35e552c1273c959 0x6369510a41dbcbed870 = tmp % 0x1ac3243c9e81ba850045 0x301753fa0827117d1968 = tmp % 0x5ce6a91010e307946b87

求解取余方程可以利用中国剩余定理,具体实现附在最后。

求出了tmp后,分析Encrypt函数,先看循环前的部分:

uint tmp = Convert(flag);        uint key_tmp = Convert(key) / 0x10000000000000000;        assembly {            let first,second            sstore(5, and(shr(96, key_tmp), 0xffffffff))            sstore(6, and(shr(64, key_tmp), 0xffffffff))            sstore(7, and(shr(32, key_tmp), 0xffffffff))            sstore(8, and(key_tmp, 0xffffffff))

之前所求的tmp就是这里的key_tmp,那么存储在storage[5]到storage[8]都是固定值可以直接求出。后续部分用到的sload(2)是取storage[2]的值,按照源码分析对应的是变量delta=0xb3c6ef3720。storage[3]对应的output用来存储结果,由循环部分每次循环计算的结果移位拼接而成。将Encrypt函数重写成python,转化过程中需要注意符号的优先级,结果如下:

tmp = flag  #Convert(flag)  48 hexkey_tmp =0x6b65795f746869735f69735f6b65795fsstore5 = 0x6b65795f # 0x6b65795f 74686973 5f69735f 6b65795f >>96sstore6 = 0x74686973sstore7 = 0x5f69735fsstore8 = 0x6b65795fsstore2 = 0xb3c6ef3720sstore3 = 0step = 1sstore4listall = []//markfor i in range(1,4):    first = (tmp >> ((24 - i * 8)+4) * 8) & 0xffffffff    second = (tmp >> (24 - i * 8) * 8) & 0xffffffff    sstore4 = 0    sstore4list = []    for j in range(0, 32):        sstore4 = ((sstore4 & 0xffffffff) + (sstore2 >> 5)) & 0xffffffff        sstore4list.append(sstore4)//mark        tmp11 = (((second * 16) & 0xffffffff) + sstore5 ) & 0xffffffff        tmp12 = (second + sstore4) & 0xffffffff        tmp13 = ((second >> 5) + sstore6) & 0xffffffff        first = (first + (tmp11 ^ tmp12 ^ tmp13)) & 0xffffffff        tmp21 = (((first << 4) & 0xffffffff) + sstore7) & 0xffffffff        tmp22 = (first + sstore4) & 0xffffffff        tmp23 = ((first >> 5) + sstore8) & 0xffffffff        second = (second + (tmp21 ^ tmp22 ^ tmp23)) & 0xffffffff    sstore4listall.append(sstore4list)//mark    sstore3 = sstore3 + ((first << (192 - (step * 32))) + (second << (192 - (i * 64))))    step = step + 2print(hex(sstore3))#sstore3 = 0x505d433947f27742f60b06f350f2583450a1f7221380eeb6

由以上这段加密过程求解flag,下面分析加密算法。算法主体是两层循环,第一层循环了3次,tmp为24字节,第一次循环求出的first和second是tmp的高位的0-4字节以及5-8字节,后续循环每次取8字节前部分为first变量,后部分为second变量。通过第二层循环后将first与second再度拼接组合,循环三次后为最终的输出。

第二层循环32次,其中的sstore4为storage[4]的存储值,初始为0并且随着循环不断变化,但是在3*32次的循环中与输入的flag无关,这96个数值是固定的,这里我设立了一个数组sstore4list用来存储记录,方便后续的解密。第二层的循环中的tmp11,tmp12,tmp13三个变量仅与second有关,first依据这三个值变化,tmp21,tmp22,tmp23三个变量仅与first有关,second依据这三个值变化,循环32次后为最后得到后续拼接的first与second。

依据主要逻辑可以理解为以下形式:

tmp = [a,b,c]output =[]t = 0for i in range(0,3):    first = tmp[i][:16]    second = tmp[i][17:]    for j in range(0, 32):        tmp1 = unknow_1(second,sstore4[t])        first = (first + tmp1)& 0xffffffff        tmp2 = unknow_2(first,sstore4[t])        second = (second + tmp2)& 0xffffffff        t = t+1    output.append([first,second])

现在逻辑就清晰多了,先分组后加密在组合,类似于常见流密码的加密方式,对以上加密过程解密的逻辑可以理解为以下形式:

tmp = []output =[a,b,c]t = 0for i in range(0,3):    first = output[i][:16]    second = output[i][17:]    for j in range(0, 32):        tmp1 = unknow_1(second,sstore4[t])        first = (first - tmp1)& 0xffffffff        tmp2 = unknow_2(first,sstore4[t])        second = (second - tmp2)& 0xffffffff        t = t+1    tmp.append([first,second])

下面为了实现解密,我需要完成充实细节,比如计算出其中的用到96次的不同的sstore4,比如变量的移位拼接操作的具体实现等,实现解密即可求解出flag,实现代码附在后面。

完整解题过程

中国剩余定理求解key_tmp:

import gmpy2def crt(b,m):    for i in range(len(m)):        for j in range(i+1,len(m)):            if gmpy2.gcd(m[i],m[j]) != 1:                print("error")                return -1    M = 1    for i in range(len(m)):        M *= m[i]    Mm = []    for i in range(len(m)):        Mm.append(M // m[i])    Mm_ = []    for i in range(len(m)):        _,a,_ = gmpy2.gcdext(Mm[i],m[i])        Mm_.append(int(a % m[i]))    y = 0    for i in range(len(m)):        y += (Mm[i] * Mm_[i] * b[i])    y = y % M    return yb = [0x2c2eb0597447608b329d,0x6369510a41dbcbed870,0x301753fa0827117d1968]m = [0x3100e35e552c1273c959,0x1ac3243c9e81ba850045,0x5ce6a91010e307946b87]key = crt(b,m)print (hex(key))print (str(hex(key)[2:-1]).decode('hex'))

解密代码实现:

sstore3 = 0x505d433947f27742f60b06f350f2583450a1f7221380eeb6key_tmp =0x6b65795f746869735f69735f6b65795fsstore5 = 0x6b65795fsstore6 = 0x74686973sstore7 = 0x5f69735fsstore8 = 0x6b65795fsstore2 = 0xb3c6ef3720tmp = 0step = 1sstore4listall = []for i in range(1,4):    sstore4 = 0    sstore4list = []    for j in range(0, 32):        sstore4 = ((sstore4 & 0xffffffff) + (sstore2 >> 5)) & 0xffffffff        sstore4list.append(sstore4)    sstore4listall.append(sstore4list)
for i in range(1,4):    first = (sstore3 >> ((24 - i * 8)+4) * 8) & 0xffffffff    second = (sstore3 >> (24 - i * 8) * 8) & 0xffffffff    sstore4 = 0    for j in range(0, 32):        sstore4 = sstore4list[31 - j]        tmp21 = (((first << 4) & 0xffffffff) + sstore7) & 0xffffffff        tmp22 = (first + sstore4) & 0xffffffff        tmp23 = ((first >> 5 )+ sstore8) & 0xffffffff        second = (second - (tmp21 ^ tmp22 ^ tmp23)) & 0xffffffff        tmp11 = (((second << 4) & 0xffffffff) + sstore5) & 0xffffffff        tmp12 = (second + sstore4) & 0xffffffff        tmp13 = ((second >> 5) + sstore6) & 0xffffffff        first = (first - (tmp11 ^ tmp12 ^ tmp13)) & 0xffffffff    tmp = tmp + ((first << (192 - (step * 32))) + (second << (192 - (i * 64))))    step = step + 2print(hex(tmp))print (str(hex(tmp)[2:-1]).decode('hex'))
ctffor循环
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 DNS TXT 记录的意义可以参考下面两篇文章:TXT 记录值 - Google Workspace 管理员帮助https://support.google.com/a/answer/2716802?hl=zh-HansDNS中TXT记录是做什么用的?既然输出没有问题了,可以进行转换了,这里又涉及一个问题:certutil 只能对文件进行转换,
它指的是一个有用的工具库,帮助处理和操作XML格式的数据。ROME库允许我们把XML数据转换成Java中的对象,这样我们可以更方便地在程序中操作数据。另外,它也支持将Java对象转换成XML数据,这样我们就可以把数据保存成XML文件或者发送给其他系统。
FastJson结合二次反序列化绕过黑名单
mruby是一个Ruby语言的轻量级实现,mruby工作方式类似CPython,它可以将Ruby源码编译为字节码,再在虚拟机中解释运行。
前言本文主要着眼于glibc下的一些漏洞及利用技巧和IO调用链,由浅入深,分为 “基础堆利用漏洞及基本IO攻击” 与 “高版本glibc下的利用” 两部分来进行讲解,前者主要包括了一些glibc相关的基础知识,以及低版本glibc下常见的漏洞利用方式,后者主要涉及到一些较新的glibc下的IO调用链。
前言在Teaser Dragon CTF 2019中有2道考察Crypto方向的题目,一道Crypto题目,一道Web+Crypto题目,在这里对题目进行一下分析。
ChainFlag是一个区块链主题的CTF OJ平台,个人感觉现有题目质量很高,值得一做,这里分享下自己做题的过程。
前言随着射频识别技术的发展,射频卡被广泛应用在了门禁控制、金融支付、库存管理等场景。在此背景下,各种安全认证机制应运而生,为保护个人隐私和敏感数据提供了可靠的保障,本文将通过一道 CTF 题目介绍 M1 卡采用的 AES认证机制,揭示其背后的原理。
看雪论坛作者ID:NYSECbao
本文示例是来自corCTF 2021中 的两个内核题,由 BitsByWill 和 D3v17 所出。针对UAF漏洞,漏洞对象从kmalloc-64到kmalloc-4096,都能利用 msg_msg 结构实现任意写。
VSole
网络安全专家