bcactf 2021

This time around I played with a team, I think I’d be up for playing in a team more often so that I can do more rev tasks. We came 127th out of 816 teams which feels like an improvement over Dawg CTF. My favourite challenges were Storytime: The Shocking Conclusion and Digitally Encrypted 2.

misc

Welcome to the Casino

Can you get three-of-a-kind on this slot machine? Let’s find out!

Author: micpap25

For this challenge you would connect to their server, enter the requested letter, and then you would hope you got lucky. Three letters would appear after a short animation and if they all matched then you’d get a flag. I was still debugging the multiprocessing when I got the flag so the script isn’t worth sharing, but this is what it looked like when you got the flag…

Spinning...
           [[[ x ]]]
           [[[ n ]]]
           [[[ x ]]]
		   
Congratulations! You won a prize!
It's my respect!

                      ¶¶¶¶¶¶¶¶¶
                    ¶¶          ¶¶
      ¶¶¶¶¶       ¶¶              ¶¶
     ¶     ¶    ¶¶     ¶¶    ¶¶     ¶¶
     ¶     ¶   ¶¶      ¶¶    ¶¶       ¶¶
     ¶    ¶  ¶¶        ¶¶    ¶¶        ¶¶
      ¶   ¶   ¶                         ¶¶
    ¶¶¶¶¶¶¶¶¶¶¶¶                        ¶¶
   ¶            ¶ ¶¶             ¶¶     ¶¶
  ¶¶            ¶  ¶¶           ¶¶      ¶¶
 ¶¶   ¶¶¶¶¶¶¶¶¶¶¶    ¶¶       ¶¶        ¶¶
 ¶               ¶     ¶¶¶¶¶¶¶         ¶¶
 ¶¶              ¶                    ¶¶.
  ¶   ¶¶¶¶¶¶¶¶¶¶¶¶                   ¶¶
  ¶¶           ¶  ¶¶                ¶¶
   ¶¶¶¶¶¶¶¶¶¶¶¶     ¶¶            ¶¶
                       ¶¶¶¶¶¶¶¶¶¶¶

Hey, you're pretty cool, dood!

Come back next time!

 /$$                           /$$                
| $$                          | $$                
| $$       /$$   /$$  /$$$$$$$| $$   /$$ /$$   /$$
| $$      | $$  | $$ /$$_____/| $$  /$$/| $$  | $$
| $$      | $$  | $$| $$      | $$$$$$/ | $$  | $$
| $$      | $$  | $$| $$      | $$_  $$ | $$  | $$
| $$$$$$$$|  $$$$$$/|  $$$$$$$| $$ \  $$|  $$$$$$$
|________/ \______/  \_______/|__/  \__/ \____  $$
                                         /$$  | $$
                                        |  $$$$$$/
                                         \______/ 
 /$$                   /$$     /$$                
| $$                  | $$    | $$                
| $$        /$$$$$$  /$$$$$$ /$$$$$$    /$$$$$$   
| $$       /$$__  $$|_  $$_/|_  $$_/   /$$__  $$  
| $$      | $$  \ $$  | $$    | $$    | $$  \ $$  
| $$      | $$  | $$  | $$ /$$| $$ /$$| $$  | $$  
| $$$$$$$$|  $$$$$$/  |  $$$$/|  $$$$/|  $$$$$$/  
|________/ \______/    \___/   \___/   \______/   



Welcome to the Lucky Lotto Slot Machine!
Let's see if you're today's big winner!
Enter the letter "j" to pull the lever...

Spinning...
           [[[ i ]]]
           [[[ i ]]]
           [[[ i ]]]
Congratulations! You won the grand prize!
It's a flag!

   .^.
  (( ))
   |#|_______________________________
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#||$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$|
   |#|'""""""""""""""""""""""""""""""'
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|
   |#|

bcactf{y0u_g0t_1ucKy_af23dd97g64n}

Flag: bcactf{y0u_g0t_1ucKy_af23dd97g64n}

pwn

BCA-Mart

After the pandemic hit, everybody closed up shop and moved online. Not wanting to be left behind, BCA MART is launching its own digital presence. Shop BCA MART from the comfort of your own home today!

Author: Edward Feng

This challenge binary offered a prompt for a storefront, where one of the items for sale is the flag. The only issue is that the flag costs more money than you have. I skimmed the code and it was pretty quickly clear that there was probably an integer overflow bug. I’ve added (...) wherever I omitted some code.

int money = 15;

int purchase(char *item, int cost) {
    int amount;
    printf("How many %s would you like to buy?\n", item);
    printf("> ");
    scanf("%d", &amount);

    if (amount > 0) {
        cost *= amount;
        printf("That'll cost $%d.\n", cost);
        if (cost <= money) {
            puts("Thanks for your purchse!");
            money -= cost;
        } else {
            puts("Sorry, but you don't have enough money.");
            puts("Sucks to be you I guess.");
            amount = 0;
        }
    } else {
        puts("I'm sorry, but we don't put up with pranksters.");
        puts("Please buy something or leave.");
    }

    return amount;
}

int main() {
    int input;

    setbuf(stdout, NULL);
    setbuf(stdin, NULL);
    setbuf(stderr, NULL);

	(...)
    while (1) {
		(...)
        puts("5) BCA© school merch: $20.00");
        puts("6) Flag: $100.00");
        puts("0) Leave");
        puts("");
        printf("You currently have $%d.\n", money);
        puts("What would you like to buy?");

        printf("> ");
        scanf("%d", &input);

        switch (input) {
			(...)
            case 5:
                purchase("wonderfully-designed t-shirts", 20);
                break;
            case 6:
                if (purchase("super-cool ctf flags", 100) > 0) {
                    FILE *fp = fopen("flag.txt", "r");
                    char flag[100];

                    if (fp == NULL) {
                        puts("[If you are seeing this on the remote server, please contact admin].");
                        exit(1);
                    }

                    fgets(flag, sizeof(flag), fp);
                    puts(flag);
                }
                break;
            default:
                puts("Sorry, please select a valid option.");
        }
    }
}

You get to input two numbers: what you would like to buy, and the amount of them that you’d like. There’s not much flexibility in the first number entered, and the latter can’t be set to a negative number. However, money is just an int, not an unsigned int, which means the leftmost bit is reserved for signaling whether money is positive or negative. if cost * amount is big enough, it’ll flip that bit and the result will become negative.

You currently have $15.
What would you like to buy?
> 5
How many wonderfully-designed t-shirts would you like to buy?
> 461168601842738780
That'll cost $-208.
Thanks for your purchse!

1) Hichew™: $2.00
2) Lays® Potato Chips: $2.00
3) Water in a Bottle: $1.00
4) Not Water© in a Bottle: $2.00
5) BCA© school merch: $20.00
6) Flag: $100.00
0) Leave

You currently have $223.
What would you like to buy?
> 6
How many super-cool ctf flags would you like to buy?
> 2
That'll cost $200.
Thanks for your purchse!
bcactf{bca_store??_wdym_ive_never_heard_of_that_one_before}

Flag: bcactf{bca_store??_wdym_ive_never_heard_of_that_one_before}

crypto

Easy RSA

As part of his CTF101 class, Gerald needs to find the plaintext that his teacher encrypted. Can you help him do his homework? ( It’s definetely not cheating ;) )

Author: akth3n3rd

We’re given p, q, n, e, ct. I adapted a script from stackoverflow that now looks like this:

# based on https://crypto.stackexchange.com/a/68732

import math
import binascii

def getModInverse(a, m):
    if math.gcd(a, m) != 1:
        return None
    u1, u2, u3 = 1, 0, a
    v1, v2, v3 = 0, 1, m

    while v3 != 0:
        q = u3 // v3
        v1, v2, v3, u1, u2, u3 = (
            u1 - q * v1), (u2 - q * v2), (u3 - q * v3), v1, v2, v3
    return u1 % m

def main():
    p = int(input('p: ').strip())
    q = int(input('q: ').strip())
    e = int(input('e: ').strip())
    ct = int(input('ct (as hex): ').strip(), 16)

    n = p*q

    # compute n
    n = p * q

    # Compute phi(n)
    phi = (p - 1) * (q - 1)

    # Compute modular inverse of e
    d = getModInverse(e, phi)

    print("n:  " + str(d))

    # Decrypt ciphertext
    pt = pow(ct, d, n)
    print()  # separate IO
    print("pt (as hex): " + hex(pt)[2:])
    print("pt (as string): " + binascii.unhexlify(hex(pt)[2:]).decode())

if __name__ == "__main__":
    main()

Here’s the input/output:

p: 251867251891350186672194341006245222227
q: 31930326592276723738691137862727489059
e: 65537
ct (as hex): b99efa97a6800b4a07f2ccb1ba0c02d8a1d07e538ac618d773d35a45cacee47

n:  4895611838388522487150697438371515909261488525071715048233750808546849654653
pt (as hex): 6263616374667b5253415f49535f454153595f41465445525f414c4c7d
pt (as string): bcactf{RSA_IS_EASY_AFTER_ALL}

Flag: bcactf{RSA_IS_EASY_AFTER_ALL}

Slightly Harder RSA

Gerald’s homework is getting trickier. He isn’t being given the primes anymore. Help him find the plaintext!

Author: akth3n3rd

Same as Easy RSA, but have to work out p, q, from n. Just used factordb to do the factoring

p: 884666943491340899394244376743
q: 1070864180718820651198166458463
e: 65537
ct (as hex): 8159e4e97b04af127fed8fbd40bd03c80c3f4b8b764a86394c
n:  834418746183915656610879248898026819357498357797104542136881

pt (as hex): 6263616374667b7273615f666163746f72696e677d
pt (as string): bcactf{rsa_factoring}

Flag: bcactf{rsa_factoring}

fnes 1

My friend developed this encryption service, and he’s been trying to get us all to use it. Sure, it’s convenient and easy to use, and it allows you to send encrypted messages easily, and…

Well, I want to get control of his service so I can monitor all the messages! I think he’s hidden some features and files behind a secret admin passphrase. Can you help me access those hidden files?

Author: eiis1000

We’re given a program that performs an encryption/decryption service. If two people connect at the same time, then they are able to encrypt or decrypt messages, and send the ciphertext to each other without fear of anybody else understanding the messages a few hours later. This is described within the program

Welcome to your Friendly Neighborhood Encryption Service (FNES)! If you and a friend both run this service at the same time, you should be able to send messages to each other! Here are the steps:

  1. Friends A and B connect to the server at the same time (you have about a five second margin)
  2. Friend A encodes a message and sends it to Friend B
  3. Friend B decodes the message, encodes their reply, and sends it to Friend A
  4. Friend A decodes the reply, rinse and repeat

Make sure to not make any mistakes, though, or your keystreams might come out of sync…

PS: For security reasons, there are four characters you aren’t allowed to encrypt. Sorry!

Our goal is to decrypt a message to the target text “Open sesame… Flag please!”.

Messages are RC4 encrypted, and the key is a secret int added to the current time. The two friends have to join at the same time because the key relates to the current time. The friends also can’t make any mistakes because the keystream will get out of sync.

I believe the intended solution was to take advantage of the RC4 keystream. It’s dependent on the key but not the data at all, and RC4 just XORs the data against the keystream. For example, here’s first few bytes from an RC4 keystream where the key is “encryption”: c2 53 53 e9 b4 ba 35 13 1e 3a e5 a8 10 7f ff

Now, here’s some plaintext and ciphertext for phrases:

plain text ciphertext
my name is john af 2a 73 87 d5 d7 50 33 77 49 c5 c2 7f 17 91
my Name is adam af 2a 73 a7 d5 d7 50 33 77 49 c5 c9 74 1e 92

The ciphertext only begins to differ at the end, where the plaintext diverges. It also differs specifically where characters differ. Whether N in name is capitalised doesn’t affect the rest of the ciphertext. Therefore, if we know the ciphertext and plaintext we can use that to find the keystream, and then arbitrarily encrypt our own messages! Here’s a quick demo.

>>> import binascii

>>> # finding the keystream
>>> ciphertext = b'\xaf\x2a\x73\x87\xd5\xd7\x50\x33\x77\x49\xc5\xc2\x7f\x17\x91'
>>> plaintext = b'my name is john'
>>> keystream = b''.join([bytes([c^p]) for c,p in zip(ciphertext, plaintext)])
>>> binascii.hexlify(keystream)
b'c25353e9b4ba35131e3ae5a8107fff'

>>> # encrypting our own message
>>> attack_plaintext = b'my Name is adam'
>>> attack_ciphertext = b''.join([bytes([k^p]) for k,p in zip(keystream, attack_plaintext)])
>>> binascii.hexlify(attack_ciphertext)
b'af2a73a7d5d750337749c5c9741e92'

Flag: bcactf{why-would-you-attack-your-FNES????-4x35rcg}

rev

I’ve grouped these by set instead of points…

Wait, this isn’t C

This just looks like a binary… Lets try to decompile it… Wait, this isn’t C…

Author: MichaelK522

We’re given a single file flag_checker_1.

strings implies we’re looking at Fortran. It doesn’t really end up mattering…

In the main function we see that a variable gets set up

mov     [rbp+target], 63h ; 'c'
mov     [rbp+target+2], 65h ; 'e'
mov     [rbp+target+4], 64h ; 'd'
mov     [rbp+target+6], 67h ; 'g'
...
mov     [rbp+target+28h], 84h
mov     [rbp+target+2Ah], 65h ; 'e'
mov     [rbp+target+2Ch], 47h ; 'G'
mov     [rbp+target+2Eh], 84h

And then the following loops are performed

  for ( i = 1; i <= 25; ++i )
    maybe_input[i - 1] = *(&maybe_input[31] + i + 1);
  for ( i = 1; i <= 25; ++i )
  {
    maybe_input[i - 1] += i;
    if ( maybe_input[i - 1] != target[i - 1] )
    {
	  <FAIL>
	}
  }
  <SUCCESS>
}

I didn’t really understand where maybe_input was set, but went on autopilot and undid the for-loop, which was sufficient to get the flag.

>>> ct
['99', '101', '100', '103', '121', '108', '130', '110', '57', '124', '127', '126', '65', '92', '110', '121', '70', '113', '118', '68', '132', '101', '71', '132', '150']
>>> ''.join([chr(int(c) - 1 - i) for i, c in enumerate(ct)])
'bcactf{f0rtr4N_i5_c0oO0l}'

Flag: bcactf{f0rtr4N_i5_c0oO0l}

Storytime: The Opening Gambit

Gather around, it’s time for a story! I’ve even included a reward at the end!

Author: micpap25

We’re given an ELF file that, when run, slowly prints the first few lines in baa baa black sheep and then quits. In IDA you can see it has a for loop that cuts out early, and the flag is in plaintext. strings would also give up the flag.

Flag: bcactf{w0ol_m4k3s_str1ng_ziv4mk3ca91b}

Storytime: The Tragic Interlude

I’ve got a really sad story for today. It’s about a very famous dragon. If you stick around, maybe I’ll give you a flag!

Author: micpap25

We’re given another ELF file, it writes 32 DWORDs in a jumbled order, and then bitshifts them and adds the index into the variable for the flag.

ciphertext[36] = 179;
ciphertext[14] = 180;
ciphertext[6] = 235;
ciphertext[13] = 207;
...
ciphertext[9] = 87;
ciphertext[22] = 52;
ciphertext[27] = 151;
ciphertext[34] = 126;
for ( i = 0; i <= 36; ++i )
    flag[i] = (ciphertext[i] >> 1) + i;

To reorder them I ran the program in gdb and dumped the variable

gef➤  x/36w $rbp-0x710
0x7fffffffda00: 0x000000c5      0x000000c4      0x000000bf      0x000000c1
0x7fffffffda10: 0x000000e0      0x000000c2      0x000000eb      0x000000da
0x7fffffffda20: 0x000000c0      0x00000057      0x000000d5      0x000000a9
0x7fffffffda30: 0x00000048      0x000000cf      0x000000b4      0x00000049
0x7fffffffda40: 0x000000c4      0x0000009d      0x000000a5      0x000000be
0x7fffffffda50: 0x00000040      0x000000a5      0x00000034      0x000000ae
0x7fffffffda60: 0x0000008f      0x0000003d      0x00000039      0x00000097
0x7fffffffda70: 0x000000b6      0x00000037      0x0000009a      0x00000089
0x7fffffffda80: 0x00000022      0x00000097      0x0000007e      0x000000a6

After some munging I got to this awful one-liner:

>>> ''.join([chr(i + (c >> 1) ) for i, c in enumerate(binascii.unhexlify(''.join('c5c4bfc1 e0c2ebda c057d5a9 48cfb449 c49da5be 40a534ae 8f3d3997 b6379a89 22977ea6'.split(' '))))])
'bcactf{th4t_0th3r_dr4g0n_76fw8kc1lav'

I probably just didn’t extract the entire thing, so I added the ending curly brace to get…

Flag: bcactf{th4t_0th3r_dr4g0n_76fw8kc1lav}

Storytime: The Shocking Conclusion

I think this is my last story for this competition. (Didn’t I only tell, like, three?) This one’s a sing-along. Let’s see how long we can keep it up!

Author: micpap25

We’re given the last ELF in the series: story3. When run it appears to just print the following over and over:

I know a song that gets on everybody’s nerves, Everybody’s nerves, yes, on everybody’s nerves, I know a song that gets on everybody’s nerves, And this is how it goes.

There’s a bunch of other functions which are not directly called. I got a chuckle out of some, one will print the beginning of the Bee Movie script (a callback to Flare-On 6 challenge 10), another prints an excerpt from https://mrt4ntr4.github.io/BCACTF-LargeData-Writeup/ which I found hilarious.

I usually try to stay polite towards organizers because I know that making a CTF is hard. But there are some issues that need to be fixed. Most importantly, every single challenge is trivially easy. I don’t think that’s inherently terrible, although it makes the CTF less enjoyable. I think having a CTF where the goal is maxing quickly is defendable. But that means you can’t release problems midway through the competition, especially if they are just as easy. Doing that completely nullifying every previous challenge. You also need to check that your challenges work. Both three-step-program and bca-store, the challenges released today, were initially broken. By the time you fix your mistake, many people have solved the challenge. So you now reduce the entire competition to seeing how fast people can refresh and submit their solution to a fixed challenge. I think the best course of action is to get rid of the newly released challenges and to release many more future challenges.

Note: I didn’t do the CTF last year so I don’t know what it was like, but I really enjoyed this one.

One of the functions wasn’t just printing junk, so it stood out to me. It was labelled FUN_2302136 (I think the author set the names for the troll functions). The meat of it was this:

  timer = time(0LL);
  time_obj = localtime(&timer);
  tm_min = time_obj->tm_min;

  for ( i = 0; i <= 33; ++i )
    flag[i] = (to_decrypt[i] >> tm_min) - tm_min * i + 21;

  return puts(flag);

It takes many numbers as input, unjumbles their ordering, and then does a simple calculation based on the current time (minutes past the hour). One of the other functions gave the clue “the second minute” which turned out to not-be a troll.

return puts("74 68 65 20 73 65 63 6f 6e 64 20 6d 69 6e 75 74 65")

// can be decoded in python with: ''.join([chr(int(x, 16)) for x in "74 68 65 20 73 65 63 6f 6e 64 20 6d 69 6e 75 74 65".split(' ')])

This was my python script to perform the decoding.

a = [467, 520, 390, 452, 459, 426, 179, 517, 257, 351, 265, 447, 389, 538, 231, 575, 365, 682, 210, 440, 330, 389, 337, 510, 554, 322, 309, 415, 401, 323, 574, 298, 511, 397]

m= {}  # the `-1` thing turned out to be the easiest way to convert the IDA output
m[21] = a[1 - 1]
m[28] = a[2 - 1]
m[7] = a[3 - 1]
m[12] = a[4 - 1]
(...)
m[23] = a[32 - 1]
m[24] = a[33 - 1]
m[10] = a[34 - 1]

"""
for ( i = 0; i <= 33; ++i )
    flag[i] = (m[i] >> tm_min) - tm_min * i + 21;
"""

minute = 2
res = [-1 for _ in range(len(m))]
for k,v in m.items():
    res[k] = (v >> minute) - minute * k + 21
print(''.join(chr(c) for c in res))

Flag: bcactf{h1dd3n_c0d3_1s_h1dd3n_2c8d}

Digitally Encrypted 1

Gerald has just learned about this program called Digital which allows him to create circuits. Gerald wants to send messages to his friend, also named Gerald, but doesn’t want Gerald (a third one) to know what they are saying. Gerald, therefore, built this encryption circuit to prevent Gerald from reading his messages to Gerald.

