CyCTF - Cairo ICT CTF 2022 by CyShield

This is my writeup for the last blockchain challenge in the Cairo ICT CTF qualifications this year.

November 20, 2022 - 8 minute read -
Security Blockchain CTF Infosec Python Reverse Hacking

CyCTF

Hello hackers, this isn’t my first time to try to solve blockchain challenges. I got some basic understanding of how it works in terms of smart contracts and the cryptography involved, but this time I was lucky to give it a big try even after the qualification round.

Smart CrackMe

The challenge was given a public deployed contract with address 0x31678C496E63BcEbd1398D25C9B21cDcb3cc5e23 on Etherscan along with its ABI. It has only one transaction and nothing else but the contract bytecode.

JEB

0x6080604052600436106100345760003560e01c806308c5dc7f146100395780633ccfd60b146100695780638da5cb5b14610080575b600080fd5b610053600480360381019061004e919061040c565b6100ab565b6040516100609190610495565b60405180910390f35b34801561007557600080fd5b5061007e6102d6565b005b34801561008c57600080fd5b506100956103d1565b6040516100a2919061047a565b60405180910390f35b600080600090505b603081101561012f5782600082603081106100d1576100d06105ae565b5b602091828204019190069054906101000a900460ff1618600282603081106100fc576100fb6105ae565b5b602091828204019190066101000a81548160ff021916908360ff160217905550808061012790610536565b9150506100b3565b5060436002600060308110610147576101466105ae565b5b602091828204019190069054906101000a900460ff1660ff1614801561019a57506079600260016030811061017f5761017e6105ae565b5b602091828204019190069054906101000a900460ff1660ff16145b80156101d257506043600280603081106101b7576101b66105ae565b5b602091828204019190069054906101000a900460ff1660ff16145b801561020b5750605460026003603081106101f0576101ef6105ae565b5b602091828204019190069054906101000a900460ff1660ff16145b8015610244575060466002600460308110610229576102286105ae565b5b602091828204019190069054906101000a900460ff1660ff16145b801561027d5750607b6002600560308110610262576102616105ae565b5b602091828204019190069054906101000a900460ff1660ff16145b156102cc5733600460006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550600190506102d1565b600090505b919050565b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610366576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161035d906104b0565b60405180910390fd5b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166108fc479081150290604051600060405180830381858888f193505050501580156103ce573d6000803e3d6000fd5b50565b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000813590506104068161060b565b92915050565b600060208284031215610422576104216105dd565b5b6000610430848285016103f7565b91505092915050565b610442816104e1565b82525050565b610451816104f3565b82525050565b60006104646018836104d0565b915061046f826105e2565b602082019050919050565b600060208201905061048f6000830184610439565b92915050565b60006020820190506104aa6000830184610448565b92915050565b600060208201905081810360008301526104c981610457565b9050919050565b600082825260208201905092915050565b60006104ec826104ff565b9050919050565b60008115159050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b60006105418261051f565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214156105745761057361057f565b5b600182019050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600080fd5b7f43616c6c6572206973206e6f7420746865206f776e6572210000000000000000600082015250565b61061481610529565b811461061f57600080fd5b5056fea26469706673582212202df7f55fc48becfec4da6f96cff3b6fd616f35a2e864984c3078521a32ff8c4064736f6c63430008070033

And the contract ABI:

// SmartCrackMe.abi
[
	{
		"inputs": [],
		"stateMutability": "nonpayable",
		"type": "constructor"
	},
	{
		"inputs": [
			{
				"internalType": "uint8",
				"name": "key",
				"type": "uint8"
			}
		],
		"name": "check_flag",
		"outputs": [
			{
				"internalType": "bool",
				"name": "",
				"type": "bool"
			}
		],
		"stateMutability": "payable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "owner",
		"outputs": [
			{
				"internalType": "address payable",
				"name": "",
				"type": "address"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "withdraw",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	}
]

First thing noticed that the contract section has a button to decompile the bytecode and after decompiling, it gives us the following code:

# Palkeoramix decompiler. 

def storage:
  stor2 is array of uint256 at storage 2
  owner is addr at storage 4

def owner(): # not payable
  return owner

#
#  Regular functions
#

def _fallback() payable: # default function
  revert

def withdraw(): # not payable
  if owner != caller:
      revert with 0, 'Caller is not the owner!'
  call owner with:
     value eth.balance(this.address) wei
       gas 2300 * is_zero(value) wei
  if not ext_call.success:
      revert with ext_call.return_data[0 len return_data.size]

def unknown08c5dc7f(uint256 _param1) payable: 
  require calldata.size - 4 >=ΓÇ 32
  require _param1 == uint8(_param1)
  idx = 0
  while idx < 48:
      stor2[0.03125 / idx].field_0 = uint8(stor(0.03125 / idx)[uint8(idx)] xor _param1) * 256^(idx % 32) or !(255 * 256^(idx % 32)) and stor2[0.03125 / idx].field_0
      if idx == -1:
          revert with 'NH{q', 17
      idx = idx + 1
      continue 
  if uint8(stor2.length) != 67:
      return 0
  if stor2.length.field_8 != 121:
      return 0
  if stor2.length.field_16 != 67:
      return 0
  if stor2.length.field_24 != 84:
      return 0
  if stor2.length.field_32 != 70:
      return 0
  if stor2.length.field_40 != 123:
      return 0
  owner = caller
  return 1

The website uses Panoramix python decompiler and as we see there’re only 3 functions owner, withdraw, and the last one check_flag that had been decompiled into this weird name unknown08c5dc7f.

  1. owner: Returns only the address of the contract owner.
  2. withdraw: Checks if the method caller address same as the contract owner address. If so withdraw the ballance, if not it aborts.
  3. check_flag: It takes 8-bit integer value, does some calculations on the defined array of size 2 and lastly it checks if the final array value has the correct flag format.

I was stuck at the decompiled line inside the loop. It has weird syntax and didn’t know how represent the first part. Actualy, I’ve tried to assume that some parts are useless and tweak the others but nothing worked. So, I decided to look at the decompiler repo and see if there’s any options that I could use to help me decompile the bytecode correctly, but unfortunately still nothing.

I asked one of my teammates if he knows any smart contract decompiler and he told me I could use JEB. I started off and loaded the bytecode into JEB, it shows me the opcode instructions and when I tried to decompile to the source code it gives a decompilation error. I’ve tried to read the instructions and map it to what I’ve in the code above, but still no luck.

JEB

On the next day, I asked our reverse mate to take another look on the decompiled weird instructions, but he couldn’t understand a thing from it. He decided to decompile it using his own JEB version and this time the source code showed up on his screen.

function start() {
    *0x40 = 0x80;

    if($msg.value != 0x0) {
        revert(0x0, 0x0);
    }

    var3 = storage[0x0];
    storage[0x0] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00) | 0xda;
    var3 = storage[0x0];
    storage[0x0] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff) | 0xe000;
    var3 = storage[0x0];
    storage[0x0] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffff) | 0xda0000;
    var3 = storage[0x0];
    storage[0x0] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffff) | 0xcd000000;
    ...
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffff) | 0xfc00000000000000;
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffff) | 0xf40000000000000000;
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffff) | 0xdc000000000000000000;
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffff) | 0xf400000000000000000000;
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffff) | 0xa10000000000000000000000;
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffff) | 0xdc000000000000000000000000;
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffff) | 0xcb00000000000000000000000000;
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffff) | 0xa60000000000000000000000000000;
    var3 = storage[0x1];
    storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffff) | 0xe4000000000000000000000000000000;
    var0 = msg.sender;
    var3 = storage[0x4];
    storage[0x4] = ((address(var0)) * 0x1) | (var3 & 0xffffffffffffffffffffffff0000000000000000000000000000000000000000);
    codecopy(0x0, 0xb77, 0x658);
    return(0x0, 0x658);
}

First, we know that storage variable has size 2 from the previous decompiled code and now the code has flatten out the iterations one by one. He tried to run the 48 lines above and the result wasn’t usefull, we tried to play with it, but still empty hand. The competition has ended and we still couldn’t figure it out. After a day I decided to give it another try and this time look a bit closer on the last decompiled code. I ran it and did a bruteforce xoring over the result and I noticed the reversed flag appeared with byte 0x99 🙂.

Final code:

from pwn import xor
storage = [0]*2
var3 = storage[0x0];
storage[0x0] = (var3 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00) | 0xda;
...
var3 = storage[0x1];
storage[0x1] = (var3 & 0xffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffff) | 0xe4000000000000000000000000000000;
part1 = xor(long_to_bytes(storage[0]), 0x99)[::-1].decode()
part2 = xor(long_to_bytes(storage[1]), 0x99)[::-1].decode()
print(part1+part2)

Flag: CyCTF{ju57_l1Ke_7EH_900d_0ld_CR4CkME2_RemEm8ER?}