суббота, 10 марта 2012 г.

Бессмертная свинья

 Определенно я не являюсь фанатом казуальных игр и "Raging Pigs Free" была скачана именно с целью попробовать изменить ход игрового процесса. Получилось сделать небольшой патч делающий *свинью* бессмертной:

Набор использованных инструментов следующий:

  • otx - "object tool extended", использует otool для дизассемблированния Mach-O приложений и серьезно улучшает вывод последнего, добавляя машинные коды для каждой инструкции, более подробную информацию по методам и т.д.
  • class-dump - консольное приложение позволяющее вытащить Obj-C runtime информацию из Mach-O файлов, генерирует вывод в виде легкочитаемых объявлений классов Obj-C.
  • gdb - GNU debugger
  • 0xED - Hex редактор
  • as - Mac OS X Mach-O GNU-based assemblers
  • codesign - Create and manipulate code signatures
Этап 1. Точка входа

У меня не было заранее определенных целей, КАК повлиять на игру, анализ начался с просмотра содержимого вывода class-dump.

Приложение представляет из себя FAT binary с поддержкой двух архитектур
$ lipo -info Raging\ Pigs\ Free
Architectures in the fat file: Raging Pigs Free are: x86_64 i386
 i386 asm на данный момент мне несколько ближе и понятнее, поэтому все дальнейшие манипуляции проводил с i386 вариантом приложения.

Получим class-dump для i386 вместе с подписями адресов реализации методов классов (флаг -A)
$ class-dump --arch i386 -A Raging\ Pigs\ Free 
По префиксу множества классов: CCBlendProtocol, CCKeyboardEventDelegate, CCAction и т.д. можно легко догадаться, что приложение построено на Cocos2D движке, во всяком случае на основе одной из его вариаций. Мое внимание привлек класс Pig, а в частности его метод
- (void)killPig:(int)arg1;      // IMP=0x00092e6a
О назначении можно легко догадаться по названию, неплохое место, для того, чтобы начать попытки.

Этап 2. Внедрение 


Для того, чтобы иметь возможность подсмотреть за происходящим в приложении во время его работы логично использовать gdb.
$ gdb -arch i386 Raging\ Pigs\ Free
(gdb) b *0x00092e6a
(gdb) run
Первой командой мы запускаем gdb, передаем ему приложение и напутствуем о том, что нас интересует архитектура i386 и именно этот вариант приложения мы хотим отлаживать. Далее выставляем breakpoint по уже известному нам адресу метода killPig: и запускаем приложение из gdb.

Задачей теперь является привести игровой процесс к моменту, когда потенциально будет вызван необходимый нам метод - подставим свинью под птицу. Задача не сложная и прямо в момент, когда поросенок должен был бы умереть приложение застынет и в gdb мы увидим:
Breakpoint 1, 0x00092e6a in ?? ()
В стеке ничего особо читаемого нет:
(gdb) bt
#0  0x00092e6a in ?? ()
#1  0x00086b5f in ?? ()
#2  0x000869ed in ?? ()
#3  0x00092dcf in ?? ()
#4  0x00085c09 in ?? ()
#5  0x00092ab0 in ?? ()
#6  0x000899a9 in ?? ()
#7  0x0004411b in ?? ()
#8  0x00046623 in ?? ()
#9  0x0005ccdc in ?? ()
#10 0x0005c96a in ?? ()
#11 0x0005cae1 in ?? ()
#12 0x93f06a88 in CVDisplayLink::performIO ()
#13 0x93f056bc in CVDisplayLink::runIOThread ()
#14 0x93f052ea in startIOThread ()
#15 0x9004a259 in _pthread_start ()
#16 0x9004a0de in thread_start ()
С помощью gdb можно легко посмотреть код текущего метода:

(gdb) x/10i $pc
0x92e6a: push   %ebp
0x92e6b: mov    %esp,%ebp
0x92e6d: push   %ebx
0x92e6e: push   %edi
0x92e6f: push   %esi
0x92e70: sub    $0xc,%esp
0x92e73: call   0x92e78
0x92e78: pop    %esi
0x92e79: mov    0x10(%ebp),%edi
0x92e7c: mov    0x8(%ebp),%ebx

Этой командой выведено 10 инструкций начиная с места на которое указывает регистр $pc, т.е. с текущей команды ожидающей выполнения. Что интересного можно тут узнать, к примеру, что за параметр был передан в метод. Содержимое стека будет следующим:

  • На вершине стека лежит адрес возврата, который оказался там благодаря команде calll - 0x00086b5f - результаты команды bt, с нами согласны
(gdb) x/x $esp
0xb020fb3c: 0x00086b5f 

  • Дальше, через 4 байта лежит адрес объекта у которого вызывается метод, он является первым параметром метода dyld_stub_objc_msgSend, который используется в Obj-C для посылки сообщений, т.е. для вызова методов.
