Се­год­ня мы прой­дем пол­ный цикл раз­работ­ки экс­пло­ита: от соз­дания фаз­зера до успешно­го запус­ка шелл‑кода. Под при­целом — пар­сер фай­лов BMP, которые игра при­нима­ет в качес­тве поль­зователь­ских ски­нов. Пра­виль­но сфор­мирован­ный файл при­ведет к исполне­нию про­изволь­ного кода, а зна­чит, и воз­можно­му зараже­нию игро­ка вре­донос­ным кодом.

warning

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

 

Подготовка

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

MD5

gta-vc.exe: 16094566bdaac10c7f9cc10beeac7ae8
gta-vc.exe: 32f157ea394c23ff0a91096226eccbf7

В качес­тве отладчи­ка я буду исполь­зовать Immunity Debugger. Закинь к нему в пап­ку PyCommands скрипт mona.py, он добав­ляет новые коман­ды во встро­енный интер­пре­татор 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)

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

 

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

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

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии