Skip to content

Instantly share code, notes, and snippets.

@Lala5th
Last active April 10, 2022 00:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lala5th/32e3f6ee75ad206a396f54242854cf4e to your computer and use it in GitHub Desktop.
Save Lala5th/32e3f6ee75ad206a396f54242854cf4e to your computer and use it in GitHub Desktop.
ICTF FlagLocker Writeup

Decompilation

We can start by decompiling the binary. The symbols have been stripped, so after locating main and some deobfuscation we can get:

int8_t main(void){
  char cVar1;
  undefined aux_buffer [48];
  undefined flag_buffer [40];
  int current_char;
  int loop_count;
  
  puts("Hello! Mind if I ask you for the flag?");
  __isoc99_scanf(&read_38_chars,flag_buffer);
  loop_count = 0;
  current_char = 0;
  do {
    if (0x25 < current_char) {
LAB_0040134a:
      if (loop_count < 0x13) {
        puts("That\'s not it! Oh well :(");
      }
      return '\0';
    }
    cVar1 = FUN_00401238(current_char,flag_buffer,aux_buffer,flag_buffer);
    if (cVar1 != '\0') {
      loop_count = loop_count + 1;
    }
    if (0x12 < loop_count) {
      print_success();
      goto LAB_0040134a;
    }
    current_char = current_char + 2;
  } while( true );
}

We can see that there is some check in the middle for the return of FUN_00401238. Ghidra struggles quite a bit with the call signature of this particular function so at first let's just explore without looking further into this

Using gdb we find that the flag is checked 2 letters at a time, which leads to the names we used in deobfuscating the loop variables. We find this by inputting test cases "ICTF{", "IDTF{" and similar patterns. We also observe that the first two and the second two letters are checked independently i.e. the first two doesn't need to be correct for the second check to succeed.

Brute force

We can now use r_socket to brute force this using radare2. The code we use does not rerun the program with new stdin, but rather jumps to before the checks and rewrites the flag_buffer. We then step through each iteration of the loop and if %al is 0 after the check then the pair of letters is stepped through the alphabet. This is done while we don't end up at end of the program (checked via a breakpoint and assertion that we are not at that breakpoint). The code implementing this is:

#include <r_socket.h>

#include <string>
#include <iostream>
#include <sstream>
#include <cstring>

static std::string r2cmd(R2Pipe *r2, std::string cmd) {
	char *msg = r2pipe_cmd (r2, cmd.c_str());
    std::string ret;
	if (msg) {
        ret = std::string(msg);
		//printf ("%s\n", msg);
		free (msg);
	}
    return ret;
}

int main(void){
    /*
     0x40134a => 4199242 -> The program reached the end
     0x401301            -> The loop counters are set to 0
                            The new guess can be written
     0x401326            -> The result of the letter check is in %al
    */
    std::stringstream ss;
    std::string alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw_.!?{}";
    size_t flag_i [0x13];
    memset(flag_i,0,0x13*sizeof(size_t));

    std::string flag = "";
    for (size_t i = 0; i < 2*0x13; ++i)
        flag.push_back(alphabet[0]);

    R2Pipe *r = r2pipe_open("r2 -q0 -d -2 ./locker");

    r2cmd(r, "dor stdin=rev_shell.php");
    r2cmd(r, "doo");
    r2cmd(r, "db 0x40134a");
    r2cmd(r, "dcu 0x401301");
    std::string prev_guess = "";
    while (true){

        for (size_t i = 0; i < 0x13; ++i){
            flag[2*i] = alphabet[flag_i[i] % alphabet.size()];
            flag[2*i + 1] = alphabet[flag_i[i] / alphabet.size()];
        }

        if (prev_guess == flag)
            break;

        prev_guess = flag;
        r2cmd(r, "dr rip = 0x401301");
        size_t curr_lttr = 0;
        std::cout << "Guess: " << flag << "\n";

        r2cmd(r, "w " + flag + " @ rbp-0x30");
        std::string n_flag = flag;
        for (size_t curr_lttr = 0; true; ++curr_lttr){

            r2cmd(r, "dcu 0x401326");
            size_t rip;
            ss << std::hex << r2cmd(r, "dr rip");
            ss >> rip;

            if (4199242 == rip)
                break;

            size_t al;
            ss << std::hex << r2cmd(r, "dr al");
            ss >> al;
            if (al == 0 && curr_lttr < 0x13){
                ++flag_i[curr_lttr];
            }
        }
    }

    std::cout << "Correct: " << prev_guess << "\n";

    r2pipe_close(r);
    return 0;
}

Using this we quickly find that the flag is ICTF{4h4tS_n0T_mY_S3GmeNtAt10N_f45LT!}. This does not seem correct though and indeed there is a conflict in the checking function and the correct flag can be recovered via common sense to be ICTF{th4tS_n0T_mY_S3GmeNtAt10N_f45LT!}

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