PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

WriteUp 2年前 (2022) admin
530 0 0

Lockbox2 题目分析


如下图所示,首先分析setup合约:

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

 

首先其导入了一个合约,导入的lockbox2合约先暂时不管,然后setup的初始化函数是new了一个lockbox2合约,然后一个isSolved函数,这个函数view修饰,不上链,返回lockbox2合约的locked函数的返回值的非值,一个bool类型,看来拿到flag的前提是要让lockbox2的locked函数返回false。
 进入lockbox2合约,如下图所示:

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析


首先它定义了一个全局变量,bool类型locked,初始为true。然后是没有参数的solve函数。此函数首先声明了一个bool类型,长度为5的数组successes。下标从0到4分别对应5个返回值。继续看每一行,分别是调用当前合约的stage1-5函数,calldata数据为msg.data第4位开始。从第四位开始,也就是不算函数签名。然后把每个调用是否成功作为一个bool值赋给success数组。然后是一个循环遍历这个bool类型的数组,每个都是true的话,继续运行,把locked设置为false。只有solve这一个入口可以改变locked变量。看来关键问题就是solve函数的5个调用。

 那么我们一起来分析这5个调用。

Stage1:随便满足,只需要msg.data的长度<500。

Stage2:传入的参数是一个uint256数组,长度为4。在传入的msg.data的前4行,一行32个字节中,每一行表示一个数,然后这个4个数要全都满足,除了自己和本身,不能再整除任何数。这个也很好构造。