(gdb) x/x $esp+4
0xb020fb40: 0x00294ac0 
  •  Вторым параметром dyld_stub_objc_msgSend является указатель на строку метода, который будет вызван
(gdb) x/x $esp+8
0xb020fb44: 0x000acc2e
(gdb) x/s 0x000acc2e
0xacc2e: "killPig:"

  • Далее передаются все параметры с которыми вызывается метод, в данном случае параметр у нас один, типа int:
(gdb) x/d $esp+12
0xb020fb48:
 Итого, метод killPig: был вызван с магическим параметром "2", что нам это дает совершенно не ясно, как оказалось это и не важно, следующей моей попыткой было просто сделать "ret" даже не заходя в тело функции...

(gdb) ret
Make selected stack frame return now? (y or n)
(gdb) c
Таким образом код функции не выполнялся, а сразу произошел возврат выше по стеку и далее "c" , продолжилось выполнение приложения, это привело к потрясающим результатам - свин выжил!

Этап 3. Закрепляемся

На данном этапе уже ясно, как изменить игровой процесс, осталось закрепиться. Чтобы закрепиться нужно, как-то подправить код программы, исходников конечно же нет, править нужно в бинарном виде и нужно научиться компилировать отдельные asm команды в их бинарное представление. На данном этапе полезным окажется "as", моя вспомогательная утилита выглядит следующим образом:

echo $1 >> temp.asm
echo "nop" >> temp.asm
as -arch i386 -o temp.bin temp.asm
rm temp.asm
od -N 256 -t x1 temp.bin | tail -n+10
rm temp.bin
Парметр пользователя это asm команда в синтаксисе at&t, следом к ней прикрепляется "nop", чтобы знать, где заканчиваются полезные данные, получившийся бинарный файл читается в HEX с отбрасыванием первых 9 строк, т.к. они являются стандартными и после них идет непосредственно наша команда.

Использование выглядит следующим образом:

$ ./opcode "movl  %esp,%ebp"
0000220    00  00  00  00  00  00  00  00  89  e5  90  00              
0000234
Таким образом команда "movl %sp,%ebp" превращается в 89e5

При помощи otx получим дизассемблированный и в хорошем виде код функции killPig:
-(void)[Pig killPig:]
+0 00092e6a  55  pushl  %ebp
+1 00092e6b  89e5  movl  %esp,%ebp
+3 00092e6d  53  pushl  %ebx
+4 00092e6e  57  pushl  %edi
+5 00092e6f  56  pushl  %esi
+6 00092e70  83ec0c  subl  $0x0c,%esp
+9 00092e73  e800000000  calll  0x00092e78
   +14 00092e78  5e  popl  %esi
   +15 00092e79  8b7d10  movl  0x10(%ebp),%edi
   +18 00092e7c  8b5d08  movl  0x08(%ebp),%ebx
   +21 00092e7f  89bbd4010000  movl  %edi,0x000001d4(%ebx)    (int)deathtype
   +27 00092e85  c683c801000001  movb  $0x01,0x000001c8(%ebx)    (BOOL)dying
   +34 00092e8c  8b83cc010000  movl  0x000001cc(%ebx),%eax    (GameScreenLayer)gameScreenLayer
   +40 00092e92  8b8ec8770200  movl  0x000277c8(%esi),%ecx
   +46 00092e98  894c2404  movl  %ecx,0x04(%esp)
 Это самое начало. Моей первой и последней попыткой было заменить первый байт кода на "ret", т.е. заставить функцию сразу вернуться без произведения каких-либо действий.

$ ./opcode "ret"
0000220    00  00  00  00  00  00  00  00  c3  90  00  00              
0000234
Т.е. код команды "ret" также занимает 1 байт и это "c3", на самом деле подсмотреть можно в самом выводе otx:
  +185 00092f23  c3  ret 
Патчинг производил с помощью 0xED. Адрес 0x00092e6a не имеет ничего общего с местоположением кода в файле и является runtime информацией. На данный момент самым простым методом определения нужного места в файле явлется поиск. По выводу otx можно видеть, что интересующее нас место выглядит как 5589e553575683ec0ce8000000005e8b7d10, это дает нам уникальный результат HEX поиска по файлу (адрес 1A2E6A).
55 -> c3 # pushl %ebp -> ret
Попытка игры после такого исправления закончится crash'ем

 "Exception Type:  EXC_CRASH (Code Signature Invalid)" - приложение из AppStore, после codesign, а я так нагло вклиниваюсь, естественно надо все переподписать.
$ sudo codesign -f -s "My Home Cert" Raging\ Pigs\ Free
Таким образом осуществляется переподписывание кода, новый становится легальным. Последним важным моментом является необходимость отметить приложение для запуска в 32bit режиме.
Результаты проделанной работы видны на видео, свинья стала бессмертной!


Комментариев нет: