pwnEd 2 Breaking The Simulation

Published: Feb. 28, 2021, 8:45 p.m.

pwnEd 2 by University of Edinburgh was one of the first proper CTFs I have attended, and I thoroughly enjoyed it. Simulation was by far one of the most fun challenges.

TL;DR

I essentially analyzed and reimplemented a CPU instruction set line by line in Rust, and then bruteforced the flag. I felt accomplished, but spoiler alert, it could have been done way simpler. Not that I have ever heard of angr, so here goes the rest of the post!

First open

When you get to main, there will be a greeting, of hundreds of single char variables... Not the most pleasant, but upon closer inspection, it's just an array! After cleaning it up, the start looks like so:

int main()
{
  input_table input1; // [rsp+28h] [rbp-1B8h]
  char stackstr[406]; // [rsp+40h] [rbp-1A0h]
  unsigned int result; // [rsp+1DCh] [rbp-4h]

  result = 0;
  printf("Please enter the flag to the flag checker:");
  fgets(input, 32, stdin);
  if ( strlen(input) == 31 )
  {
  ...
  }
}

Afterwards come bytes mixed up with the input, like so:

stackstr[0] = 2;
stackstr[1] = input[0];
stackstr[2] = 0;
qmemcpy(&stackstr[3], "\x01\r\x01\t\x03\x01\x02", 7);
stackstr[10] = input[1];
stackstr[11] = 0;
qmemcpy(&stackstr[12], "\x02\x0E\x02\x04\x03\x02\x02", 7);
stackstr[19] = input[2];

But what is it? It seems like some list of instructions, and in fact, it is.

Let's dig deeper

Afterwards come 4 function calls:

setup(&input1, stackstr);
run(&input1);
print_output(&input1);
destruct((__int64)&input1);

destruct is self expanatory, it doesn't interest us. Opening up setup you will be greeted with lots of function calls to address 0x402020, and 0x402040, taking the same argument every time:

*(_QWORD *)(sub_402020(a1 + 8) + 8) = a2;
v5 = sub_402040(a1) - 1;
*(_QWORD *)(sub_402020(a1 + 8) + 16) = v5;
v4 = sub_402040(a1) - 1;
*(_QWORD *)(sub_402020(a1 + 8) + 24) = v4;
v3 = sub_402040(a1 + 16) + 30;
*(_QWORD *)(sub_402020(a1 + 8) + 32) = v3;
*(_BYTE *)(sub_402020(a1 + 8) + 7) = 1;

The functions are matrioshkas that call another function immediately after, take the first one for instance:

__int64 __fastcall sub_402020(__int64 a1)
{
  return sub_402560(a1);
}

This behaviour continues until the function at 0x402620, which simply returns the input:

__int64 __fastcall sub_402620(__int64 a1)
{
  return a1;
}

All functions in the chain are the same, apart from this one:

__int64 __fastcall sub_4025A0(__int64 a1)
{
  return *(_QWORD *)sub_4025C0(a1);
}

So, all that this chain does is just dereferences a pointer and returns it back, so, let's just rename the root function to something we can remember:

some_new_struct *__fastcall deref_self_for_newstruct(some_new_struct **a1)
{
  return (some_new_struct *)sub_402560((__int64)a1);
}

After renaming the function, and defining proper structures we get a more manageable output:

deref_self_for_newstruct(&input_table->newstruct)->stack_ptr = stackstr;
input_minus_one = (char *)(deref_self_lmao2((__int64)input_table) - 1);
deref_self_for_newstruct(&input_table->newstruct)->tape_pos = input_minus_one;
input_minus_one = (char *)(deref_self_lmao2((__int64)input_table) - 1);
deref_self_for_newstruct(&input_table->newstruct)->input_minus_one_2 = input_minus_one;
last_array1f_elem = (void *)(deref_self_lmao2((__int64)&input_table->array_1f_some_key) + 30);
deref_self_for_newstruct(&input_table->newstruct)->array_1f_last = last_array1f_elem;
deref_self_for_newstruct(&input_table->newstruct)->c7_set_true = 1;
result = deref_self_lmao2((__int64)&input_table->array_1f_some_key);

