Writeup: super_otp

One time pads are perfectly secure.* Here is an example of an extra perfectly secure implementation.

# get 3 one time pads
otps = [urandom(MAX_LENGTH) for _ in range(3)]

# combine the one time pads into a "super" one time pad
super_otp = xor_list(otps)

# encrypt the flag using the super one time pad
enc_flag = xor_bytes(FLAG.encode(), super_otp)

# print out the encrypted flag
print(f'The encrypted flag in base 64:')
print(b64encode(enc_flag).decode())

for otp in otps:
    # get the message that the user wants to encrypt (up to 60 characters)
    print('Enter message you would like to encrypt (input truncated to 60 characters): ')
    user_message = str(input())[:60]

    # encrypt the message using xor
    enc_message = xor_bytes(user_message.encode(), otp)

    # print encrypted message
    print(f'Your encrypted message is in base 64:')
    print(b64encode(enc_message).decode())

If I had a nickel for every time this challenge reused a key, I’d have zero nickels. Which isn’t a lot, but – wait a second, zero nickels? We don’t reuse the key at all? So it’s perfectly secure?

The encrypted flag in base 64:LtVYq6Y989bBhQ4S4hEJ8NgsOzNUs1DxJy6YEyAoEnter message you would like to encrypt (input truncated to 60 characters): NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
Your encrypted message is in base 64:
Wyn1xCotf77chnIDI5qvv2ttfdR7sfe4ylx3jbma34PUk70ngFUJQDaIO3WWmg5YdznQdlZjn9DiEnter message you would like to encrypt (input truncated to 60 characters): NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
Your encrypted message is in base 64:
wgL55ruDV80AgYSPD2Ec0xDA8JYgp+/WpaC7qkKb4NJ45bAkILw66ZfnwNigbI9d0rAMBfqR7dSv
Enter message you would like to encrypt (input truncated to 60 characters): NOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO
Your encrypted message is in base 64:lsJuvRa54tkgkuXi9PawjKOgymF3g0rjWM1bP7AbjGnNfvawy38iA+TuTDvc9Wejj5KWrto5s

OK, so it’s sort of reused. But more importantly, we can control the plaintext. He who controls the plaintext, controls the key. Or at least can figure out what the key is.

Anyways, one aspect of OTP’s is that the key and plaintext are interchangeable. If you know the plaintext, you can XOR it with the ciphertext and it will reveal the key. Similarly, XORing “any bytes” with null bytes results in “any bytes.” That was a confusing way to say that XOR does nothing when one or more sides is a 0. So if we send 60 null bytes instead of “NOOOOOO”, we get to see what the key for each message was.

With that, we send 60 null bytes to see what the key for each message was

Then, we XOR them together.

Then, we XOR that with the bytes of the flag.

Then, we have the flag in plaintext.

Submitting osu{nev3r_R3uSE_On3_7iM3_P@D$} completes the challenge!

* Unless they're part of a CTF challenge

Writeup: pop quiz

POP QUIZ TIME! Can you solve these challenging questions?

What number am I thinking of?

If I were a human, it would be difficult to know what the answer to this question might be1. Thankfully, I am a computer, and will bare my essential nature to anyone with a keyboard. How about you decompile me and figure out what I’m thinking? In fact, I’ll do it for you.

bool question_1(void)

{
  long in_FS_OFFSET;
  int local_18;
  int local_14;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_14 = 0x2325;
  printf("What number am I thinking of? ");
  __isoc99_scanf(&DAT_00100e27,&local_18);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return local_14 == local_18;
}

First, I check if local_14 (my secret number) is equal to local_18 (your input). local_14 is assigned a value of 0x2325 (in my language). Converting to meat numbers gives us 8997, which is hopefully the number you were thinking of already. Regardless,

gimme some characters

This isn’t even a question! It’s a command! And it’s so vague! Does it mean fictional characters? historical figures? This quiz sucks!

These are all things you might be saying if this had been written by a human. Thankfully, you’re asking a computer, who is more than willing to bare its essential nature to anyone with a keyboard. Isn’t this easy?

bool question_2(void)

