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.numberes sencillamente1, porque el Smart Contract se despliega en el primer bloque- Podemos encontrar
block.timestampa partir de la información del bloque msg.senderes 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}