Now, I skipped through a step of figuring out the actual structure of the data, for that, we need to check the first 3 function calls:

v6 = (some_new_struct *)(create_and_assign_to((__int64)input_table, 0x3E8uLL) + 8);
create_newstruct(v6);
create_and_assign_to((__int64)&input_table->array_1f_some_key, 0x1FuLL);

create_and_assign_to simply callocs and assigns the buffer to input variable:

__int64 __fastcall create_and_assign_to(__int64 a1, unsigned __int64 a2)
{
  void *newed_array; // [rsp+0h] [rbp-30h]

  newed_array = (void *)operator new[](a2);
  memset(newed_array, 0, a2);
  assign_new_to_first_ptr(a1, (__int64)newed_array);
  return a1;
}

The assignment is done in assign_new_to_first_ptr. create_newstruct is the interesting bit:

__int64 __fastcall create_newstruct(some_new_struct *a1)
{
  some_new_struct *v2; // [rsp+8h] [rbp-38h]

  v2 = (some_new_struct *)operator new(0x28uLL);
  zero_newstruct(v2);
  assign_new_to_first_ptr2((__int64)a1, (__int64)v2);
  return (__int64)a1;
}

This immediately tells us that the structure's size is 40 bytes. Then, we have the zero_newstruct function:

some_new_struct *__fastcall zero_newstruct(some_new_struct *a1)
{
  some_new_struct *result; // rax

  result = a1;
  a1->c0 = 0;
  a1->c1 = 0;
  a1->c2 = 0;
  a1->c3 = 0;
  a1->c4 = 0;
  a1->c5 = 0;
  a1->c6 = 0;
  a1->c7_set_true = 0;
  a1->stack_ptr = 0LL;
  a1->tape_pos = 0LL;
  return result;
}

This pretty much tells us the actual layout of the structure. I picked naming for stack_ptr from the setup function, and tape_pos from the code we are going to reverse in just a bit... Same with c7, that field holds significant value.

These fields get initialized in setup function, where the last field of the struct also gets initialized:

key_pointer = deref_self_lmao2((__int64)&input_table->array_1f_some_key);
*(_QWORD *)key_pointer = *(_QWORD *)key_thing;
*(_QWORD *)(key_pointer + 8) = *(_QWORD *)&key_thing[8];
*(_QWORD *)(key_pointer + 16) = *(_QWORD *)&key_thing[16];
*(_DWORD *)(key_pointer + 24) = *(_DWORD *)&key_thing[24];
*(_WORD *)(key_pointer + 28) = *(_WORD *)&key_thing[28];
*(_BYTE *)(key_pointer + 30) = key_thing[30];

This key_thing, as the name implies, is the key thing, and has this byte sequence:

[
    0x79, 0x73, 0x4E, 0x2B, 0x23, 0x0B4, 0x5B, 0x33, 0x35, 0x7E, 0x7A,
    0x13, 0x69, 0x74, 0x4E, 0x10, 0x52, 0x76, 0x0FA, 0x0EC, 0x69,
    0x0, 0x2C, 0x35, 0x75, 0x64, 0x18, 0x57, 0x33, 0x78, 0xED
]

Full structures look like so:

struct some_new_struct
{
  char c0;
  char c1;
  char c2;
  char c3;
  char c4;
  char c5;
  char c6;
  char c7_set_true;
  char *stack_ptr;
  char *tape_pos;
  char *input_minus_one_2;
  char *array_1f_last;
};

struct input_table
{
  char (*newed_array)[1000];
  some_new_struct *newstruct;
  char (*array_1f_some_key)[31];
};

We are done with the setup code, let's get on with the rest!

A quick peek to the end

After reversing the structure layout, print_output reveals an incredibly important detail:

if ( deref_self_for_newstruct(&a1->newstruct)->c7_set_true )
  result = printf("Correct, congratulations!");
else
  result = printf("Incorrect.. Sorry");

As you can see, the flag is only correct when c7 is true! And this value is true by default, let's figure out where it goes false...

Virtual hell

Opening up run, we can see a while loop with a read from stack buffer, and a switch statement:

while ( 1 )
{
  result = *(unsigned __int8 *)deref_self_for_newstruct(&a1->newstruct)->stack_ptr;
  switch ( result )

Afterwards we have a 16 switch cases (and a default break one), which call into unique functions. They are rather short, but I decided to go through and find where c7 gets set to false, which turns out to be case 4:

__int64 __fastcall the_one_that_sets_c7_to_false(input_table *a1)
{
  some_new_struct *result; // rax

  deref_self_for_newstruct(&a1->newstruct)->c7_set_true = 0;
  result = deref_self_for_newstruct(&a1->newstruct);
  ++result->stack_ptr;
  return (__int64)result;
}

This one is rather straightforward, it sets c7 to false, and increments the stack pointer. At this point there is no doubt that the big buffer in main is a list of instructions. Now, all we need to do, is not hit 4 as an instruction. Haha, as simple as that! Now, how are we going to do that?

Big rewrite

I played around with gdb scripts for a bit, looked at the functions, but felt like I just couldn't get my head wrapped around this by just looking at it. So, I decided to simply rewrite this program. I picked Rust, because that's what I'm most comfortable with. However, I was just too lazy to deal with it being safe code, so readers beware!!!

At first I defined the structures:

struct InputTable
{
    registers: [u8; 4],
    c6: bool,
    stack_ptr: *mut u8,
    tape_pos: *mut u8,
    identity: *mut u8,
    target_key_index: usize,
    target_key: [u8; 31],
}

Spoiler alert, the 1000 byte array that gets created in setup is never used! c5 is never used either! After a glance at the functions, c1 through c4 seem like some sort of registers, so I called them that, registers. Here, array_1f_last becomes an index (called target_key_index), rather than pointer, because that target_key could move around if we move the struct around. Creating this structure is rather simple:

pub unsafe fn new(input: *mut u8) -> Self {
    Self {
        registers: [0; 4],
        c6: false,
        stack_ptr: input,
        tape_pos: input.sub(1),
        identity: input.sub(1),
        target_key_index: 30,
        target_key: [
            0x79, 0x73, 0x4E, 0x2B, 0x23, 0x0B4, 0x5B, 0x33, 0x35, 0x7E, 0x7A,
            0x13, 0x69, 0x74, 0x4E, 0x10, 0x52, 0x76, 0x0FA, 0x0EC, 0x69,
            0x0, 0x2C, 0x35, 0x75, 0x64, 0x18, 0x57, 0x33, 0x78, 0xED
        ],
    }
}

The main function creates the context, and simply goes through the loop:

let mut tape = create_tape(key);
let mut ctx = InputTable::new(tape.as_mut_ptr());

let success = loop {
    match *ctx.stack_ptr {
        0 => ctx.case_0(),
        1 => ctx.case_1(),
        2 => ctx.case_2(),
        3 => ctx.case_3(),
        4 => break false,
        5 => ctx.case_5(),
        6 => ctx.case_6(),
        7 => ctx.case_7(),
        8 => ctx.case_8(),
        9 => ctx.case_9(),
        10 => ctx.case_10(),
        11 => ctx.case_11(),
        12 => ctx.case_12(),
        13 => ctx.case_13(),
        14 => ctx.case_14(),
        15 => ctx.case_15(),
        16 => ctx.case_16(),
        _ => break true
    }
};

As for create_tape, I just duct-taped the disasm output to compile on rust (pun surprisingly not intended):

// does it look to you like I have time?
pub fn create_tape(key: [u8; 31]) -> [u8; 406] {
    let mut stackstr = [0; 406];

    stackstr[0] = 2; // case 0
    stackstr[1] = key[0];
    stackstr[2] = 0;
    stackstr[3..10].copy_from_slice(b"\x01\r\x01\t\x03\x01\x02");
    ...
    stackstr
}

For some behind the scenes, I left a few comments from the middle of my thought process, check it out on the repo.

Now was the time to implement all these 16 functions... They aren't all that special, it was just tedious to rewrite them. But here are all the cases:

Case 0, just reads into registers:

unsafe fn case_0(&mut self) {
    let p1 = self.tape_pos;
    let p2 = self.identity;
    if p1 != p2 {
        let c = *self.stack_ptr.add(1) as usize;
        if c > 0 && c <= 4 {
            self.registers[c - 1] = *self.tape_pos;
        }
    }
    self.tape_pos = self.tape_pos.sub(1);
    self.stack_ptr = self.stack_ptr.add(2);
}

Case 1, puts the target key into the registers. I call it xor key, because it is used for xoring later on, just not in the trivial way...

// This gets the xor key out to the registers
unsafe fn case_1(&mut self) {
    let c = *self.stack_ptr.add(1) as usize;
    if c > 0 && c <= 4 {
        self.registers[c - 1] = self.target_key[self.target_key_index];
    }
    self.target_key_index -= 1;
    self.stack_ptr = self.stack_ptr.add(2);
}

Case 2, writes onto the stack/tape (I started to think of this all like a turing machine lol):

unsafe fn case_2(&mut self) {
    let c = *self.stack_ptr.add(1);
    self.tape_pos = self.tape_pos.add(1);
    // Override tape...
    *self.tape_pos = c;
    self.stack_ptr = self.stack_ptr.add(2);
}

Case 3, writes a register value to the tape, or zero

unsafe fn case_3(&mut self) {
    let c = *self.stack_ptr.add(1) as usize;
    let v = if c > 0 && c <= 4 {
        self.registers[c - 1]
    } else {
        0
    };
    self.tape_pos = self.tape_pos.add(1);
    *self.tape_pos = v;
    self.stack_ptr = self.stack_ptr.add(2);
}

Case 4, I didn't implement it, instead, I made it an explicit break out of the loop.

Case 5, compare 2 registers, set c6 flag:

unsafe fn case_5(&mut self) {
    let c1 = *self.stack_ptr.add(1) as usize;
    let c2 = *self.stack_ptr.add(2) as usize;

    let v1 = if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1]
    } else {
        0
    };

    let v2 = if c2 > 0 && c2 <= 4 {
        self.registers[c2 - 1]
    } else {
        0
    };

    self.c6 = v1 == v2;
    self.stack_ptr = self.stack_ptr.add(3);
}