Author: MichaelK522

We’re given two files: circuit_1.dig and encrypted.txt. Eyeballing encrypted.txt, it looks like some 17 byte XOR.

00000000  42 36 41 34 36 45 45 39 31 33 42 33 33 45 31 39 20  |B6A46EE913B33E19 |
00000011  42 43 41 36 37 42 44 35 31 30 42 34 33 36 33 32 20  |BCA67BD510B43632 |
00000022  41 34 42 35 36 41 46 45 31 33 41 43 31 41 31 45 20  |A4B56AFE13AC1A1E |
00000033  42 44 41 41 37 46 45 36 30 32 45 34 37 37 35 45 20  |BDAA7FE602E4775E |
00000044  45 44 46 36 33 41 42 38 35 30 45 36 37 30 31 30     |EDF63AB850E67010|

After looking at the circuit loaded in Digital, it becomes pretty obvious it’s an eight byte XOR, with key 0xD4C70F8A67D5456D.

Here’s the cyberchef Recipe: https://gchq.github.io/CyberChef/#recipe=From_Hex(‘Auto’)XOR(%7B’option’:’Hex’,’string’:’D4C70F8A67D5456D’%7D,’Standard’,false)To_Hexdump(17,false,false,false/disabled)

Flag: bcactf{that_was_pretty_simple1239152735}

Digitally Encrypted 2

Gerald and Gerald have just learned that Gerald has cracked their previous cypher. Gerald scolds Gerald, saying that he shouldn’t have given away the key. Gerald, therefore, decides to create a new cypher, hopefully one that Gerald can’t crack.

Author: MichaelK522

For this challenge we are given a more challenging circuit, along with ciphertext.

I rewrote the process in python to confirm my understanding

def xnor(a, b):  # only need to handle case of 32 bits
    return 0xffffffff - (a ^ b)


def encryptor(pt, key):
    """

    :param pt: 64 bits
    :param key: 40 bits
    :return:
    """
    print('pt:', hex(pt))
    print('key:', hex(key))

    ph = pt >> 32  # high DWORD from plaintext QWORD
    pl = pt & 2**32-1  # low DWORD from plaintext QWORD

    k1 = key & 2**32-1
    k2 = (key >> 8) & 2**32-1

    A = xnor(k1, pl)
    B = A ^ ph  # low DWORD from ciphertext QWORD

    C = xnor(B, k2)
    D = pl ^ C  # high DWORD from ciphertext QWORD

    output = B | (D << 32)
    print('output', hex(output))
    return output

