ctf 区块链 出题指南 solidity篇
前言:
区块链方向已然成为ctf一大主流的竞赛内容,故写此文章来分享如何出eth相关的题目,主要面向solidity,move语言暂时不在本文章讨论范围内,后续看心情编写。本文只介绍如何在公链和私链部署智能合约题目。
在测试链上部署智能合约:
首先谈一下测试链的问题,以及为什么逐渐被淘汰了,首先的问题就是测试链不防上车,一个选手解出来后通过eth的浏览器可以轻松的看见该选手的exp内容,可以快速实现上车的目的
接下来浅谈一下如何在测试链上部署题目
关于测试币如何领取可以参考之前的文章:Goerli与Sepolia,mumbai测试币领取-魔法少女雪殇 (snowywar.top)
关于测试链上合约编写特点
由于测试链上环境共享,每个选手在做题时独立来部署合约不现实(现在可以实现了,后续介绍),所以通用办法是写一个mapping来记录每个选手所交互的地址是否满足条件,如果满足条件则触发emit事件(emit可以由后台脚本进行监听)来决定是否进行发送相关的flag内容等。
案例演示:
编写智能合约部署在测试网:
pragma solidity ^0.8.9;
contract Testcontract {
mapping(address=>uint) _flag;
event sendflag(string email,string tokens);
function test() public {
_flag[msg.sender] = 1;
}
function payforflag(string memory _base64Email,string memory _token) public {
require(_flag[msg.sender] == 1);
emit sendflag(_base64Email , _token);
}
}
然后基本上所有的eth浏览器都给了可以查询的api接口,比如我用的mumbai测试网的api网址
Mumbai PolygonScan – PolygonScan
接下来就是参考文档写轮询脚本即可
花了五分钟写的,大概逻辑就是反复请求给的api就行了,然后把event进行解密,之后发送邮箱,具体思路这样
from web3 import Web3, HTTPProvider
import requests
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import base64
import time
sender = "xxx@qq.com"
mail_host = "smtp.qq.com"
smtpport = 465
password = "xxxx"
def send_mail(mail, token):
try:
smtp=smtplib.SMTP_SSL(mail_host, smtpport)
smtp.login(sender, password)
message = MIMEText("flag is a flag{123}", "plain", "utf-8")
message["From"] = Header("Sender", "utf-8")
message["To"] = Header("Receiver", "utf-8")
subject = "here is your flag"
message["Subject"] = Header(subject, "utf-8")
smtp.sendmail(sender, mail, message.as_string())
except smtplib.SMTPException as e:
print(e)
finally:
smtp.quit()
topics = []
def event():
url = "https://api-testnet.polygonscan.com/api?module=logs&action=getLogs&topic1&address=xxxxx"
response = requests.get(url)
data = response.json()
print(data)
if data['status'] == '1':
for i in data['result']:
if i["topics"][0] in topics:
continue
else:
#发送邮件
logs = i['data']
logs = logs[2:]
logs = [logs[i:i+64] for i in range(0, len(logs), 64)]
logs[3] = Web3.toText(Web3.toBytes(hexstr=logs[3]))
logs[5] = Web3.toText(Web3.toBytes(hexstr=logs[5]))
mail = logs[3]
token = logs[5]
mail = base64.b64decode(mail).decode()
send_mail(mail, token)
topics.append(i["topics"][0])
while(True):
event()
time.sleep(20)
具体内容log要做具体解析,看区块链浏览器的event就好。
效果演示:
测试链结合自动部署出题方案
该方案部署起来也比较方便,不需要单独运行水龙头等内容,可以运行在测试网上
使用工具:
eth-challenge-base/example at main · chainflag/eth-challenge-base (github.com)
后续也会使用这套工具来进行,还是非常感谢这套系统的作者的。
项目克隆下来后,进入example路径,编写.env文件
其余几个不用管,常规题目基本上用不傲,只需要注意第一个即可
前往app.infura.io网站注册,获取api
选择想要部署的网络,复制即可。
将你编写好的合约文件放入contract文件夹中,来编写一个测试合约
pragma solidity 0.8.7;
contract Checkin {
string greeting;
constructor(string memory _greeting) public {
greeting = _greeting;
}
function greet() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _greeting) public {
greeting = _greeting;
}
function isSolved() public view returns (bool) {
string memory key = "HelloCTF";
return keccak256(abi.encodePacked(key)) == keccak256(abi.encodePacked(greeting));
}
}
然后编写challenge.yml
其实注释也写得比较完整,比较需要注意的是contract和args,contract上要写的内容是合约的名称,比如上述合约是Checkin,那么这个是checkin,包括后续去编写工厂合约还是ERC20等相关内容,只需要写上主要的主合约名称即可。
args决定了部署合约时传输的内容,以数组模式表示,此处对应constructor,如果传输数量和 constructor所需内容不符就会报错,这里要对应上,如果不需要就注释
value值决定了该合约在创建后有多少个eth,对应的在创建时需要转给相应的eth,单位是eth
其他的就没什么好说了,后面都有注释。
(注:合约要有isSolved函数,如果没有就去修改solved_event)
编写好后就可以docker-compose up -d进行部署测试,编译需要一段时间,等几分钟再nc查看,如果报错可能是网络问题,自行解决。
nc即可,如有需要也可以修改docker-compose.yml来修改端口
效果演示
还是比较方便的,也不需要去维护geth,如果没有特殊要求我个人还是比较推荐该方案的。
当然所有数据在测试网均可查,转账也是用metamask进行转账操作。
私链水龙头+自动部署题目出题方案
这个方案应该是如今ctf环境用得比较多的,当然部署起来还是有点小坑,这里也都编写出来分享一下。
整体的项目还是参考chainflag/eth-challenge-base: xinetd docker for building ethereum contract challenges (github.com)
这个项目,具体修改内容如下:
主目录的entrypoint.sh可能存在编码问题,我建议跑一次uinx2doc。
然后其余不用动了,进入geth文件目录,主要修改内容是.env.example
这些内容其实在 readme里面有详解,那么也还是说一下。chain_id其实无所谓,就是指定一个id,任意即可,第二个是geth所指定的一个用户地址,这个需要用来当水龙头的分发地址和miner的地址,必须要有的,第三行是对应上面地址的一个私钥,可以从metamask导出,也可以自主生成,总之要和地址对应起来
第四行也任意,配置好后保存重命名为.env文件
修改项目自带的njs路径下的eth-jsonrpc-access.js
项目自带的该文件无法让用户自行部署合约,这很不好,不符合我使用remix进行解题的幻想,重写为如下内容
function access(r) {
var whitelist = [
"eth_blockNumber",
"eth_call",
"eth_chainId",
"eth_estimateGas",
"eth_gasPrice",
"eth_getBalance",
"eth_getCode",
"eth_getStorageAt",
"eth_getTransactionByHash",
"eth_getTransactionCount",
"eth_getTransactionReceipt",
"eth_sendRawTransaction",
"net_version",
"rpc_modules",
"web3_clientVersion"
];
try {
var payload = JSON.parse(r.requestBody);
if (payload.jsonrpc !== "2.0") {
r.return(401, "jsonrpc version not supported\n");
return;
}
// if (!whitelist.includes(payload.method)) {
// r.return(401, "jsonrpc method is not allowed\n");
// return;
// }
if (Object.keys(payload).filter(key => key.toLowerCase() === 'method').length > 1) {
r.return(401, "jsonrpc method is not allowed\n");
return;
}
} catch (error) {
r.return(415, "Cannot parse payload into JSON\n");
return;
}
r.internalRedirect('@jsonrpc');
}
export default { access }
(不排除其他问题,但却是使用默认的无法部署合约。欢迎补充)
修改后然后编写docker-compose.yml
把上个方案的那些内容拿过来,然后把uri改为geth:8545
文件也复制在同一目录下
在docker-compose.yml中追加,当然也可以部署多个,直接后面继续修改端口和新建几个合约文件夹即可,这样题目和水龙头就都在一个网络里了
docker-compose up -d进行部署
可以访问8080查看水龙头
可以在metamask进行连接rpc网络。
剩下的与上一方案基本没差别,只是把整体网络搬到了私链,极大程度加大了上车的难度。整体响应速度也比在公链上快,也不会出现搞不到水龙头的情况。
补充:该项目nc后的第四个选项只会显示主要编译的合约,如果出现了多合约的题目,个人建议再challenges.yml的show_source:False这段取消注释,然后以附件的形式提供题目源码等内容。当然有能力的也可以去修改app.py源码,可以自行发挥。
总结
以上是常见的三种eth出题的一个方案,也算是在自己近期出题的时候踩的坑和相关内容进行一个记录,有些地方可能存在不全或者不正确的问题,也希望大家能够指出,大家伙也给chainflag这个项目去点点star,真的非常牛逼。
参考文章
智能合约攻击面及ctf出题指南 – DoubleMice – 我以晦朔春秋为聘,不知你愿否共我度完蜉蝣小年。
chainflag/eth-challenge-base: xinetd docker for building ethereum contract challenges (github.com)
- 最新
- 最热
只看作者