Case 6 moves values between registers:

unsafe fn case_6(&mut self) {
    let c2 = *self.stack_ptr.add(2) as usize;

    let v2 = if c2 > 0 && c2 <= 4 {
        self.registers[c2 - 1]
    } else {
        0
    };

    let c1 = *self.stack_ptr.add(1) as usize;
    if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1] = v2;
    }

    self.stack_ptr = self.stack_ptr.add(3);
}

Cases 7, and 8, jump based on the flag value:

// Skips variable amount of steps...
unsafe fn case_78(&mut self, variable: bool) {
    if variable {
        let v = *self.stack_ptr.add(1) as usize;
        self.stack_ptr = self.stack_ptr.add(v);
    } else {
        self.stack_ptr = self.stack_ptr.add(2);
    }
}

unsafe fn case_7(&mut self) {
    self.case_78(self.c6);
}

unsafe fn case_8(&mut self) {
    self.case_78(!self.c6);
}

Case 9, jump backwards:

unsafe fn case_9(&mut self) {
    let c1 = *self.stack_ptr.add(1) as usize;
    self.stack_ptr = self.stack_ptr.sub(c1);
}

At this point some fun instructions are added :)

Case 10:

// xors!
unsafe fn case_10(&mut self) {
    let c1 = *self.stack_ptr.add(1) as usize;
    let xor_key = *self.stack_ptr.add(2);

    if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1] ^= xor_key;
    }

    self.stack_ptr = self.stack_ptr.add(3);
}