We know 7 of the first 8 bytes of the flag: bcactf{. We can therefore guess the last character to have all 8, and then we have enough variables to calculate key_1, key_2 (XNOR is written v because I don’t know the correct character…): B = pl v k1 ^ ph => k1 = B ^ ph v pl D = b v k2 ^ pl => k2 = D ^ pl v B

def decryptor():
    base_pt = int(binascii.hexlify(b'bcactf{\x00'), 16)

    ct = 0x7B18824F93FB072A  # first eight bytes from ciphertext

    B = ct & 0xffffffff
    D = ct >> 32

    for i in range(0x20, 0x80):  # brute force last char
        print('trying i =', hex(i), chr(i))
        pt = base_pt + i
        ph = pt >> 32
        pl = pt & 2**32-1

        k1 = xnor(B ^ ph, pl)
        k2 = xnor(D ^ pl, B)
        key = k1 | (k2 << 8)

        if (k1 >> 8 == k2 & 2**23-1):
            print('k1', hex(k1))
            print('k2', hex(k2))
            print('key', hex(key))
            print('pt', hex(pt))

k1 and k2 overlapped, so that could be used to verify the decode was correct. The correct values ended up being:

k1 = 0x7a01e2ce  
k2 = 0x637a01e2

with the last value of the plaintext being x. From there we can decode the rest of the ciphertext

def decrypt_now_that_we_know_the_key(ct):
    """
    :param ct: 64 bits
    """
    k1 = 0x7a01e2ce
    k2 = 0x637a01e2

    B = ct & 0xffffffff  # 2850399423
    D = ct >> 32  # 37502721

    pl = xnor(B, k2) ^ D
    ph = xnor(k1, pl) ^ B

    pt = pl | (ph << 32)
    return binascii.unhexlify(hex(pt)[2:])


if __name__ == '__main__':
    ct = [0x7B18824F93FB072A, 0x2909D67381E26C31, 0x57238C7EFEF9132D, 0x7D24AD42B991216A, 0x464B9173A2811D13]
    print('Flag:', b''.join([decrypt_now_that_we_know_the_key(part) for part in ct]).decode())

Flag: bcactf{x0r5_4nD_NXoR5_aNd_NnX0r5_0r_xOr}

web

These were basically reversing challenges…

WASM Protected Site 1

Check out my super safe website! Enter the password to get the flag

Author: Andrew

We’re given a simple website with an input. Here’s the interesting parts of main.js:

const fetchWASMCode = () => {
    return new Promise((res, rej) => {
        const req = new XMLHttpRequest();

        req.onload = function () {
            res(req.response);
        }
        req.onerror = (err) => {
            console.warn('If you\\'re seeing this logged, something broke');
            rej(err)
        }
        req.open("GET", "./code.wasm");
        req.responseType = "arraybuffer";
        req.send();
    });
};
const input = document.querySelector('input#password');
const response = document.querySelector('p#response-text');

document.querySelector('button').addEventListener('click', () => {
    if (wasm) {
        const memory = new Uint8Array(wasm.instance.exports.memory.buffer);
        memory.set(new TextEncoder().encode(input.value + "\x00"));

        const resultAddr = wasm.instance.exports.checkPassword(0);

        const end = memory.indexOf(0, resultAddr);

        response.innerText = "Response: " + new TextDecoder().decode(memory.subarray(resultAddr, end));
    } else {
        response.innerText = "Please try again in a few seconds";
    }
}, 1);

In the second chunk you can see that wasm.instance.exports.checkPassword is called. In the first chunk you can see that it comes from ("GET", "./code.wasm"). I hexdump’d the wasm, and found the flag in plaintext (it would also be found in strings…).

For completeness: When the user enters WASMP4S5W0RD, an element will be added below with Response: bcactf{w4sm-m4g1c-xRz5}

Flag: bcactf{w4sm-m4g1c-xRz5}

WASM Protected Site 2

Similar to wasm protected site 1, but this time there is no password, only the flag.

Enter the flag, and the program will check it for you

Author: Andrew

Same as before, we’re given a wasm flag checker. I tried a couple of different things including attempting to generate c and look at it in IDA, but ran into

In file included from code.c:10:0:
wasm-rt-impl.h:22:10: fatal error: wasm-rt.h: No such file or directory
 #include "wasm-rt.h"
          ^~~~~~~~~~~
compilation terminated.

and didn’t know how to fix it. Eventually settled on using wasm-decompile, which just tries very hard to give something legible. Comments and reasonable variable names are my own.

;; generated with: wasm-decompile code.wasm

export memory memory(initial: 1, max: 0);

data d_bjsxPKMH7ND3bPRe(offset: 1000) = 
"bjsxPKMH|"7N\1bD\043b]PR\19e%\7f/;\17";

function cmp(a:int, b:int):int {
  var iterator:int;
  var v0:int;
  var v1:int;
  loop L_a {
    if ((iterator + v0)[0]:ubyte != ((iterator + v1)[0]:ubyte ^ (iterator * 9 & 127)) & 
        iterator != 27) {  ;; loop limit
      return 0
    }
    iterator = iterator + 1;
    if (eqz((iterator - 1 + v0)[0]:ubyte)) { return 1 }
    continue L_a;
  }
  return 0;
}

export function checkFlag(a:int):int {
  return cmp(a, 1000)
}

From here I understood what to do - each character of the ciphertext is XOR’d according to its index into the flag, and then compared against my input.

>>> ct = b"bjsxPKMH|\"7N\x1bD\x043b]PR\x19e%\x7f/;\x17"
>>> ''.join([chr(c ^ ((i * 9) & 0x7f)) for i, c in enumerate(ct)])
'bcactf{w4sm-w1z4rDry-Xc0wZ}'

Flag: bcactf{w4sm-w1z4rDry-Xc0wZ}