Rafa10

WolvCTF2022 Homework Help

Problem statement

I don’t have the statement exactly but it was something about a friend needing help with their homework.

Binary

We are given a binary that asks us for our answer to a problem and if we guess it correctly it will check the flag, but there is a catch. When we analyse the main function, it just calls the “ask” function for our input.

__int64 ask()
{
  char anonymous0; // [rsp+0h] [rbp+0h] BYREF
  unsigned __int64 vars8; // [rsp+8h] [rbp+8h]

  vars8 = __readfsqword(0x28u);
  puts("Bro can you help me with my homework?");
  puts(
    "What is the largest number of circles with a diameter of 0.7 that will fit completely inside a 2 x 1 rectangle without overlapping?");
  __printf_chk();
  gets(&anonymous0);
  return eval(&anonymous0);
}
__int64 __fastcall eval(char *s)
{
  char *parcel; // rbp
  size_t v2; // r13
  unsigned int answer; // r12d
  char accept[13]; // [rsp+Bh] [rbp-3Dh] BYREF
  unsigned __int64 v6; // [rsp+18h] [rbp-30h]

  v6 = __readfsqword(0x28u);
  strcpy(accept, "0123456789+\n");
  parcel = strtok(s, "+");
  v2 = strspn(s, accept);
  if ( v2 == strlen(s) )                        // All match.
  {
    answer = 0;
    if ( !parcel )
      goto LABEL_5;
    do
    {
      answer += strtol(parcel, 0LL, '\n');
      parcel = strtok(0LL, "+");
    }
    while ( parcel );
    if ( answer == 3 )
    {
      puts("Thanks, I'll help you check the flag");
      __printf_chk();
      fgets(FLAG, 33, stdin);
    }
    else
    {
LABEL_5:
      puts("That really dosn't look right, I'll keep trying");
    }
  }
  else
  {
    answer = 0;
    puts("Uh, the answer wouldn't look like that");
  }
  return answer;
}

The call to strspn will return the amount of chars from "0123456789+\n" inside s, our input, and we are tokenizing + with strtok. The literal values of the sum parcels are turned to integers with strtol and added. Simple, right? There it is hardcoded, we must input a sum that results in 3.

Bro can you help me with my homework?
What is the largest number of circles with a diameter of 0.7 that will fit completely inside a 2 x 1 rectangle without overlapping?
Answer: 1+1+1
Thanks, I'll help you check the flag
Flag:

Yeah, we don’t see any relevant code after and the program seems to just terminate… What a bummer… Or do we? Modern compilers can detect obvious overflows of dangerous functions that can corrupt the stack or use heuristics to evaluate how dangerous a function might be in the not-obvious situations, hence the existance of stack canaries. Decompilers like IDA Pro, Ghidra or Binja may hide that call from the user, since it is placed by the compiler itself, but it is there, and a call to gets is obviously dangerous. Why am I talking about this? Well, it turns out the author of the challenge decided to troll us and implement their own _stack_chk_fail where the flag is tested.

void __noreturn _stack_chk_fail()
{
  __int64 v0; // rax
  int i; // edx
  int v2; // [rsp+Ch] [rbp-15Ch]
  int v3[2]; // [rsp+10h] [rbp-158h]
  __int64 v4; // [rsp+18h] [rbp-150h]
  __int64 v5; // [rsp+20h] [rbp-148h]
  __int64 v6; // [rsp+28h] [rbp-140h]
  __int64 v7; // [rsp+30h] [rbp-138h]
  __int64 v8; // [rsp+38h] [rbp-130h]
  __int64 v9; // [rsp+40h] [rbp-128h]
  __int64 v10; // [rsp+48h] [rbp-120h]
  __int64 v11; // [rsp+50h] [rbp-118h]
  __int64 v12; // [rsp+58h] [rbp-110h]
  __int64 v13; // [rsp+60h] [rbp-108h]
  __int64 v14; // [rsp+68h] [rbp-100h]
  __int64 v15; // [rsp+70h] [rbp-F8h]
  __int64 v16; // [rsp+78h] [rbp-F0h]
  __int64 v17; // [rsp+80h] [rbp-E8h]
  __int64 v18; // [rsp+88h] [rbp-E0h]
  struct __jmp_buf_tag env[1]; // [rsp+90h] [rbp-D8h] BYREF
  unsigned __int64 v20; // [rsp+158h] [rbp-10h]

  v20 = __readfsqword(0x28u);
  v3[1] = 20;
  v4 = 0x1200000017LL;
  v5 = 0x500000001DLL;
  v6 = 0x5D00000046LL;
  v7 = 0x4100000042LL;
  v8 = 0x330000006CLL;
  v9 = 0x5A0000005DLL;
  v10 = 0x3A0000000ELL;
  v11 = 0x410000006ALL;
  v12 = 0x5700000040LL;
  v13 = 0x3400000008LL;
  v14 = 0xB0000003CLL;
  v15 = 0x3400000003LL;
  v16 = 0x4600000028LL;
  v17 = 0x530000005FLL;
  v18 = 0x5000000010LL;
  v2 = 54;
  if ( _setjmp(env) )
  {
    puts("Nope.");
  }
  else
  {
    v0 = 0LL;
    for ( i = 65; ; i = v3[v0] )
    {
      v2 ^= i;
      if ( FLAG[v0] != v2 )
        __longjmp_chk();                        // Get out.
      if ( ++v0 == 32 )
        break;
    }
    puts("Well Done.");
  }
}

There we go, a checker. Once a character doesn’t match what it expects from the xorring, it will exit with the libc longjmp shenenigans. We see the v3 array overflows into the other stack vars. Thanks to the addressing, the 0s in the middle are to make the space to address every value as u32 even though they clearly fit in u8.

i = 65
flag = ""
cur = 54
v3 = [0, 0x14, 0x17, 0x12, 0x1D, 0x50, 0x46, 0x5D, 0x42, 0x41, 0x6C, 0x33, 0x5D, 0x5A, 0x0E, 0x3A, 0x6A, 0x41, 0x40, 0x57, 0x08, 0x34, 0x3C, 0x0B, 0x03, 0x34, 0x28, 0x46, 0x5F, 0x53, 0x10, 0x50]
v0 = 0
while(True):
    v0 += 1
    cur = (i ^ cur)
    flag += chr(cur)
    if (v0 == 32):
        break
    i = v3[v0]

print(flag)

wctf{+m0r3_l1ke_5t4ck_chk_w1n=-}

There, so easy to statically analyse that I feel like this writeup is completely introductory. However, I decided to learn about Frida because I had heard good stuff about it in the community and I went out for dinner with my dad in the meantime and sure a blackbox for this would be easy to implement anyway (maybe shouldn’t have procrastinated with learning a new tool during a CTF, but oh well). It just so happens that the eval function has the call to _stack_chk_fail right before ask, so jumping to it there would just be perfect to make the program loop and ease out a blackbox, so we do that patch in the binary itself and it will check the flag.

var r = Process.getModuleByName("homework_help_patched").base;

var getchar = Module.getExportByName("libc.so.6", "getchar");

var gets = r.add(0x1140);
var fgets = r.add(0x1110);

var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ";
var cur_char_id = 0;

var cur_rax = 5;
var cur_flag = "wctf{";

Interceptor.replace(gets, new NativeCallback((inPtr) => {
  Memory.copy(inPtr, Memory.allocUtf8String("1+1+1\n\x00"), 8);
  return inPtr;
}, 'pointer', ['pointer']));

Interceptor.replace(fgets, new NativeCallback((inPtr, sz, iin) => {
    let _data = cur_flag + chars[cur_char_id] + "\n\x00";
    Memory.copy(inPtr, Memory.allocUtf8String(_data), _data.length + 2);
  return inPtr;
}, 'pointer', ['pointer', 'int', 'int']));

Interceptor.attach(r.add(0x1426), function() {
    let nrax = this.context.rax;
    let getchar_call = new NativeFunction(getchar, 'uint64', []);
    if (nrax.toInt32() != cur_rax) {
        cur_flag += chars[cur_char_id];
        cur_char_id = 0;
        cur_rax = nrax;
    } else if (nrax.toInt32() == 31) {
        console.log("flag: " + cur_flag);
        getchar_call();
    } else {
        cur_char_id++;
    }
});

This was the exploit I came up with. So, here I use the Interceptor class that represents the hooking primitive in binary patching. I use two methods: replace to completely replace a C function (gets to hardcode the sum we talked about early and fgets to place the current blackbox flag in memory, both with Memory primitives) with a JavaScript function and attach to perform an inline hook right at the basic block where longjmp is called thanks to failing a match with a char.

RAX is the indexing register, so we can check for an incremented value and cache the correct guess from the alphabet. I match until 31 and then stall with a call to getchar so I can see the flag.

Bro can you help me with my homework?
What is the largest number of circles with a diameter of 0.7 that will fit completely inside a 2 x 1 rectangle without overlapping?
Answer: Thanks, I'll help you check the flag
Flag: Nope.
Bro can you help me with my homework?
What is the largest number of circles with a diameter of 0.7 that will fit completely inside a 2 x 1 rectangle without overlapping?
Answer: Thanks, I'll help you check the flag
Flag: Nope.
Bro can you help me with my homework?
What is the largest number of circles with a diameter of 0.7 that will fit completely inside a 2 x 1 rectangle without overlapping?
Answer: Thanks, I'll help you check the flag
Flag: Nope.
Bro can you help me with my homework?
What is the largest number of circles with a diameter of 0.7 that will fit completely inside a 2 x 1 rectangle without overlapping?
Answer: Thanks, I'll help you check the flag
Flag: flag: wctf{+m0r3_l1ke_5t4ck_chk_w1n=-

I found Frida pretty intuitive and nice for someone to learn binary patching and intrumentation. The frida-gum lib implements a lot of instrumentation logic and can be called by native languages like C and Rust. Honestly, I would still prefer to write a Python gdbscript in blackbox situations, due to their simplicity and since Frida is suited for more complex instrumentation.