Cases 11, and 12 are interesting, they rotate bits left or right, implemented as a few shifts and ors in the disassembled code, but here we can use a simple function call:

// rotate bits
unsafe fn case_11(&mut self){
    let c1 = *self.stack_ptr.add(1) as usize;
    let rotate_by = *self.stack_ptr.add(2) as u32;

    if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1] = self.registers[c1 - 1].rotate_right(rotate_by);
    }

    self.stack_ptr = self.stack_ptr.add(3);
}

unsafe fn case_12(&mut self){
    let c1 = *self.stack_ptr.add(1) as usize;
    let rotate_by = *self.stack_ptr.add(2) as u32;

    if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1] = self.registers[c1 - 1].rotate_left(rotate_by);
    }

    self.stack_ptr = self.stack_ptr.add(3);
}

Cases 13, 14, 15, and 16 implement some arithmetic operations:

// arithmetic!
unsafe fn case_13(&mut self){
    let c1 = *self.stack_ptr.add(1) as usize;
    let add_val = *self.stack_ptr.add(2);

    if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1] = self.registers[c1 - 1].wrapping_add(add_val);
    }

    self.stack_ptr = self.stack_ptr.add(3);
}

unsafe fn case_14(&mut self){
    let c1 = *self.stack_ptr.add(1) as usize;
    let sub_val = *self.stack_ptr.add(2);

    if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1] = self.registers[c1 - 1].wrapping_sub(sub_val);
    }

    self.stack_ptr = self.stack_ptr.add(3);
}

unsafe fn case_15(&mut self){
    let c1 = *self.stack_ptr.add(1) as usize;
    let mul_val = *self.stack_ptr.add(2);

    if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1] = self.registers[c1 - 1].wrapping_mul(mul_val);
    }

    self.stack_ptr = self.stack_ptr.add(3);
}

unsafe fn case_16(&mut self){
    let c1 = *self.stack_ptr.add(1) as usize;

    if c1 > 0 && c1 <= 4 {
        self.registers[c1 - 1] += 1;
    }

    self.stack_ptr = self.stack_ptr.add(2);
}

Holy cow! As I was writing this write-up, I realized it is a completely self sufficient instruction set! The only instr we missing is the failure flag, but damn, you, xeno, you got me to implement a CPU! Nice job!

Exactly, if we just rename the cases, we get a real nice emulated instruction set!

let success = loop {
    match *ctx.stack_ptr {
        0 => ctx.pop_reg_op(),
        1 => ctx.read_secret_op(),
        2 => ctx.push_mem_op(),
        3 => ctx.push_reg_op(),
        4 => break false,
        5 => ctx.cmp_op(),
        6 => ctx.mov_op(),
        7 => ctx.je_op(),
        8 => ctx.jne_op(),
        9 => ctx.jmp_back_op(),
        10 => ctx.xor_op(),
        11 => ctx.ror_op(),
        12 => ctx.rol_op(),
        13 => ctx.add_op(),
        14 => ctx.sub_op(),
        15 => ctx.mul_op(),
        16 => ctx.inc_op(),
        _ => break true
    }
};

Now, once this is all done, how are we going to crack the key?

The crackening

At this point I had the instruction set implemented, but mind you, I still was working with unnamed functions, and the fact that the push instructions overwrote the instructions themselves didn't help with the mystery of how this all worked. Also, now thinking, that 1000 byte buffer was probably meant for the stack, but was later scrapped :D

So, I did what I do best - behavioral analysis. I added a few prints here and there, tracking how the instruction flow went, and one thing I noticed was that only the last 4 byte was ever hit as an instruction. Meaning, all other 4 bytes would either be hit when I change my input, or are never used as opcodes, but rather as their operands.

But the key thing I did, was notice that things were being pushed on top of the stack, so I decided to print out the first 31 bytes of the stack. To my surprise, it would output some gibberish, that precisely depended on the input key. But it was no ordinary xor relationship. Yes, the output of the first 2 bytes would change predictably based on old_char ^ new_char, so would the fourth, but then, the third one would change in surprising ways. That means, I can't just apply something on the target_key and get the key out, no. We are still missing something...

But then, I noticed something crucial! Take a look at this flag:

pwned{aaaaaaaaaaaaaaaaaaaaaaaa}

When I print the contents of the stack, this is what I get:

79 73 4e 2b 23 b4 f2 64 23 40 4a 06 6b 62 41 41 41 63 0b c2 5d 11 6c 66 64 62 b0 52 16 66 ed

Does this strike any resemblence? No? Take a look at target_key:

79 73 4e 2b 23 b4 5b 33 35 7e 7a 13 69 74 4e 10 52 76 fa ec 69 00 2c 35 75 64 18 57 33 78 ed

Specifically, the first 6 bytes match, which is the "pwned{" part! Just to be sure, the last bytes match too! This means, that we can run code and check whether individual bytes of the key match or not!

This is exactly what I did, I defined a alphanumeric keyset, with spaces and underscores:

let alphabet = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_ ";

Defined base flag:

let mut key: [u8; 31] = *b"pwned{aaaaaaaaaaaaaaaaaaaaaaaa}";

And handled the result of the emulator:

if success {
    println!("Success!!!");
    println!("{}", std::str::from_utf8(&key).unwrap());
    break;
} else {
    println!("KEY FAILED {}", std::str::from_utf8(&key).unwrap());
    for i in 0..31 {
        if tape[i] != ctx.target_key[i] {
            let pos = alphabet.iter().copied().enumerate().find(|(_, e)| *e == key[i]).unwrap().0;
            key[i] = alphabet[pos + 1];
        }
    }
}

I essentially go through all bytes that did not match the target key, and change the input to the next character from the alphabet, and then just rerun the emulation. After putting this all together, let's try and run it!