Stage3:传入的参数是3个uint256类型数据,首先调用了mstore,把b存在栈中a的位置,b表示数据,a表示栈里的位置。然后一个调用,a,b的和表示一个地址,然后staticcall(静态调用)这个地址。我们无法构造这么个地址,所以返回一定是空。需要满足返回值长度和传入的参数c相等。又根据stage2已知c一定除了自己和本身不能整除任何数,而这里返回值长度一定是0,看上去不可完成。因为注意到开始有个mstore,往内存的任何位置存储,这个mstore一定是有意义的。首先通过资料,我们了解到solidity特性,由于调用的返回值为空,所以会存入一个特殊的位置0x60(https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html#layout-in-memory)。接下来我们debug验证一下。

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

可以看到静态调用之后,mload去读了栈中0x60位置的值,所以只需要在mstore中,给指定位置存入数据,我们就可以让stage3通过。我们可以传入0x61,0x0101,0x1。首先通过mstore把0101存到0x61。因为用的是mstore,存入的是32字节数据,0x0101高位补0,补齐32字节,又因为60位置一共32字节,但是mstore从61位置开始存,整个60存不完,所以会占用80一个位置。看下图:

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

现在成功控制了60位置,mstore(a,b)在bytes memory data声明之前,又由于返回数据为空,所以会来0x60取数。所以通过mstore,控制输入参数来通过stage3。

 Stage4:stage4传入两个参数,类型都是bytes类型,首先用a作为参数创建了一个合约,合约的code(在链上保存的代码)为a,并返回一个地址。然后执行静态调用,参数为b。然后一个require判断,新生成合约的代码要等与tx.origin的公钥。这个也比较好构造。

首先我们打印一下tx.origin,发现tx.origin前面多两个0。

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

在构造公钥的时候要构造前两位是00的。

import randomfrom Crypto.Util.number import isPrimefrom ecdsa import ecdsag = ecdsa.generator_secp256k1while True:    private_key = random.randint(0, 1 << 256 - 1)    public_key = private_key * g    x = str(hex(public_key.x())[2:])    x = ("00" * 32 + x)[-32 * 2:]    y = str(hex(public_key.y())[2:])    y = ("00" * 32 + y)[-32 * 2:]    public_key_hex = x + y   if public_key_hex[:2] == "00":        print(private_key, public_key_hex)        break;

我们先用以上脚本跑一个公钥和私钥。其中公钥的前两位是0,如下面所示。

private_key:53696799650805905702178748833560284763518490362681353450771033938641345485772

public_key:00c71a98df7527e420247f8e4baa7a5e8c66108c63107c3d1c9a4cf49574cffc4f37e410c847198bafb557e5fe8ba61fa1b61a55724ebac021acf438a3961cf5

然后我们需要构造一个opcode,让公钥内容保存在链上。

PUSH1 0x40DUP1PUSH1 0x06PUSH1 0x0CODECOPYPUSH1 0RETURN


把上面的opcode和构造的公钥拼接起来。先解释一下opcode的含义,把40入栈,复制一下,然后把06,0入栈。此时,栈里的内容,从栈顶往下分别是0,0b,40,40。codecopy接收3个参数,从栈顶往下0,0b,40。分别表示目标位置,当前位置,代码长度。我们公钥的代码是64字节,所以长度0x40,因为opcode做完才是公钥的内容,所以公钥的当前位置是opcode字节码长度总和,也就是11=0x0b,我们要把公钥(代码)从0b复制到0。把0入栈,现在栈里还有0,40,接着执行return,把0到40位置的内容返回,即代码内容。这个opcode就把公钥的内容部署到链上。完整版如下所示:

604080600B6000396000f300c71a98df7527e420247f8e4baa7a5e8c66108c63107c3d1c9a4cf49574cffc4f37e410c847198bafb557e5fe8ba61fa1b61a55724ebac021acf438a3961cf


即a要传这么个bytes数组,b传0x0即可。

Stage5:还剩最后一个stage5,回调了当前合约的solve函数,但要返回失败,第一遍成功,第二遍失败,我们考虑call调用的时候控制gas。这个等之后debug的时候做,比较好处理。先构造一下数据。

首先来看一下solve的函数签名。

emit log_bytes(: 0x890d6908)

然后就是构造数据了,首先需要搞4个uint256类型的数据,满足>=1,且只能整除1和本身。上文已经构造出来了。

000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000001010000000000000000000000000000000000000000000000000000000000000001

这样构造能满足stage2,stage3,理由上文已说明。对于接下来stage4,因为目前只有3个整数,缺少一个,而不能满足stage2,所以还要再搞一个数。

首先搞清一下bytes字符串在内存中的存法,虽然stage4让传2个字符串,但有用的只有a。字符串是先存长度,再存内容。第一行的msg.data表示偏移量,所以我们接下来的构造要去第4行,正好跟stage2的第4个参数冲突。因为从61位置开始存,所以第4行肯定存不满,而且后面的数据我们可以后面补0,所以我们构造一个长度为0100的,让其在第4行的表现为01,满足stage2。

0000000000000000000000000000000000000000000000000000000000000061 000000000000000000000000000000000000000000000000000000000000000101 200000000000000000000000000000000000000000000000000000000000000001 400000000000000000000000000000000000000000000000000000000000000001 6000

我们只需要前面这样构造,满足stage1,2,3。Stage4所需要的数据我们abi编码一下,后面补0,补齐到0100字节即可。完整数据如下:

890d6908000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000001010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100604080600B6000396000f300c71a98df7527e420247f8e4baa7a5e8c66108c63107c3d1c9a4cf49574cffc4f37e410c847198bafb557e5fe8ba61fa1b61a55724ebac021acf438a3961cf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000


通过合约call调用的方式控制,传入我们的msg.data,现在我们只需要找到stage5的gas花费,如下图所示:

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

我们直接在这个地方打印一下gas使用,结果如下。

409548

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

暴力了一下,找的有点麻烦,不如直接暴力搞。

PARADIGM CTF 2022题目分析(3)-Lockbox2 分析


PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

然后我们看到locked在执行了solve之后变成了false。从而顺利拿到flag


敬请期待更多有关PARADIGM CTF 2022题目分析。


PARADIGM CTF 2022题目分析(3)-Lockbox2 分析


原文始发于微信公众号(Numen Cyber Labs):PARADIGM CTF 2022题目分析(3)-Lockbox2 分析

版权声明:admin 发表于 2022年8月29日 下午6:48。
转载请注明:PARADIGM CTF 2022题目分析(3)-Lockbox2 分析 | CTF导航

相关文章

暂无评论

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