Try   HackMD

Write-up Secure Database (CTFZone 2021)

User1 Mon, Jun 28, 2021 5:07 AM

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Самый секьюрный канал на бешаном востоке @ch4nnel1

Original write-up: https://hackmd.io/@osogi/Secure-Database

Разбор

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Сам таск (https://ctf.bi.zone/challenges/8)

Сначала, постучимся на сервер с помощью приведенной команды nc securedb.2021.ctfz.one 7777, он попросит у нас id дадим ему id, который столь любезно дали нам админы 229386e9-d6b0-4006-babc-5a15f7acee72

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Просто блекбокся сервер, мы мало чего получим, так что стоит взглянуть на файл из таска. В нем как нестрано дан бинарник. Так как время поджимало (до конца ctf'а осталось около часа), то я сразу подключил читы воспользовался идой. Там в списке функций сразу приметилась одна start_routine, рутина все такое.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Открыв ее сразу замечаем строчку "To retrieve your medical report...", что говорит о том, что мы нашли функцию с которой стоит начать курить бинарь.

start_routine

void __fastcall __noreturn start_routine(void *a1) { __int16 v1; // [rsp+20h] [rbp-20h] BYREF unsigned __int16 v2; // [rsp+22h] [rbp-1Eh] int fd; // [rsp+24h] [rbp-1Ch] void *buf; // [rsp+28h] [rbp-18h] void *ptr; // [rsp+30h] [rbp-10h] unsigned __int64 v6; // [rsp+38h] [rbp-8h] v6 = __readfsqword(0x28u); fd = *(_DWORD *)a1; buf = "To retrieve your medical report, please enter the unique identifier.\n\n"; ptr = 0LL; v2 = 70; v1 = 0; if ( fd ) { if ( send(fd, buf, v2, 0) != -1 ) { ptr = (void *)sub_4013C8((unsigned int)fd, &v1); if ( ptr ) { sub_4018E5((unsigned int)fd, ptr, (unsigned int)v1); free(ptr); } } shutdown(fd, 2); close(fd); puts("Thread exit.."); pthread_exit(0LL); } puts("Connection failed, thread exit.."); pthread_exit(0LL); }

Дальше нас интересует функция sub_4013C8, в которую передается сокет (20 строчка).

sub_4013C8

char *__fastcall sub_4013C8(int a1, __int16 *a2) { char *result; // rax __int16 v3; // [rsp+10h] [rbp-C0h] __int16 v4; // [rsp+12h] [rbp-BEh] int v5; // [rsp+14h] [rbp-BCh] int v6; // [rsp+14h] [rbp-BCh] int v7; // [rsp+14h] [rbp-BCh] void *s; // [rsp+20h] [rbp-B0h] char *ptr; // [rsp+28h] [rbp-A8h] struct timeval timeout; // [rsp+30h] [rbp-A0h] BYREF fd_set readfds; // [rsp+40h] [rbp-90h] BYREF unsigned __int64 v12; // [rsp+C8h] [rbp-8h] v12 = __readfsqword(0x28u); memset(&readfds, 0, sizeof(readfds)); readfds.fds_bits[a1 / 64] |= 1LL << (a1 % 64); timeout.tv_sec = 180LL; timeout.tv_usec = 0LL; v5 = select(a1 + 1, &readfds, 0LL, 0LL, &timeout); puts("Receiving data.."); if ( v5 == -1 ) goto LABEL_2; if ( !v5 ) goto LABEL_28; puts("Data is available now.."); s = malloc(3uLL); if ( !s ) return 0LL; memset(s, 0, 3uLL); if ( recv(a1, s, 2uLL, 0) == -1 ) { free(s); return 0LL; } *a2 = (unsigned __int8)sub_401847(s); free(s); if ( !*a2 ) return 0LL; if ( *a2 <= 0 || *a2 > 36 ) return 0LL; v6 = select(a1 + 1, &readfds, 0LL, 0LL, &timeout); if ( v6 == -1 ) { LABEL_2: puts("Select error..."); return 0LL; } if ( v6 ) { puts("Data is available now.."); ptr = (char *)malloc(*a2 + 1); if ( ptr ) { memset(ptr, 0, *a2 + 1); v3 = 0; timeout.tv_sec = 1LL; while ( *a2 > v3 ) { v7 = select(a1 + 1, &readfds, 0LL, 0LL, &timeout); if ( v7 == -1 ) { puts("Select error..."); free(ptr); return 0LL; } if ( !v7 ) break; v4 = recv(a1, &ptr[v3], *a2 - v3, 0); if ( v4 <= 0 ) { free(ptr); return 0LL; } v3 += v4; } if ( *a2 == v3 ) { result = ptr; } else { puts("Invalid data..."); free(ptr); result = 0LL; } } else { puts("ERROR.."); result = 0LL; } } else { LABEL_28: puts("No data within 3 minutes. Exiting.."); result = 0LL; } return result; }

Тут нас будет интересовать получение данных и что потом с ними происходит. Для начала это строчки 31 и 36, по строчке 31 все понятно в переменную s записываются 2 первых байта, и после если все хорошо в 36 строчке s передается в функцию sub_401847.

sub_401847

__int64 __fastcall sub_401847(unsigned __int8 *a1) { unsigned __int8 *v1; // rax unsigned __int8 v4; // [rsp+1Dh] [rbp-3h] unsigned __int8 v5; // [rsp+1Eh] [rbp-2h] unsigned __int8 v6; // [rsp+1Fh] [rbp-1h] char v7; // [rsp+1Fh] [rbp-1h] v4 = 0; v5 = 0; while ( v4 <= 1u ) { v1 = a1++; v6 = *v1; if ( *v1 <= '/' || v6 > '9' ) { if ( v6 <= '`' || v6 > 'f' ) { if ( v6 <= '@' || v6 > 'F' ) { puts("ERROR: hex convert to int.."); return 0LL; } v7 = v6 - 55; } else { v7 = v6 - 87; } } else { v7 = v6 - 48; } v5 = (16 * v5) | v7 & 0xF; ++v4; } return v5; }

Чуть изучив эту функцию легко понять, что она конвертит хексовое представление байта в инт (int(s[:2], 16) - псевдокод в студию).

Окей, вернемся к анализу sub_4013C8. Этот инт как раз записывается по адресу a2, из дальнейшего кода модно понять, что в a2 хранится длина данных, которые будут при приняты дальше (после первых 2 символов), записаны в ptr и возвращены в start_routine. В результате в start_routine ptr хранит переданные данные (без первых двух байт), а v1 - размер этих данных, и все это вместе с сокетом передается в sub_4018E5.

sub_4018E5

__int64 __fastcall sub_4018E5(int a1, __int64 a2, unsigned int a3) { const char *v3; // rax __int64 result; // rax _BYTE n_4[12]; // [rsp+24h] [rbp-2Ch] BYREF __int64 src; // [rsp+30h] [rbp-20h] BYREF const char *v8; // [rsp+38h] [rbp-18h] BYREF void *s; // [rsp+40h] [rbp-10h] unsigned __int64 v10; // [rsp+48h] [rbp-8h] v10 = __readfsqword(0x28u); *(_QWORD *)&n_4[4] = 0LL; src = 0LL; v8 = 0LL; s = 0LL; *(_QWORD *)n_4 = (unsigned int)sqlite3_open("patient_data.db", &n_4[4]); if ( *(_DWORD *)n_4 ) { v3 = (const char *)sqlite3_errmsg(*(_QWORD *)&n_4[4]); fprintf(stderr, "Cannot open database: %s\n", v3); sqlite3_close(*(_QWORD *)&n_4[4]); result = 0LL; } else { s = malloc(0x34uLL); if ( s ) { memset(s, 0, 0x34uLL); memcpy(s, "SELECT * FROM PatientList WHERE HashID = '", 0x2AuLL); src = sub_401B1B(a2, a3, 64400LL); memcpy((char *)s + 42, &src, 8uLL); memcpy((char *)s + 50, "'", sizeof(char)); if ( (unsigned int)sqlite3_exec(*(_QWORD *)&n_4[4], s, sub_401CE1, a1, &v8) ) { fprintf(stderr, "SQL error: %s\n", v8); free(s); sqlite3_free(v8); sqlite3_close(*(_QWORD *)&n_4[4]); result = 0LL; } else { free(s); sqlite3_close(*(_QWORD *)&n_4[4]); result = 1LL; } } else { puts("ERROR.."); sqlite3_close(*(_QWORD *)&n_4[4]); result = 0LL; } } return result; }

Одно из самых интересных мест для нас это строчки 30-36 в них создается строчка s="SELECT * FROM PatientList WHERE HashID = '{src}'", где src занимает 8 байт и получается посредством передачи в sub_401B1B значений ptr и v1 из sub_4018E5. После как нестрано s используется как sql запрос, форматированный результат которого и выдается нам по сокету назад.

Дальше я пытался понять каким HashID обладает нужный мне пациент, но решил подумать об этом позже, когда научусь генерить нужные s. Так что я пошел изучать sub_401B1B.

sub_401B1B

unsigned __int64 __fastcall sub_401B1B(unsigned __int8 *a1, unsigned __int64 a2, __int64 a3) { unsigned __int8 *v3; // rax unsigned __int64 v5; // [rsp+20h] [rbp-38h] unsigned __int8 *i; // [rsp+28h] [rbp-30h] v5 = a3 ^ (0xC6A4A7935BD1E995LL * a2); for ( i = a1; i != &a1[8 * (a2 >> 3)]; i += 8 ) { v3 = i; v5 = 0xC6A4A7935BD1E995LL * ((0xC6A4A7935BD1E995LL * (((0xC6A4A7935BD1E995LL * *(_QWORD *)v3) >> 47) ^ (0xC6A4A7935BD1E995LL * *(_QWORD *)v3))) ^ v5); } switch ( a2 & 7 ) { case 1uLL: goto LABEL_11; case 2uLL: goto LABEL_10; case 3uLL: goto LABEL_9; case 4uLL: goto LABEL_8; case 5uLL: goto LABEL_7; case 6uLL: goto LABEL_6; case 7uLL: v5 ^= (unsigned __int64)i[6] << 48; LABEL_6: v5 ^= (unsigned __int64)i[5] << 40; LABEL_7: v5 ^= (unsigned __int64)i[4] << 32; LABEL_8: v5 ^= (unsigned __int64)i[3] << 24; LABEL_9: v5 ^= (unsigned __int64)i[2] << 16; LABEL_10: v5 ^= (unsigned __int64)i[1] << 8; LABEL_11: v5 = 0xC6A4A7935BD1E995LL * (*i ^ v5); break; default: return ((0xC6A4A7935BD1E995LL * ((v5 >> 47) ^ v5)) >> 47) ^ (0xC6A4A7935BD1E995LL * ((v5 >> 47) ^ v5)); } return ((0xC6A4A7935BD1E995LL * ((v5 >> 47) ^ v5)) >> 47) ^ (0xC6A4A7935BD1E995LL * ((v5 >> 47) ^ v5)); }

Увидев эту страшную крипто-функцию, я сразу же пошел гуглить константу C6A4A7935BD1E995, первой же ссылкой получил https://github.com/vitkyrka/kninja/blob/master/mmh2.py, чуть изучив этот код я пришел к выводу, что он очень схож с функцией sub_401B1B. Подав им на входы одинаковые данные (sub_401B1B и коду), я получил одинаковый результат, что лучше всего доказывает, что sub_401B1B есть MurmurHash2 (такое название было в ссылке выше).

Загуглив MurmurHash2 reverse, натыкаемся на любимый сайт, stackoverflow. Хоть конкретно в этом вопросе и спрашивают про mmh3, а не про mmh2 во втором ответе есть полезная ссылочка с инверсией разных алгоритмов. Воспользовавшись все тойже константой C6A4A7935BD1E995, найдем нужную функцию инверсии murmur_hash_64_inverse.

Чуть поигравшись, понастраивая и помучавшись с ендингами, получил такой сишный скрипт, который получает в аргументах желаемую 8 битную строчку, и выдает 8 байт необходимых для генерации такой строчки. Потестил, работает, теперь пришло время вспомнить про s="SELECT * FROM PatientList WHERE HashID = '{src}'", и тут я приисполнился в сознании, осознал (вспомнил), что sql это максимально дырявая технология. Но так как из меня вебер (ну или кто обычно эксплуатирует sql), как из рыбы топор (лучшей аналогии в 4 часа ночи я не придумал), то я попросил помощи у пошлых одуванчиков моей команды

Они мне предожили использовать 'or not', запихнем это в уже скомпилированный код, что был разработан раньше

С первого раза не получилось, но потом я исправился. Теперь все что осталось это, отослать эти байтики на сервер, не забыв про 'хедер', который будет равен 08 (так как байтов после него 8 штук). И так же стоит не забыть про греп, чтобы потом не искать флаг в куче мусора.

Ееее флаг!

Решение

  1. Найти набор функций отвечающий за обработку внесенных данных
  2. Отреверсить их, понять, что:
    • из первых двух байт формируется хедер, отвечающий за кол-во последующих байт
    • из оставшихся байт посредством функции sub_401B1B, формируется src
    • приложение делает запрос к бд, вида "SELECT * FROM PatientList WHERE HashID = '{src}'"
  3. Понять, что sub_401B1B == MurmurHash2
  4. Найти алгоритм инверсии для MurmurHash2
  5. Найти значение src для эксплуатации "SELECT * FROM PatientList WHERE HashID = '{src}'" (один из вариантов src=='or not')
  6. С помощью шага 4 сгенерировать байты, которые преобразуются в нужное значение src (пример скрипта)
  7. Отправить нужные байты + хедер на сервер, получить флаг

Эпилог

Он будет коротким, иметь умных сокомандников - круто! А вообще, вот такие смешаные таски они и вправду прикольные, понимаешь надобность команды.

А может и неочень коротким, вот написал я уже второй райтап на русском для ctf'а, где вроде участвует немало другого народа, незнающих русский язык. Тип вот будет им интересно решение таска, а у меня мало того, что используется свободный стиль (пытаюсь не писать сухо, оставлять коментарии), так и в русском языке есть ошибки.

In short, exuse me if you read my writeups by translater.

Using russian text on photo for apology, genius!