“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 Hz | 1336 Hz | 1477 Hz | 1633 Hz | |
---|---|---|---|---|
697 Hz | 1 | 2 | 3 | A |
770 Hz | 4 | 5 | 6 | B |
852 Hz | 7 | 8 | 9 | C |
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}