Skip to content

Instantly share code, notes, and snippets.

@IamMusavaRibica
Last active May 13, 2024 14:47
Show Gist options
  • Save IamMusavaRibica/a9e0f0a303d0e05162306e3ccb3aa16d to your computer and use it in GitHub Desktop.
Save IamMusavaRibica/a9e0f0a303d0e05162306e3ccb3aa16d to your computer and use it in GitHub Desktop.
Unfair Camera Wire - TBTL CTF 2024 writeup

TBTL CTF 2024 - Unfair Camera Wire writeup

This challenge was given on TBTL CTF 2024. It was put into the 'Misc' category.

We were the only team that solve this challenge. I have to admit, it was exhausting, and I spent more time looking for the right tools than actually figuring out how to solve it. So, what's going on?

The image shows what appears to be 12 words written out on a piece of paper, but the 3rd and 9th one are cut out.

The words are a mnemonic (aka a seed phrase) from which a seed can be derived. Seed derivation in the common sense is described by BIP39. However....

Let's look at flag.enc. It contains a base64 encoded encrypted version of the flag:

QklFMQM6iVSAKDJd3BYkUNA6exgQrGFqmjc58uR5++3nfQWRYVV9Nyx6HIHi72uXFO+IjTg7jmoaNVXv+RXp+DrtYlUhk+K/yUi5RWiwUrSAK0X/Cw39KVq6gvWxpwLFVrkaMEJJ1bGEcjc4UaWFfUwrZPoOuvfINYskrFZtgm9xDzVsYw==

The first four bytes of encrypted flag are 'BIE1', which likely indicates a header (magic) for some encryption scheme. We can look it up on grep.app. There are a lot of results also coming with keywords: electrum, ecies, ecc, but one thing stands out: https://github.com/sCrypt-Inc/scryptlib/blob/master/patches/bsv/lib/ecies/electrum-ecies.js#L46
Because of this, I was now sure that the flag was encrypted by this Electrum Ecies method.

I downloaded Electrum portable version, created a new wallet from a randomly generated 12-word seed phrase, studied a few options in the application, and learned some things:

  • You cannot type out words from BIP39 wordlist by hand and expect to make private keys out of them (the last word is some sort of checksum for the first 11)
  • At first I was confused by 'Encrypt/decrypt message' function, but figured out it is possible and easy by right clicking on Addresses (enable it through View -> Addresses), more on this later [1].
  • Electrum's way of converting seed phrases / mnemonics is not the same as BIP39! When creating a wallet from an existing seed (I already have a seed button), it's possible to choose the seed type: Electrum, BIP39 seed or SLIP39 seed. SLIP39 is irrelevant now. If you select BIP39, a message pops up saying that Electrum does not officially use BIP39. However, there is still a way to verify correctness of an Electrum seed phrase, because if you enter a wrong one in the wallet creation menu, the button Next is greyed out. There is even a comment mentioning that seed derivation is different than BIP39. (yes, Electrum is open-source)

This is nice, because when bruteforcing, it will drastically reduce the number of possible combinations. By default, there are 2048 words in the wordlist, so this would mean 2048^2 combinations. The image tells us something about the missing words. Third word ends in y, and is for sure longer than 5 characters. On the other hand, word #9 could end in h, m, n or y and is short, probably no more than four characters.

So how to verify if a seed phrase is correct? I found this very useful tool, in it there is a function findPhraseErrors(phrase) which I used along with the wordlist copied from Electrum source code. (since this function, and evertyhing else is in a closure scope, I had to put window.findPhraseErrors = findPhraseErrors to expose it globally)

Here is the code, I just put it in a new <script> tag at the bottom, and opened the file to run it:

let goodPhrases = [], goodPairs = [];
for (let word1 of wordlist) {
    for (let word2 of wordlist) {
        if (word1.length < 5 || word2.length > 4)
            continue;
        if (word1.charAt(word1.length-1) != 'y')
            continue;
        if (!'ymnh'.includes(word2.charAt(word2.length-1)))
            continue;

        let p = 'west peanut ' + word1 + ' cousin napkin unfair camera wire ' + word2 + ' convince act oppose';
        if (!findPhraseErrors(p)) {
            goodPhrases.push(p);
            goodPairs.push([word1, word2]);
        }
    }
}

I ended up with only 64 possible combinations. Remember [1]? Converting from mnemonic to seed is easy, Electrum has a function for that. However, obtaining this many addresses from the seed is tricky. I spent a good amount of time studying electrum source code to find out how they were generated, before discovering this html tool which, luckily, can do that too. Just input the seed phrase in the field above, and scroll to the bottom. When I inputted my seed phrase which I generated in Electrum for testing, the tool generated the same public and private keys that I saw in Electrum too. So I knew I didn't have to change any more settings. This tool has a lot of code, so I decided to monkey-patch it to retrieve the generated private keys:

  1. Find what function gets called when the input element's value is changed. Select it in DevTools and in console run getEventListeners($0). After a bit of digging through the returned value, you will find phraseChanged
  2. Expose function phraseChanged to global scope, e.g. window.onPhraseChanged = phraseChanged
  3. Notice there are two ways to display the results: via html table, and in a csv (probably so you can export that stuff easily). I located the function updateCsv by searching for string "path,address,public key,private key". It copies data from html table and transforms it into csv format. Here's the patch:
function updateCsv() {
    var tableCsv = "path,address,public key,private key\n";
    var rows = DOM.addresses.find("tr");
    for (var i=0; i<rows.length; i++) {
        var row = $(rows[i]);
        var cells = row.find("td");
        
        let textRow = [];  // columns of current row
        for (var j=0; j<cells.length; j++) {
            var cell = $(cells[j]);
            if (!cell.children().hasClass("invisible")) {
                let cellText = cell.text()
                tableCsv = tableCsv + cellText;
                textRow.push(cellText);  // push column
            }
            if (j != cells.length - 1) {
                tableCsv = tableCsv + ",";
            }
        }
        window.WIFS.push(textRow[3]);  // get the fourth column, it's the private key
        tableCsv = tableCsv + "\n";
    }
    DOM.csv.val(tableCsv);
}
  1. Put this code in a <script> tag at the bottom of body. We use setTimeout because the page will otherwise freeze. The delay (1700 in this case) should not be too small, it's better to use a larger value if you have a slow pc or a lot of apps or tabs open.
// let goodPairs = [["ability","cry"],["ability","iron"],...,["worry","such"]];
// we have goodPairs from before
window.WIFS = []

function testPhrase(pairIndex) {
    if (pairIndex === goodPairs.length)
        return;
    
    let [word1, word2] = goodPairs[pairIndex];
    console.log('testing', pairIndex, word1, word2);
    let p = 'west peanut ' + word1 + ' cousin napkin unfair camera wire ' + word2 + ' convince act oppose';
    
    onPhraseChanged(p);
    setTimeout(() => {testPhrase(pairIndex + 1)}, 1200);
}

setTimeout(() => {testPhrase(0)}, 100);

We got the keys. Around 1200 possible keys. Decryption by hand through Electrum app makes no sense. How to do it programatically? Electrum provides a decrypt_message function, however, to use it, we need to obtain private key as bytes.

These keys are in WIF format, however converting is trivial and can be done by hand, really. I used python bit module to do that for me.

In elliptic curve cryptography, the public key is defined as a multiple of G, a specific constant called a generator. The private key is just by how much we multiply G to obtain the public key. This isn't just simple algebratic multiplication, though, it uses various arithmetic on a elliptic curve, out of scope for this writeup, just understand that it's not possible to feasibly reverse that operation.

Here is the code. Note that it has to be run on Linux because electrum requires a native dll for elliptic curve related stuff (or it can on Windows, however I spent a few hours trying to do exactly that, but there was a lot of problems with loading libsecp256k1.dll)

from electrum import ecc, mnemonic  # requires installing, see their website for instructions
from bit import Key  # py -3.9 -m pip install bit   /   python3 -m pip install bit

FLAG_ENC = 'QklFMQM6iVSAKDJd3BYkUNA6exgQrGFqmjc58uR5++3nfQWRYVV9Nyx6HIHi72uXFO+IjTg7jmoaNVXv+RXp+DrtYlUhk+K/yUi5RWiwUrSAK0X/Cw39KVq6gvWxpwLFVrkaMEJJ1bGEcjc4UaWFfUwrZPoOuvfINYskrFZtgm9xDzVsYw=='
wifovi = '''L18UtD89wLQ5p2Uuh4LiKEJzTiKtfa3yUnG5QUPycc2cebrcPFHs
KxkCZJdUEjVykiD5tWnTv9grpHo6W53WQpquBrMccmd3TAiZ2xTj
KzXwhjcgSgcUfKyhx9XoTN2RpSB6nsWtRh7yTrKMDe8USXgUP2jr'''  #  this is a short sample

for w in wifovi.split('\n'):
    privkey_bytes = Key(w).to_bytes()

    ecc_priv = ecc.ECPrivkey(privkey_bytes)
    try:
        print(w, ecc_priv.decrypt_message(FLAG_ENC))
    except Exception as e:
        pass
musava@DESKTOP-ULOC0VP:~$ sudo python3 brutefocer.py
libsecp256k1 library found but it was built without desired module (--enable-module-schnorrsig)
L2hz6o4GXuAJtKdxJ3mCEh7Yw6YDYJ8RDoyj1kLt6F2mPJJpuEFu b'TBTL{12_w0rd5_70_5h13ld_y0u_fr0m_7h3_c47457r0ph3}\n'

And if you're wondering, the words were poverty and toy. Challenge descrption mentions 'honey' and 'key', but they weren't useful for solving. Maybe the key was used to scrape these two words (and encrypt the flag)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment