Summarize (rev)

All you have to do is find six numbers. How hard can that be?

14KB
Open

This challenge is a crackme, which requires us to correctly enter 6 numbers in order for the flag to be shown.

The main function given by Ghidra looks like this:

undefined8 main(void)
{
  char cVar1;
  long in_FS_OFFSET;
  uint local_60;
  uint local_5c;
  uint local_58;
  uint local_54;
  uint local_50;
  uint local_4c;
  char local_48 [56];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts(
      "To get the flag, you must correctly enter six 9-digit positive integers: a, b, c, d, e, and f ."
      );
  putchar(10);
  printf("a = ");
  __isoc99_scanf(&DAT_0040206c,&local_60);
  printf("b = ");
  __isoc99_scanf(&DAT_0040206c,&local_5c);
  printf("c = ");
  __isoc99_scanf(&DAT_0040206c,&local_58);
  printf("d = ");
  __isoc99_scanf(&DAT_0040206c,&local_54);
  printf("e = ");
  __isoc99_scanf(&DAT_0040206c,&local_50);
  printf("f = ");
  __isoc99_scanf(&DAT_0040206c,&local_4c);
  cVar1 = check(local_60,local_5c,local_58,local_54,local_50,local_4c);
  if (cVar1 == '\0') {
    puts("Wrong.");
  }
  else {
    puts("Correct.");
    sprintf(local_48,"uiuctf{%x%x%x%x%x%x}",(ulong)local_60,(ulong)local_5c,(ulong)local_58,
            (ulong)local_54,(ulong)local_50,(ulong)local_4c);
    puts(local_48);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

Our 6 inputs are first checked using check(). If it returns 1 we have passed the check, and we obtain the flag. Looking at the check() function:

Let's look at fun1_2 and fun1 first.

fun1_2 is simply a wrapper for fun1, except that the second parameter is negated before calling fun1.

local_2c and local_30 are initialised to be param_1 and param_2 respectively at first. Then, with each iteration of the for loop, they are each right shifted by 1 bit. bVar1 is also incremented with each iteration. The loop stops if both of them are equivalent to 0. Within the loop, uVar2 is the rightmost bit of local_2c, while uVar3 is the rightmost bit of local_30. Then, local_10 is incremented by (uVar2 ^ uVar3 ^ local_20) << (bVar1 & 0x1f). Notice that (uVar2 ^ uVar3 ^ local_20) only evaluates to 1 if all 3 variables are 1, or only one of them is 1. This looks like how the result bit is obtained from addition of integers!

Let's think about the next line, local_20 = (uVar3 & local_20) | uVar2 & (uVar3 | local_20). In what scenarios would local_20 be set to 1?

  • Case 1: local_20 = 1.(uVar3 & local_20) evaluates to uVar3, and (uVar3 | local_20) evaluates to 1 here, so we are left with uVar3 | uVar2.

  • Case 2: local_20 = 0. (uVar3 & local_20) evaluates to 0 and (uVar3 | local_20) evaluates to uVar3. So we are left with uVar2 & uVar3.

So if local_20 was already 1, as long as either uVar3 or uVar2 is 1, local_20 will be 1. If local_20 was 0, it will become 1 only if both uVar2 and uVar3 are 1. This reminds us of the carry bit during addition of integers! This can be further confirmed by the fact that local_20 is added to local_10 as the most significant bit once the loop exits.

With the above observations, we can deduce that fun1 is essentially doing integer addition of param_1 and param_2 . This means that fun1_2 would be doing subtraction of param_2 from param_1.

Now let's look at fun2.

This looks similar to fun1 in that it is performing some bitwise operation, but our carry flag is gone. We are using param_1's bits, but param_2 is used without extracting its bits. Our loop is iterating through the bits in param_1 starting from the rightmost bit. Let's run through an example. Say param_1 = 2 = 0b10 and param_2 = 3 = 0b11.

In the first iteration, local_1c & 1 = 0, so local_14 remains as 0.

In the second iteration, bVar1 = 1. So param_2 is left-shifted by 1 bit, and this time local_1c & 1 = 1. So we get local_14 = local_14 + (3 << 1) * 1).

The result we get is 6. Notice that this is the result of doing 2 * 3!

In general, if bit i of param_1 = 1, then local_14 = local_14 + (param_2 << i), which is equivalent to local_14 = local_14 + param_2 * 2^i.

In another example if param_1 = 3 = 0b11 and param_2 = x, then we would get a result of x * 2^0 + x * 2^1 = x * 1 + x * 2 = param_2 * 3. This should convince you that fun2 is simply a multiplication function!

Now let's look at fun3.

fun3 seems very similar to fun1, except that our carry bit is gone and now the bit being added in each iteration is given by (uVar2 ^ local_20 & 1). This is essentially an XOR operation between the bit we obtained from param_1 and the bit we obtained from param_2. So fun3 is an XOR function.

Moving on to fun4:

It is similar to fun3, except that the bit being added is (uVar2 & local_20 & 1). That is the AND operation between the bit obtained from param_1 and the bit obtained from param_2. So fun4 is performing an AND operation on param_1 and param_2.

Looking back at the restrictions imposed by main(),

Our 6 integers are restricted to be within the range (0x5f5e101, 1000000000) (which means they must all be positive and 9-digits long), and further modulo restrictions are imposed on the arithmetic results. Recall that fun1 is addition, fun1_2 is subtraction, fun2 is multiplication, fun3 is XOR, and fun4 is AND. We can use a z3 script to solve these constraints and obtain the 6 integers.

Below is my script:

Last updated