Dialtone challenge

“You might need a pitch-perfect voice to solve this one. Once you crack the code, the flag is CTF{code}.”

Initial thoughts

Unlike other challenges we probably need to find a ‘code’ which will be part of the flag.
Dialtone sounds like something related to telecommunications, and the description probably verifies it.

Overview

We are given an attachment called “a.out” which is a X86_64 ELF binary.
Taking a brief look at the ELF we notice that in addition to imports from standard libraries we also see imports from libpulse.so and libpulse-simple.so which are libraries of a sound server software called PulseAudio.

Functions

main()

pa_conn = pa_simple_new(0, *argv, 2, 0, "record", &g_pa_struc,0, 0, &error);
if (!pa_conn) {
    fprintf(stderr, "pa_simple_new() failed: %s\n", pa_strerror(error));
    return 1;
}
struc.field_0 = 0;
struc.field_4 = 0;
struc.field_8 = 0;
do {
    if (pa_simple_read(pa_conn, buff, 0x8000, &error) < 0) {
        fprintf(stderr, "pa_simple_read() failed: %s\n", pa_strerror(error));
        return 1;
    }
    x(buff, &derived_buff);
    r_result = r(&struc, derived_buff);
    if (r_result < 0) {
        fwrite("FAILED\n", 1, 7, stderr);
        return 1;
    }
} while (r_result);
fwrite("SUCCESS\n", 1, 8, stderr);
pa_simple_free(pa_conn, 1);
return 0;

The plan

We probably want “SUCCESS” to be printed instead of “FAILED”.

Thus we want to make r() return 0 at some point, and avoid negative return values.

Find sinks of our data input to see what’s going on.

x()

bit_flip(buff, derived_buff);
for (i = 1; i < 14; i++) {
    tmp = 1 << i;
    for (j = 0; j < 0x2000; j += tmp) {
        y(derived_buff, j, tmp);
    }
}

This function and the functions down this path seem to not modify the previously read 0x8000 bytes.

Instead it reads our input to produce some other data into derived_buff which is later passed on to r().

After verifying that input is only read to write something out we decided to skip over most of the code down this path thinking it may be interesting later when we want to know more about how derived_buff is created.

r()

int __fastcall f(struct_0* p_struc, void* derived_buff) {
  ++p_struc->field_0;
  if (p_struc->field_0 > 20) {
      return -1;
  }

  arr_1[0] = f(derived_buff, 1209);
  arr_1[1] = f(derived_buff, 1336);
  arr_1[2] = f(derived_buff, 1477);
  arr_1[3] = f(derived_buff, 1633);
  chosen_arr_1_idx = -1;
  tmp_arr_1_val = 1.0;
  for (i = 0; i < 4; i++) {
      if (arr_1[i] > tmp_arr_1_val) {
          chosen_arr_1_idx = i;
          tmp_arr_1_val = arr_1[i];
      }
  }

  arr_2[0] = f(derived_buff, 697);
  arr_2[1] = f(derived_buff, 770);
  arr_2[2] = f(derived_buff, 852);
  arr_2[3] = f(derived_buff, 941);
  chosen_arr_2_idx = -1;
  tmp_arr_2_val = 1.0;
  for (j = 0; j < 4; j++) {
      if (arr_2[j] > tmp_arr_2_val) {
          chosen_arr_2_idx = j;
          tmp_arr_2_val = arr_2[j];
      }
  }

  if (p_struc->field_8) {
      if (chosen_arr_1_idx < 0 && chosen_arr_2_idx < 0) {
          p_struc->field_8 = 0;
          p_struc->field_0 = 0;
      }
      return 1;
  }
  if (chosen_arr_1_idx >= 0 && chosen_arr_2_idx >= 0) {
      y = chosen_arr_1_idx | 4 * chosen_arr_2_idx;
      found_match = 0;
      switch (p_struc->field_4) {
          case 0:
              found_match = y == 9;
          break;
          case 1:
              found_match = y == 5;
          break;
          case 2:
              found_match = y == 10;
          break;
          case 3:
              found_match = y == 6;
          break;
          case 4:
              found_match = y == 9;
          break;
          case 5:
              found_match = y == 8;
          break;
          case 6:
              found_match = y == 1;
          break;
          case 7:
              found_match = y == 13;
          break;
          case 8:
              if (y == 0) {
                  return 0;
              }
          break;
      }
      if (found_match != 1) {
          return -1;
      }
      ++p_struc->field_4;
      p_struc->field_0 = 0;
      p_struc->field_8 = 1;
  }
  return 1;
}
Arguments

