Для решения нужно подобрать ключ, после чего программа отобразит флаг.
Бинарные файлы:
Solution
Первым делом получу инфу о бинаре с помощью DIEC:
Info:
File name: /spbctf_4_x86_64/spbctf_4_x86_64
Size: 8856
File type: ELF64
String: ELF(AMD64)
Extension: elf
Operation system: Ubuntu Linux(16.04.4,ABI: 2.6.32)
Architecture: AMD64
Mode: 64-bit
Type: EXEC
Endianness: LE
Пойду изучать бинарь статически. Таблица импортов не затерта, что сильно упрощает анализ.
Пробежавшись взглядом по функции main, я нашел следующий вызов:

Он то нам и нужен. Полезным делом будет переименовывать переменные и указывать тип для них, так как в дальнейшем это упростит анализ. Использовать вызовы printf для этого очень удобно.
В функцию передается форматная строка, из которой можно вытащить тип переменных, а иногда даже имя:

Красотища какая… Вот весь код функции main:
{
void* fsbase;
int64_t rax = *(uint64_t*)((char*)fsbase + 0x28);
int32_t var_278;
__builtin_memcpy(&var_278, "\x08\x00\x00\x00\x07\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x09\x00\x00\x00\x0a\x00\x00\x00", 0x28);
char* flag;
__builtin_strncpy(&flag, "cPK}[aYr^@ZZR`C]TBP_\\Y_U\x7fUWE", 0x1d);
int32_t result;
if (argc > 1)
{
int32_t rax_4 = strlen(argv[1]);
int32_t var_284_1 = 0;
while (true)
{
void var_248;
if (var_284_1 >= rax_4)
{
bubble_sort_sequence_executor_aka_transposition_performer(&var_278, &var_248, rax_4);
int32_t var_280_1 = 1;
for (int32_t i = 0; i <= 9; i += 1)
{
if (&var_278[((int64_t)i)] != (i + 1))
var_280_1 = 0;
}
if (var_280_1 == 0)
puts("Hmmm, Not exactly what I was ask…");
else
{
int32_t var_284_2 = 0;
while (((int64_t)var_284_2) < strlen(&flag))
{
*(uint8_t*)(&flag + ((int64_t)var_284_2)) ^= argv[1][((int64_t)(((int64_t)var_284_2) % rax_4))];
var_284_2 += 1;
}
printf("Your flag: %s\n", &flag);
}
result = 0;
break;
}
char rax_10 = argv[1][((int64_t)var_284_1)];
int32_t rax_11;
if ((rax_10 <= 0x2f || rax_10 > 0x39))
rax_11 = 0;
else
rax_11 = 1;
if (rax_11 == 0)
{
puts("Only 0-9");
result = 2;
break;
}
*(uint32_t*)(&var_248 + (((int64_t)var_284_1) << 2)) = (((int32_t)argv[1][((int64_t)var_284_1)]) - 0x30);
var_284_1 += 1;
}
}
else
{
puts("Go away, lazy student!");
result = 1;
}
if (rax == *(uint64_t*)((char*)fsbase + 0x28))
return result;
__stack_chk_fail();
/* no return */
}
Буду разбираться, что же происходит в коде.
Вот тут копируется строка в переменную flag:
char* flag;
__builtin_strncpy(&flag, "cPK}[aYr^@ZZR`C]TBP_\\Y_U\x7fUWE", 0x1d);
Можно сделать вывод, что флаг имеет длину 29.
if (argc > 1) {
...
} else {
puts("Go away, lazy student!");
result = 1;
}
Вот и первая проверка. Значит, для получения флага нужно передавать какой-то аргумент через командную строку.
int32_t rax_4 = strlen(argv[1]);
int32_t var_284_1 = 0;
Получаем длину переданного аргумента. Сразу переименую переменную:
int32_t input_len = strlen(argv[1]);
int32_t var_284_1 = 0;
Разберем следующий кусок кода. Писать о каждом переименовании переменных слишком запарно. Откройте бинарь и сопоставьте. Воть. Если внимательно изучить функцию main, то можно увидеть, что в первую очередь выполняется данный кусок кода:
int32_t *input_nums;
if ((c <= '/' || c > '9'))
rax_9 = 0;
else
rax_9 = 1;
if (rax_9 == 0) {
puts("Only 0-9");
result = 2;
break;
}
*(uint32_t*)(&input_nums + (((int64_t)pos) << 2)) = (((int32_t)argv[1][((int64_t)pos)]) - 0x30);
Вот эта строчка выглядит максимально неясно)
*(uint32_t*)(&input_nums + (((int64_t)pos) << 2)) = (((int32_t)argv[1][((int64_t)pos)]) - 0x30);
Ее можно упростить до следующего вида:
input_name[pos] = argv[pos] - `\0`;
Стало понятнее. В качестве входных параметром нужно передавать строку. А именно в данной строке из ascii символов получают int.
Поехали дальше. После преобразования всех символов в int выполняется следующее условие, и мы проваливаемся дальше:
if (pos >= input_len_1)
Вот так выглядит следующая проверка:
bubble_sort_sequence_executor_aka_transposition_performer(&key, &input_nums, input_len_1);
int32_t ret = 1;
for (int32_t i = 0; i <= 9; i += 1)
{
if (key[((int64_t)i)] != (i + 1))
ret = 0;
}
if (ret == 0)
puts("Hmmm, Not exactly what I was ask…");
else
{
int32_t j = 0;
while (((int64_t)j) < strlen(&flag))
{
*(uint8_t*)(&flag + ((int64_t)j)) ^= argv[1][((int64_t)(((int64_t)j) % input_len_1))];
j += 1;
}
printf("Your flag: %s\n", &flag);
}
result = 0;
break;
Изучу содержимое функции bubble_sort_sequence_executor_aka_transposition_performer:
int64_t bubble_sort_sequence_executor_aka_transposition_performer(int32_t* key, uint8_t* input_nums, int32_t len)
{
int32_t i;
for (i = 0; i < len; i += 1)
{
int32_t tmp = key[((int64_t)input_nums[((int64_t)i)])];
key[((int64_t)input_nums[((int64_t)i)])] = key[(((int64_t)input_nums[((int64_t)i)]) + 1)];
key[(((int64_t)input_nums[((int64_t)i)]) + 1)] = tmp;
}
return i;
}
Выглядит не очень. Давайте разбираться. Мы проходим по всем элементам массива input_nums. В tmp мы сохраняем input_nums[i]. В key[input_nums[i]] сохраняется key[input_nums[i] + 1)]. Далее в key[input_nums[i] + 1] сохраняется tmp. По факту данная функция меняет местами 2 соседние ячейки массива.
Вот тут видно, что в массиве key индекс должен совпадать со значением в ячейке. А значит нужно отсортировать массив:
for (int32_t i = 0; i <= 9; i += 1)
{
if (key[((int64_t)i)] != (i + 1))
ret = 0;
}
bubble_sort на что-то намекает… Но вот на что…