KEY FAILED pwned{aaaaaaaaaaaaaaaaaaaaaaaa}
KEY FAILED pwned{bbbbbbbbbbbbbbbbbbbbbbbb}
KEY FAILED pwned{cccccccccccccccccccccccc}
KEY FAILED pwned{dddddddddddddddddddcdddd}
KEY FAILED pwned{eeeeeeeeeeeeeeeeeeecedee}
KEY FAILED pwned{fffffffffffffffffffcfdff}
KEY FAILED pwned{gggggggggggggggggggcgdgg}
KEY FAILED pwned{hhhhghhhhhhhhhhhhhhchdhh}
KEY FAILED pwned{iiiigiiiiiiiiiiiiiicidii}
KEY FAILED pwned{jjjjgjjjjjjjjjjjjjjcjdjj}
KEY FAILED pwned{kkkkgkkkkkkkkkkkkkkckdkk}
KEY FAILED pwned{llllgllllllllllllllcldll}
KEY FAILED pwned{mmmmgmmmmmmmmmmmmmmcmdmm}
KEY FAILED pwned{nnnngnnnnnnnnnmnnnncndnn}
KEY FAILED pwned{nooogooonooooomoooocodoo}
KEY FAILED pwned{npppgpppnpppppmppppcpdpp}
KEY FAILED pwned{nqqqgqqqnqqqqqmpqqpcqdqq}
KEY FAILED pwned{nrrrgrrrnrrrrrmprrpcrdrr}
KEY FAILED pwned{nsssgsssnsrsssmpsspcsdss}
KEY FAILED pwned{ntttgttsntrtttmpttpctdts}
KEY FAILED pwned{nuuuguusnurtuumpuupcudus}
KEY FAILED pwned{nvvvgvvsnvrtvvmpvvpcvdvs}
KEY FAILED pwned{nwwwgwwsnwrtwvmpwwpcwdws}
KEY FAILED pwned{nxwxgxxsnxrtxvmpxxpcxdxs}
KEY FAILED pwned{nywygyysnyrtyvmpyypcydys}
KEY FAILED pwned{nzwzgzzsnzrtzvmpzzpczdzs}
KEY FAILED pwned{nAwAgAAsnArtAvmpAApcAdAs}
KEY FAILED pwned{nBwBgBBsnBrtBvmpBBpcBdBs}
KEY FAILED pwned{nCwCgCCsnCrtCvmpCCpcCdCs}
KEY FAILED pwned{nDwDgDDsnDrtDvmpDDpcDdDs}
KEY FAILED pwned{nEwEgEEsnErtEvmpEEpcEdEs}
KEY FAILED pwned{nFwFgFFsnFrtFvmpFFpcFdFs}
KEY FAILED pwned{nGwGgGGsnGrtGvmpGGpcGdGs}
KEY FAILED pwned{nHwHgHHsnHrtHvmpHHpcHdHs}
KEY FAILED pwned{nIwIgIIsnIrtIvmpIIpcIdIs}
KEY FAILED pwned{nJwJgJJsnJrtJvmpJJpcJdJs}
KEY FAILED pwned{nKwKgKKsnKrtKvmpKKpcKdKs}
KEY FAILED pwned{nLwLgLLsnLrtLvmpLLpcLdLs}
KEY FAILED pwned{nMwMgMMsnMrtMvmpMMpcMdMs}
KEY FAILED pwned{nNwNgNNsnNrtNvmpNNpcNdNs}
KEY FAILED pwned{nOwOgOOsnOrtOvmpOOpcOdOs}
KEY FAILED pwned{nPwPgPPsnPrtPvmpPPpcPdPs}
KEY FAILED pwned{nQwQgQQsnQrtQvmpQQpcQdQs}
KEY FAILED pwned{nRwRgRRsnRrtRvmpRRpcRdRs}
KEY FAILED pwned{nSwSgSSsnSrtSvmpSSpcSdSs}
KEY FAILED pwned{nTwTgTTsnTrtTvmpTTpcTdTs}
KEY FAILED pwned{nUwUgUUsnUrtUvmpUUpcUdUs}
KEY FAILED pwned{nVwVgVVsnVrtVvmpVVpcVdVs}
KEY FAILED pwned{nWwWgWWsnWrtWvmpWWpcWdWs}
KEY FAILED pwned{nXwXgXXsnXrtXvmpXXpcXdXs}
KEY FAILED pwned{nYwYgYYsnYrtYvmpYYpcYdYs}
KEY FAILED pwned{nZwZgZZsnZrtZvmpZZpcZdZs}
KEY FAILED pwned{n1w1g11sn1rt1vmp11pc1d1s}
KEY FAILED pwned{n2w2g22sn2rt2vmp22pc2d2s}
KEY FAILED pwned{n3w3g33sn3rt3vmp33pc3d3s}
KEY FAILED pwned{n4w4g44sn4rt4vmp44pc4d3s}
KEY FAILED pwned{n5w5g55sn5rt5vmp55pc5d3s}
KEY FAILED pwned{n6w6g66sn6rt6vmp66pc6d3s}
KEY FAILED pwned{n7w7g77sn7rt7vmp77pc7d3s}
KEY FAILED pwned{n8w8g88sn8rt8vmp88pc8d3s}
KEY FAILED pwned{n9w9g99sn9rt9vmp99pc9d3s}
KEY FAILED pwned{n0w0g00sn0rt0vmp00pc0d3s}
Success!!!
pwned{n0w_g0_sn0rt_vmp_0pc0d3s}

Booyah!!! This is it! Submit the flag, and it's correct!

Summary

This challenge, it was tedious. That's what you get for not knowing angr lmao :D but it was lots of fun. It had layers on it, it had some lazy obfuscation attempts as well, but it was fun! Good job, xeno, all your reversing challenges were a joy to solve! I definitely got that tiny bit better at reversing, I learned a few tricks of debugging along the way as well, and I definitely cannot wait to solve more challenges like this!

Thank you once again xeno, and the rest of pwnEd organizers, for putting together this CTF!

Code for the challenge can be found here, on my github.