p_struc is a reference to a zero’d structure of 3 dwords.

derived_buff is a reference to the derived data written by x() called by main().

Flow

It starts with an increment and a check on the incremented field, so we may treat p_struc->field_0 as some kind of counter.

We notice 2 symmetrical pieces of code which call f() and then loop on the results.

Each loop sets its chosen index variable (chosen_arr_1_idx and chosen_arr_2_idx) to max(array) if there exists a value greater than 1.0 in array.

We now reach the first condition for making main() continue looping.

It looks like when p_struc->field_8 != 0 the function would always return 1.

When chosen_arr_1_idx and chosen_arr_2_idx are negative p_struc->field_8 and p_struc->field_0 are set back to 0.

p_struc->field_0 = 0 is a good sign because it makes returning a negative value less likely.

It could be said that p_struc->field_8 is some sort of state flag which needs to keep changing for r() to do something else.

In a subsequent call to r() when p_struc->field_8 is zero we can see that if any of chosen_arr_1_idx or chosen_arr_2_idx are negative then r() would return 1.

This probably means that for some interesting thing to happen in r() we want (chosen_arr_1_idx >= 0 && chosen_arr_2_idx >= 0) || (chosen_arr_1_idx < 0 && chosen_arr_2_idx < 0).

y = chosen_arr_1_idx | 4 * chosen_arr_2_idx is a 4×4 array index calculation which is compared to constant values inside the switch bracket.

If found_match != 1 then a negative value is returned, so we always want found_match == 1.

p_struc->field_4 is incremented which would make us compare other values with y in subsequent calls to r().

p_struc->field_0 = 0 makes us less likely to return -1.

p_struc->field_8 = 1 would lead us into the possibility of returning it to zero if chosen_arr_1_idx < 0 && chosen_arr_2_idx < 0.

After going over the code it is now clear what is the flow:

  • if flag == 0
    • if chosen_arr_1_idx >= 0 and chosen_arr_2_idx >= 0
      • if i == 8 and y == 0
        • SUCCESS
      • if y == constant_nums[i]
        • flag = 1
        • i++
  • else
    • if chosen_arr_1_idx < 0 and chosen_arr_2_idx < 0
      • flag = 0

We want each call to r() to be followed by another call to r() in a way that keeps comparing y untill we reach p_struc->field_4 == 8 where y == 0.

Our input

At this point we wanted to understand how we can create derived_buff such that it would follow our planned flow untill SUCCESS.

But something caught our attention inside f().

double __fastcall f(_complex *modified_buff, int const_val)
{
  return cabs(modified_buff[(const_val << 13) / 44100]);
}

The division by 44100 looked too pretty, so we searched for it on google and the first result was “44,1000 Hz – Wikipedia” which wasn’t unexpected because of the audio context.

This still wasn’t enough to really help us, but we felt lucky so we decided to google search all constant values we could find in the binary.

We found out that the constant values passed to f() calls were all related to DTMF code.

DTMF diagrams from google images also felt like the state machine of r().

We also found images of DTMF pads which are 4×4.

So we started to write down values from the DTMF pad with chosen_arr_1_idx and chosen_arr_2_idx being our indices in the order of the calculated y value comparsion.

chosen_arr_1_idx | 4 * chosen_arr_2_idx
  • 1 | 4 * 2 == 9
  • 1 | 4 * 1 == 5
  • 2 | 4 * 2 == 10
  • 2 | 4 * 1 == 6
  • 1 | 4 * 2 == 9
  • 0 | 4 * 2 == 8
  • 1 | 4 * 0 == 1
  • 1 | 4 * 3 == 13
  • 0 | 4 * 0 == 0
1209 Hz1336 Hz1477 Hz1633 Hz
697 Hz123A
770 Hz456B
852 Hz789C
941 Hz*0#D

We got the sequence 859687201 which turned out to be part of the flag as stated in the challenge description: CTF{859687201}