Нам нужна сортировка пузырьком. Можно написать самому или же нагуглить реализацию:
def bubble_sort(arr):
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swaps.append(j)
swapped = True
if not swapped:
break
return arr
Но это еще не все. Отсортировать то мы можем, но нам нужно запомнить индексы ячеек, которые мы меняли местами:
def bubble_sort(arr):
swaps = []
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swaps.append(j)
swapped = True
if not swapped:
break
print(''.join([str(n) for n in swaps]))
return arr
Великолепно. Реализую все решение целиком:
def xor(s: bytes, key: list[int]) -> bytes:
return bytes([s[i] ^ key[i % len(key)] for i in range(len(s))])
def perm(key, input_nums):
for i in input_nums:
tmp = key[i]
key[i] = key[i + 1]
key[i + 1] = tmp
def bubble_sort(arr):
swaps = []
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swaps.append(j)
swapped = True
if not swapped:
break
return swaps
# main()
if __name__ == '__main__':
s = b'cPK}[aYr^@ZZR`C]TBP_\\Y_U\x7fUWE'
key = [8, 7, 5, 4, 1, 3, 2, 6, 9, 10]
swaps = bubble_sort(key.copy())
perm(key, swaps)
xor_key = ''.join([str(n) for n in swaps]).encode()
print(xor(s, xor_key).decode())
Вы можете его скачать для своих экспериментов.
Ключ получился следующий:
012345601234501230121
А вот и искомый флаг:
SayNoToBoringProgrammingLabs
В этом можно убедиться, запустив бинарный файл:
root@2a01d28f0022:/rev# ./spbctf_4_x86_64 012345601234501230121
Your flag: SayNoToBoringProgrammingLabs