crypto(EVMEnc)
contract
1 | pragma solidity ^0.5.10; |
info.txt
1 | transaction |
analyses
0.
1 | function Convert(string memory source) public pure returns (uint result) { |
add(source, 32)
: Move the pointer of the source string to the start position of its actual data (in Solidity, the first 32 bytes of the string store its length)mload(add(source, 32))
: Load 32 bytes from memory, so tmp now contains the first 32 bytes of the source stringresult = uint(tmp) / 0x10000000000000000
: This converts tmp to an unsigned integer and performs a division operation. 0x1000000000000000 is the 16th power of 16, which is equivalent to shifting the 256 bit byte sequence to the right by 64 bits. This converts the original bytes32 variable to the first 192 bits of uint. If the length of the input string exceeds 24 bytes (because each byte has 8 bits, 24 bytes are equal to 192 bits), the function will discard the excess.
1
1 | 0x81200224.................. |
It is obvious that it is a function selector, it is the result of keccak256("set_key(string)");
:
1 | function set_key(string memory tmp) public { |
we only know the msg.sender call the set_key()
, but he hides the parameter.
2.
1 | 2. |
The 2.3.4 of the info.txt is the result of keccak256("cal_(uint256)");
:
1 | function cal_(uint x) public { |
sload(0) returns the content of slot(0), it is result
in this contract.
3.
1 | 5. |
0xe6dc28ae
is the result of keccak256("Encrypt(string)")
. sload(3) returns the content of slot(3), it is output
in this contract.
4.
Now we know what happened in the info.txt:
1 | transaction |
This level is an encryption and decryption question constructed using Solidity language. The question sets an unknown key value and then calls cal_
three times. In the end, we need to get the parameter of Encrypt by the output.
5.
The cal_
can be understood as taking remainder:
1 | 0x2c2eb0597447608b329d = tmp % 0x3100e35e552c1273c959 |
We can infer tmp
by “中国剩余定律”, the result is: tmp=0x6b65795f746869735f69735f6b65795f
6.
Let’s analyses Encrypt()
:
1 | uint tmp = Convert(flag); |
uint key_tmp = Convert(key) / 0x10000000000000000;
: this is the value of tmp we calculate before: 0x6b65795f746869735f69735f6b65795fsstore(5, and(shr(96, key_tmp), 0xffffffff))
: storage[5] := v, it equal to that:
1 | v = (0x6b65795f746869735f69735f6b65795f >> 96) & 0xffffffff |
sstore 6,7,8 is the same theory as sstore 5, in the end, we get:
1 | tmp = flag #Convert(flag) 48 hex |
7.
之前所求的tmp就是这里的key_tmp,那么存储在storage[5]到storage[8]都是固定值可以直接求出。后续部分用到的sload(2)是取storage[2]的值,按照源码分析对应的是变量delta=0xb3c6ef3720。storage[3]对应的output用来存储结果,由循环部分每次循环计算的结果移位拼接而成。将Encrypt函数重写成python,转化过程中需要注意符号的优先级,结果如下:
1 | tmp = flag #Convert(flag) 48 hex |
8.
由以上这段加密过程求解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。
依据主要逻辑可以理解为以下形式:
1 | tmp = [a,b,c] |
9.
现在逻辑就清晰多了,先分组后加密在组合,类似于常见流密码的加密方式,对以上加密过程解密的逻辑可以理解为以下形式:
1 | tmp = [] |
总结:获取slot3的数值output,它是调用Encrypt()
的结果,我们需要做的是根据这个结果来反推它的输入key。那么,我们就要理解加密干了些什么,然后根据加密方式,倒推出解密方式,从而获得key值
solve
中国剩余定理求解key_tmp:
1 | class Numbertheory(): |
1 | moudle = [0x3100e35e552c1273c959, 0x1ac3243c9e81ba850045, 0x5ce6a91010e307946b87] |
解密代码实现:
1 | sstore3 = 0x505d433947f27742f60b06f350f2583450a1f7221380eeb6 |