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

区块链安全 2年前 (2021) admin
402 0 0

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


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如下图所示:

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

分析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'))
【技术分享】区块链CTF OJ平台ChainFlag -EVMEnc Writeup

- 结尾 -
精彩推荐
【技术分享】PrintSpoofer提权原理探究
【技术分享】第五空间线上赛web部分题解与模块化CTF解题工具编写的一些思考
【技术分享】绕过宝塔Getshell
【技术分享】区块链CTF OJ平台ChainFlag -EVMEnc Writeup
戳“阅读原文”查看更多内容

原文始发于微信公众号(安全客):【技术分享】区块链CTF OJ平台ChainFlag -EVMEnc Writeup

版权声明:admin 发表于 2021年10月21日 上午10:00。
转载请注明:【技术分享】区块链CTF OJ平台ChainFlag -EVMEnc Writeup | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...