{
  long in_FS_OFFSET;
  undefined4 local_14;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Gimme some characters: ");
  local_14 = 0;
  __isoc99_scanf("\n%c%c%c%c",&local_14,(long)&local_14 + 1,(long)&local_14 + 2,(long)&local_14 + 3)
  ;
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return (int)local_14._3_1_ + (int)(char)local_14 + (int)local_14._1_1_ + (int)local_14._2_1_ ==
         0x1a0;
}

If you’re paying attention, you’ll notice that I don’t particularly care what the characters are. I just want them to add up to 0x1a0. Which is 416 in meat numbers. It’s time for you to google “ASCII table” once again and click the top result which is really the worst result because it’s not ctrl-F’able.5

I also am only scanfing 4 characters, so please be concise. “hhhh” works for me.2

???

Screw you, meatbag! You don’t even get a vague command anymore!

void question_3(void)

{
  long in_FS_OFFSET;
  undefined4 local_14; // int/uint
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("???");
  local_14 = 0;
  __isoc99_scanf(&DAT_00100e27,&local_14);
  check(local_14); // Check runs on the integer
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

Haha! This function doesn’t even return anything! That’ll throw you for a loop! What’s that, you didn’t even notice and have begun to reverse check(local_14)??3

undefined8 check(uint guess)

{
  int iVar1;
  int WHAT;
  size_t sVar2;
  undefined8 WIN;
  long lVar3;
  undefined8 *inputPointer;
  long in_FS_OFFSET;
  int local_128;
  int local_124;
  undefined8 input;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  inputPointer = &input;
  for (lVar3 = 0x1f; lVar3 != 0; lVar3 = lVar3 + -1) {
    *inputPointer = 0;
    inputPointer = inputPointer + 1;
  }
  *(undefined4 *)inputPointer = 0;
  *(undefined2 *)((long)inputPointer + 4) = 0;
  *(undefined *)((long)inputPointer + 6) = 0;
  sprintf((char *)&input,"%d",(ulong)guess);
  sVar2 = strnlen((char *)&input,0xff);
  WHAT = (int)sVar2;
  if ((sVar2 & 1) == 0) {
    if (WHAT < 4) {
      WIN = 0;
    }
    else {
      for (local_128 = 1; iVar1 = WHAT / 2, local_128 < WHAT / 2; local_128 = local_128 + 1) {
        if (*(char *)((long)&input + (long)local_128) <=
            *(char *)((long)&input + (long)(local_128 + -1))) {
          WIN = 0;
          goto function1;
        }
      }
      do {
        local_124 = iVar1 + 1;
        if (WHAT <= local_124) {
          WIN = 1;
          goto function1;
        }
        lVar3 = (long)iVar1;
        iVar1 = local_124;
      } while (*(char *)((long)&input + (long)local_124) < *(char *)((long)&input + lVar3));
      WIN = 0;
    }
  }
  else {
    WIN = 0;
  }
function1:
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return WIN;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

Try reversing this, bucko! You don’t stand a chance! Wait, what’s that? You still had 8997 in your clipboard and entered that and got the flag? Wait, you even added some gibberish text to the end and it passed my function?4 That’s awesome 🙂

Hacking is all about playing by the rules however, so let’s build some character and actually look at what this does (pardon the partial variable renames):

undefined8 check(uint guess)

{
  int iVar1;
  int WHAT;
  size_t sVar2;
  undefined8 WIN;
  long lVar3;
  undefined8 *inputPointer;
  long in_FS_OFFSET;
  int local_128;
  int local_124;
  undefined8 input;
  long local_10;
  

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  // Honestly the next lines are so ugly. They look like they're zeroing out
  // input via inputPointer, so we'll go with that
  inputPointer = &input;
  for (lVar3 = 0x1f; lVar3 != 0; lVar3 = lVar3 + -1) {
    *inputPointer = 0; // null bytes
    inputPointer = inputPointer + 1;
  }
  *(undefined4 *)inputPointer = 0;
  *(undefined2 *)((long)inputPointer + 4) = 0;
  *(undefined *)((long)inputPointer + 6) = 0;
  sprintf((char *)&input,"%d",(ulong)guess); // Scan guess into input
  sVar2 = strnlen((char *)&input,0xff); // These lines put the length
  WHAT = (int)sVar2;                    // of input into WHAT
  if ((sVar2 & 1) == 0) { // strlen must be even
    if (WHAT < 4) {      // Input has to be at least 4 char
      WIN = 0;           // As soon as I got to this function I just tossed
    }                    // in 8997 and won. lol. onwards!
    else {
      // Looks like we're looping through the first half of the string
      for (local_128 = 1; iVar1 = WHAT / 2, local_128 < WHAT / 2; local_128 = local_128 + 1) {
        // If the character at local_128 is <= the previous one, fail
        if (*(char *)((long)&input + (long)local_128) <=
            *(char *)((long)&input + (long)(local_128 + -1))) {
          WIN = 0;
          goto function1; // Considered harmful
        }
      }
      // Let's not pretend that either of us are fully familiar with
      // do while syntax.
      // But it's gotta be somewhat intuitive, right?
      // Looks like this increments iVar1 and local_124
      // starting just after where we stopped
      do {
        local_124 = iVar1 + 1;
        // If we've iterated past the end of the string, win!
        if (WHAT <= local_124) {
          WIN = 1;
          goto function1;
        }
        lVar3 = (long)iVar1;
        iVar1 = local_124; // iVar1 gets set to where the first loop ended
      } while (*(char *)((long)&input + (long)local_124) < *(char *)((long)&input + lVar3)); // Same as the last loop, but descend
      WIN = 0; // Otherwise, lose
    }
  }
  else {
    WIN = 0;
  }
function1:
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return WIN;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

This function walks up the first half of the number, expecting each digit to be greater than the last. It then walks back down, expecting each digit to be lesser than the last. Any number that fits this criteria will work, but 8997 happens to be a convenient test case 🙂

footnotes

1: The most common PIN numbers are 1234, 1111, and 0000, in case you ever find a debit card on the street and want to take your chances at the ATM.

2: There also might be junk after the bytes you send in. Testing shows that 0xD0 + 0xD0 + 0x00 + 0x00 (ÐÐ), which should sum correctly does not succeed. It doesn’t I don’t seem to like extended ASCII codes anyways, but bonus points if you can figure out a way to do it with fewer characters. I think I’ll drop the computer roleplay for the rest of the footnotes

3: This is, presumably, a Ghidra bug. Anyways, I didn’t notice until afterward. And I started by googling various permutations of “check c” because I had forgotten that it was a handmade function.

4:

Pin em wojak
Nooo you can’t just paste gibberish you’re supposed to read through my incomprehensible function!!
8997abagaga go brrrrr

5: Seriously, I need to permanently remove that site from search results. Sorry this footnote is out of order, I’m putting them in by hand. Next project a writeup bloginator with footnote capabilities that would bring David Foster Wallace to jealous tears.

Writeup: bounce_the_flag

OSUSEC CTF League is back, baby! And it’s on Mondays! Piping hot writeups are back on the regular menu and we’re getting right into things with a classic SQL injection.

The challenge: “Let’s kick things off with one of my favorite classic games: Bounce the Flag! Bounce the flag is an immersive hyper-realistic gaming experience blah blah blah. One of Bounce the Flag’s most celebrated competitors, Mr. Flag, blahdie blahdie blah, forgot the password to his account, blah blah blah”

Forget about all that stuff – It’s gaming time!

Sweet! High score!

Awesome! I just got a high score! Time to record this epic win on the Bounce the Flag HOF!

What?! I definitely typed my password in right, but I must not have an account. I’m mad! This piece of gaming history deserves to be on the leaderboard! I’m gonna get this score up on the leaderboard, mark my words.

Luckily, we have access to the source code of the server and have been told ahead of time that the server is vulnerable to SQL injection 😀

username = request.form['username_input']
password = request.form['password_input']

res = sql_fetchall(
        connection,
        f"""
        SELECT score, game_time
        FROM users
        INNER JOIN games
        ON users.id = games.user_id
        WHERE username = '{username}' AND password = '{password}'
        ORDER BY game_time
        """
    )

Our opportunity lies in the unsanitized username and password field. Submitting

Mr. Flag' -- 

gives us the message “Pfffffft you call that a high score?!! Try again when you score at least 1337 points!”

As the score is held client-side, opening up the dev console and entering score=1338 is enough to log in and save our score.

If we can modify our username or password to break the SQL request for the stats page, we’re golden.

I had trouble crafting the username statement, so I switched to putting the exploit in the password, with the final exploit being:

Username: Mr. Flag
Password ' or 1=1 union select password,username from users --