.. :: ____ _ :: _ ____ _ ____ _ ____ ____\ (________) _/________) _ |_____) \ _____ \\__ _______/ _| /__ | _ \/ /___ / | _ | _ | \ | __// … \\___| \___| \____| \_____| \ … +-------|______//--|_______//---|______//pO!i|_______//-----+ .. ____ _ ____ _ <<:::::::::::::::::::: :: … tEAM-fT … ____| (___\ (_______ ___ ___ ___ ___ ___ ___ ___\ _ _______/__ \\__\\__\\__\\__\\__\\__\ \\___ ___/ | ___// .. … / |__/\___| \ … :: +-----\\______|-------|_______//--+ :: :: TEAM FIFTY THREE TUTORiALS #12 :: Исследование программы Math Tablet 2.02L. Разбор алгоритма генерации ключа. Reversed by Getorix (mailto: getorix@ngs.ru) Edited By NightCat (mailto: teamft@gmail.com) Объект: Math Tablet 2.02L (мощная математическая система) Цель: Найти место проверки ключа на правильность, исследовать принцип проверки и разработать алгоритм генерации ключа Инструменты: 1. IDA Pro 4.8; 2. MS eMbedded Visual C++ 4.0 с установленным SP3; 3. PocketPC 2003 SDK (Nov. 2003); 4. MS ActiveSync 3.7.1 и выше; 5. Resource Hacker. ПОИСК ФУНКЦИИ ПРОВЕРКИ КЛЮЧА Итак, запускаем на КПК установленную программу. Сразу появляется диалог регистрации. В поле Owner видим имя, установленное в личных данных нашего КПК. Вводим произвольный код, например «123» и жмем Register. Получаем окно с сообщением «Software is not registered». Нажимаем «OK» и закрываем программу. Копируем исполняемый файл "Math Tablet.exe" с КПК на PC и запускаем IDA Pro. Выбираем File New… вкладку PDAs/Handhelds/Phones PocketPC ARM Executable. Жмем -> -> -> «OK» и выбираем наш файл "Math Tablet.exe". В появившемся окне отмечаем галочками оба пункта Imported DLL options и Analysis options. Нажимаем «Далее». Отмечаем галочкой только Create imports segment, снова нажимаем «Далее». Следующие окна оставляем без изменений, жмем три раза «Далее». Отмечаем галочкой «Start analysis now» и нажимаем кнопку «Готово». Ждем, пока IDA проанализирует его… Через некоторое время файл будет проанализирован. Теперь надо найти строку «Software is not registered». Жмем SHIFT+F12 и в окне Strings window начинаем просматривать записи. Если просматривать внимательно, можно увидеть строку "00000036" unicode Software is not registered. То что надо! Щелкаем по этой строке 2 раза, и попадаем в код: .data:00043650 aSoftwareIsNotR unicode 0, ,0 .data:00043650 ; DATA XREF: .text:off_3A014^ Смотрим DATA XREF (откуда идет обращение к строке). Щелкаем дважды на off_3A014 и попадаем в следующий код: .text:0003A00C off_3A00C DCD aErrorCanNotCha ; DATA XREF: sub_39BF0:loc_39FC8^ .text:0003A00C ; "ERROR: can not change registration" .text:0003A010 off_3A010 DCD aTrialTimeReset ; DATA XREF: sub_39BF0+3D0^ .text:0003A010 ; "Trial time reset. Please return..." .text:0003A014 off_3A014 DCD aSoftWareIsNotR ; DATA XREF: sub_39BF0+360^ .text:0003A014 ; "Software is not registered" Хм, сколько любопытных надписей. Снова щелкаем дважды по sub_39BF0+360 в DATA XREF и попадаем на адрес 39F50. Смотрим выше и видим: .text:00039F24 CMP R0, #1 .text:00039F28 MOV R3, #0 ;uType .text:00039F2C BNE loc_39F4C .text:00039F30 LDR R2, =aRegistration ; lpCaption .text:00039F34 LDR R1, =aRegistrationSu ; lpText .text:00039F38 LDR R0, [R6] ; hWnd .text:00039F3C BL MessageBoxW .text:00039F40 MOV R4, #4 .text:00039F44 MOV R9, R4 .text:00039F48 B loc_39FE0 .text:00039F4C ;----------------------------------------------------- .text:00039F4C .text:00039F4C loc_39F4C ; CODE XREF: sub_39BF0+33C^ .text:00039F4C CMP R0, #0 .text:00039F50 LDREQ R1, =aSoftwareIsNotR .text:00039F54 BEQ loc_39FD0 Окно о неудачной регистрации выпадает по адресу 39F4C в результате выполнения команды BNE (Branch if not equivalent - перейти, если не равно) по адресу 39F2C, на что указывает черная пунктирная стрелка. Если же инструкция не срабатывает – выдается окно со строкой «Registration Successful». BNE смотрит результат выполнения операции CMP R0, #1 по адресу 39F24, поскольку инструкция MOV R3, #0 флаги не меняет. Что ж, значит перед операцией сравнения должна быть функция проверки ключа, однако мы наблюдаем там вызов DialogBoxIndirectParamW. Безупречное знание ARM ассемблера уже подсказывает вам, что результат работы этой функции будет занесен в регистр R0, с которым и происходит сравнение. Заглянув в MSDN, обнаруживаем следующую информацию: «This function creates a modal dialog box from a dialog box template in memory», что дословно переводится как «Функция создает модальный диалог из шаблона в памяти». Можно в MSDN, а можно и в коде IDA посмотреть параметры и результат этой функции. Находим любопытный параметр – lpDialogFunc. Это указатель на функцию-обработчик событий диалога, результат которой (с помощью вызова EndDialog) и возвращается функией DialogBoxIndirectParamW. Итак, адрес этого обработчика – 3A038. Переходим в IDA на этот адрес. Просмотрев функцию, обнаруживаем в ней массу вызовов GetDlgItem, SendMessageW и EndDialog. Трассировать ее нет никакого желания, поэтому подойдем к поиску нужного нам кода с умом. А нужно нам найти чтение имени, ключа и самое главное место проверки этого ключа на правильность. Пора вспомнить что наш диалог загружается из шаблона, значит пора запускать ResHacker. Ищем наше диалоговое окно в папке Dialog и вскоре находим в папке 119, ресурс 1033. Теперь мы знаем, что ID поля Owner – 1002 (3EA), а поля Code – 1003 (3EB). Функция GetDlgItem включает следующие параметры: hDlg (handle окна) и iCtrlID (ID элемента управления). И снова знания ARM ассемблера нам подсказывают, что первый параметр перед вызовом функции будет занесен в регистр R0, а второй в R1. Следовательно, для загрузки ключа нам нужна строка типа MOV R1, 0x3EB. Ищем от начала нашей функции-обработчика 3A038. Находим два адреса: 3A0AC и 3A334. Должно быть, где-то рядом то, что мы ищем… В принципе можно и дальше искать функцию генерации кода чисто аналитически, но раз уж мы локализовали место обращения к ключу в диалоге, не будем мучиться и перейдем в отладчик. Загружаем eVC, создаем проект: File -> Open… -> Тип файлов Executable Files -> "Math Tablet.exe". Жмем «ОК» и запускаем программу через Build->Start Debug->Step Intro или просто нажимаем F11. Останавливаемся на первой строке (у меня это 2A040594). Учтите вместо 2A у вас может быть другое смещение. Нажимаем CTRL+G и вводим первый адрес - 3A0AC. Ставим на него бряк (F9). Снова нажимаем CTRL+G и вводим второй адрес - 3A334. Также ставим на него бряк (F9). Запускаем программу… Вводим в поле Code 123, жмем Register. Оп-па! Останавливаемся по адресу 3A0AC. Что ж трассируем по F11. Зная о некорректной работе eVC c инструкциями типа BL, ставим после них бряк и трассируем дальше. Сразу становится любопытно, что же за сообщение посылается с помощью SendMessageW по адресу 3A0C8 после GetDlgItem. Первый параметр этой функции – handle нашего окна, второй - 0xD (что не очень понятно). Однако опыт подсказывает нам, что коды параметров с названиями можно посмотреть в SDK, а конкретно в файле winuser.h. Ищем его на винте и смотрим: #define WM_GETTEXT 0x000D, то есть считать текст. Отлично – то, что надо! После вызова этой функции идет цикл (3A0D4 - 3A0E0). Попробуем понять, что он делает. 2A03A0CC add r3, sp, #0x2C [cохранить в R3 адрес на память] 2A03A0D0 sub r3, r3, #2 [отнять от этого адреса 2] 2A03A0D4 ldrh r0, [r3, #2]! [загрузить в R0 данные из памяти по адресу R3+2] 2A03A0D8 mov r1, r0, lsl #16 [сдвинуть R0 на 16 бит влево и записать в R1] 2A03A0DC movs r2, r1, lsr #16 [сдвинуть R1 на 16 бит вправо и записать в R2] 2A03A0E0 bne 2A03A0D4 [если R2 и сдвинутый R1 не равны – переход] Для начала нас интересует, что же лежит по адресу «sp, #0x2C» (у меня это 2A06F790). Выделим его в eVC в окне регистров и перетащим на окно памяти. Опа – это же наш ключ! Таким образом, мы грузим по одной букве ключа, с сохранением нового значения (R3+2) регистра R3 (на это указывает «!» в конце команды) и смотрим, когда содержимое регистра с кодом этой буквы со сдвигом будет равно содержимому регистра с кодом этой буквы без сдвига. Такое возможно только тогда, когда код буквы – 0. Сравнение осуществляется с помощью команды MOVS, которая не только перемещает, но и сравнивает перемещаемое значение с источником, выставляя соответствующие флаги. Таким образом, после цикла в R3 у нас сохраняется адрес конца ключа (код 0). Далее: 2A03A0E4 E28D002C add r0, sp, #0x2C - снова заносим в R0 указатель на ключ в памяти 2A03A0E8 E0430000 sub r0, r3, r0 - в R0 заносим длину ключа в памяти (R3 – R0) 2A03A0EC E1A010A0 mov r1, r0, lsr #1 - делим R0 на 2, получаем кол-во символов ключа 2A03A0F0 E3510008 cmp r1, #8 - сравниваем длину ключа с 8 2A03A0F4 9A000040 bls 2A03A1FC - если длина меньше 8 – переход То есть длина ключа должна быть меньше 8 символов. Что ж, запомним. Переходим по адресу 3A1FC. Сначала посмотрим этот код в IDA: .text:0003A1FC loc_3A1FC ; CODE XREF: sub_3A038+DC^ .text:0003A1FC ADD R0, SP. #0xA4+var_78 ; wchar_t * .text:0003A200 BL _wtol .text:0003A204 MOV R1, #0x3E8 .text:0003A208 STR R0, [SP, #0xA4+var_90] .text:0003A20C ORR R1, R1, #2 ; iCtrlID .text:0003A210 MOV R0, R6 ; hDlg .text:0003A214 BL GetDlgItem .text:0003A218 ADD R3, SP, #0xA4+var_78 ; lParam .text:0003A21C MOV R2, #0x32 ; wParam .text:0003A220 MOV R1, #0xD ; Msg .text:0003A224 BL SendMessageW .text:0003A228 LDR R0, [SP,#0xA4+var_90] .text:0003A22C ADD R1, SP, #0xA4 .text:0003A230 BL sub_39860 .text:0003A234 TST R0, #0xFF .text:0003A238 BEQ loc 3A2C8 Первая строка по адресу 3A1FC аналогична ADD r0, sp, #0x2C, то есть представляет собой занесение в R0 адреса ключа в памяти. В этом можно убедиться, посмотрев на адрес 3A1FC в eVC. А дальше вызывается какая-то функция _wtol. Снова MSDN и видим, что это преобразование string to double. Далее в R1 заносится какое-то очень знакомое значение – 3E8 (1000), а потом с помощью команды ORR оно превратится в 3EA (1002). Это же ID поля с именем! Далее идет совсем уже знакомый код с GetDlgItem и SendMessageW, который загружает наше имя в память. Следующие после SendMessageW две строки лучше трассировать в eVC. Ставим бряк на адрес 3A228 и нажимаем F5 (запуск программы). Первая строка загружает из памяти в R0 значение по адресу 2A06F778. Там оказалось значение 7B. Вспоминая недавний вызов функции _wtol, проверяем на калькуляторе. 7Bh = 123. Вторая строка сохраняет в R1 указатель на адрес в памяти (2A06F790). Посмотрим, что там лежит… ага – наше имя. Ну а дальше идет вызов какой-то подпрограммы по адресу 39860, параметрами которой, кстати, является числовое представление введенного нами ключа (R0) и наше имя (R1). Ну вот и нашлась наша функция проверки. Переходим туда… АНАЛИЗ ФУНКЦИИ ПРОВЕРКИ КЛЮЧА Что ж, начнем по порядку. Итак, мы в eVC по адресу 39860. 2A039860 E92D4FF0 stmdb sp!, {r4 - r11, lr} - сохраняем регистры с стеке 2A039864 E1A0A000 mov r10, r0 - R0 (ключ) копируем в R10 2A039868 E1A08001 mov r8, r1 - R1 (адрес имени) копируем в R8 2A03986C E59F4288 ldr r4, [pc, #0x288] - в R4 грузим какой-то адрес (00043540) Выделяем этот адрес (00043540) в окне регистров, перетаскиваем его на окно памяти и видим, что это адрес на слово «Unknown». Теряемся в догадках, но видимо это имя используется в том случае, если в личных данных КПК имя не указано. Смотрим дальше. 2A039870 E3A090D9 mov r9, #0xD9 - в R9 заносим 0xD9 (217) 2A039874 E1A07009 mov r7, r9 - копируем это значение в R7 Видимо это значение будет использоваться для генерации кода из имени, поэтому запоминаем его на всякий случай и смотрим дальше. 2A039878 E0483004 sub r3, r8, r4 - в R3 заносим разность адресов R8 и R4 Вспоминаем, что по адресу R8 у нас наше имя, а по адресу R4 лежит Unknown. Зачем нам их разность? Опыт подсказывает – для того чтобы синхронно двигаться посимвольно. Действительно если разность между первыми буквами имен прибавить ко второй букве первого имени (с младшим адресом) – мы получим адрес второй буквы второго имени. Видимо не за горами сравнение нашего имени с «Unknown». 2A03987C E19300B4 ldrh r0, [r3, +r4] - загрузить в R0 первую букву имени Почему первую букву нашего имени? Считаем вместе. В R4 лежит адрес на «Unknown», в R3 разность между первыми буквами. Тогда R3+R4 даст нам адрес первой буквы нашего имени. Все просто. 2A039880 E1A01800 mov r1, r0, lsl #16 - зачем-то сдвигаем R0 на 16 бит влево 2A039884 E0D400B2 ldrh r0, [r4], #2 - загружаем в R0 первую букву из “Unknown” Первую - потому что данная инструкция с пост-инкрементом, то есть сначала берется значение из R4 а после выполнения операции загрузки к нему добавляется 2 (то есть адрес следующей буквы). 2A039888 E1A02821 mov r2, r1, lsr #16 - код первой буквы нашего имени сдвигаем на 16 бит вправо и сохраняем в R2 2A03988C E1A01800 mov r1, r0, lsl #16 - код буквы «U» сдвигаем на 16 бит влево и сохраняем в R1 2A039890 E1520821 cmp r2, r1, lsr #16 - код буквы «U» сдвигаем на 16 бит вправо с сравниваем с первой буквой нашего имени в R2 2A039894 1A000003 bne 2A0398A8 - если буквы не равны – переход на 2A0398A8. Вообще не понятно, зачем нужна такая куча сдвигов для просто проверки двух кодов букв. Однако интересно, что будет, если первая буква нашего имени тоже «U». Итак, если буквы все же одинаковы – переход не будет осуществлен, и мы попадаем на следующую инструкцию: 2A039898 E3520000 cmp r2, #0 - сравниваем первую букву нашего имени с 0 2A03989C 1AFFFFF6 bne 2A03987C - если не 0 – переход на 2A03987C Смотрим, что инструкция по адресу 2A03987C загружала у нас первую букву имени, но после этого R4 увеличился на 2, то есть сейчас указывает уже на следующую букву. Значит, мы были правы – идет сравнение нашего имени с «Unknown». Имя ограничивается кодом 0, значит после проверки всех букв условие cmp r2, #0 будет верно. И что же произойдет? Смотрим 2A0398A0 E3A00000 mov r0, #0 - заносим 0 в R0 2A0398A4 E8BD8FF0 ldmia sp!, {r4 - r11, pc} - выходим из нашей функции Таким образом если наше имя «Unknown», функция проверки вернет 0, то есть регистрация не будет успешной. Нет, это не для нас. Вспомним, что если одна из букв нашего имени не совпадет с «Unknown», осуществится переход по адресу 2A0398A8. Посмотрим что там. 2A0398A8 E2486002 sub r6, r8, #2 - отнимаем от адреса нашего имени 2 2A0398AC E1A03006 mov r3, r6 - копируем получившийся адрес в R3 2A0398B0 E1F300B2 ldrh r0, [r3, #2]! - загружаем в R0 букву нашего имени по адресу R3+2 (то есть первую) и сохраняем новый адрес в регистре R3 (на это указывает «!»). 2A0398B4 E1A01800 mov r1, r0, lsl #16 - в R1 сдвиг кода буквы на 16 бит влево 2A0398B8 E1B02821 movs r2, r1, lsr #16 - в R2 сдвиг R1 на бит вправо и сравнение 2A0398BC 1AFFFFFB bne 2A0398B0 - переход на загрузку следующего символа Идентичную конструкцию мы разбирали в функции обработчике, оказалось, что она просто считает количество символом в строке. Ставим бряк на следующую после BNE строку и жмем GO (А5). В регистре R3 будет адрес конца строки (код 0). 2A0398C0 E0430008 sub r0, r3, r8 - в R0 сохраняем разницу между началом (R8) и концом (R3) строки 2A0398C4 E1A010A0 mov r1, r0, lsr #1 - делим на 2 (сдвиг на 1 бит вправо) и в R1 получаем реальное число символов. 2A0398C8 E1A02801 mov r2, r1, lsl #16 2A0398CC E1A03822 mov r3, r2, lsr #16 2A0398D0 E1A00803 mov r0, r3, lsl #16 2A0398D4 E1A01840 mov r1, r0, asr #16 Не расстраивайтесь, мне тоже не понятно, зачем забивать все регистры R0-R4 значением количества символов, сдвинутыми в разные стороны. Никакого эффекта от этого пока не видно, а значит смотрим дальше. 2A0398D8 E3A0E00A mov lr, #0xA - в регистр LR заносим значение 0хA (10) 2A0398DC E351000A cmp r1, #0xA - сравниваем кол-во символов нашего имени (в R1) с 10 2A0398E0 DA000009 ble 2A03990C - если меньше или равно – переход на 2A03990C Очевидно, что это проверка на длину имени, но что если имя действительно больше 10 символов. Тогда переход не будет осуществлен и программа перейдет на следующую строку. Посмотрим что там. 2A0398E4 E2410005 sub r0, r1, #5 - в R0 записываем R1-5 (длина имени - 5) 2A0398E8 E0881080 add r1, r8, r0, lsl #1 - умножаем R0 на 2 (получаем длину имени в памяти), складываем с адресом первой буквы (R8) получаем адрес (длина-5)буквы, все это в R1 2A0398EC E288400A add r4, r8, #0xA - в R4 заносим адрес 6-й буквы имени 2A0398F0 E0413004 sub r3, r1, r4 - в R3 заносим разность между R1 и R4, то есть между (длина-5)-ой буквой и 6-ой. Догадывается – для синхронизации. 2A0398F4 E19420B3 ldrh r2, [r4, +r3] - загружаем в R2 (длина – 5)-ю букву 2A0398F8 E1A00802 mov r0, r2, lsl #16 - сдвиг для последующей проверки 2A0398FC E0C420B2 strh r2, [r4], #2 - запись символа по адресу 6-ой буквы 2A039900 E1B01820 movs r1, r0, lsr #16 - сдвиг со сравнением (проверка на 0) 2A039904 1AFFFFFA bne 2A0398F4 - если не 0 переход на чтение нового символа Отчасти код очень похож на код сравнения нашего имени с «Unknown», разница состоит в том, что мы копируем по символу из конца в начало после 5-й буквы. Причем перемещаем начиная с (длина-5)-го символа. То есть из строки Vasya_Pupkin получится Vasyaupkin. Таким образом идет адаптация строки к 10 символам. Сдвиги кодов символов обеспечивают проверку на конец строки, так как 0 сдвинутый в любую сторону остается нулем. А переход к следующему символу осуществляется в команде записи в память (strh) с помощью знакомого формата с пост- инкрементом - strh r2, [r4], #2. Полсе этого идет команда восстановления в R3 длины имени 2A039908 E1A0300E mov r3, lr и дальше как раз идет адрес, на который мы бы попали, будь наше имя меньше 10 символов. То есть адрес 2A03990C. Исследуем дальше. 2A03990C E1A00803 mov r0, r3, lsl #16 - в R0 пишем сдвинутый на 16 бит влево R3 2A039910 E59FB1E0 ldr r11, [pc, #0x1E0] - в R1 грузим какое-то значение (55555556) 2A039914 E1A05840 mov r5, r0, asr #16 - в R5 заносится длина имени 2A039918 E3A00000 mov r0, #0 - R0 очищается 2A03991C E3550000 cmp r5, #0 - длина имени в R5 сравнивается с 0 2A039920 DA000013 ble 2A039974 - если длина нулевая – переход Будем считать, что длина у нас не нулевая, и в этом коде нас волнует лишь один вопрос – что такое 55555556. Хм, если задуматься это практически треть FFFFFFFF, только еще +1. Пока не понятно, идем дальше. 2A039924 E1A04000 mov r4, r0 - очищаем R4 2A039928 E0880084 add r0, r8, r4, lsl #1 - умножаем R4 на 2 и прибавив к R8 пишем в R0, а в R8 как мы помним лежит первая буква нашего имени. R4 пуст, поэтому R0 будет равен R8, потом там будет адрес текущей буквы 2A03992C E1D010B0 ldrh r1, [r0] - загружаем букву имени в регистр 2A039930 E1A02801 mov r2, r1, lsl #16 - сдвигаем ее код влево на 16 бит 2A039934 E1A03822 mov r3, r2, lsr #16 - потом вправо на 16 бит 2A039938 E3530020 cmp r3, #0x20 - и сравниваем с 0х20 (кстати это пробел) 2A03993C 0A000005 beq 2A039958 - если пробел, прыгаем куда-то дальше Снова будем считать, что первый символ нашего имени не пробел, а значит BEQ (branch if equal – переход если равно) не сработает. Оказываемся на следующей команде. 2A039940 E0C10B94 smull r0, r1, r4, r11 Остановимся подробнее на этой команде. Это так называемое БОЛЬШОЕ умножение с результатом в 64 бита. В данном случае R4 будет умножено на R11 и младшие 32 бита будут помещены в регистр R0, старшие в R1. Вспоминаем, что в R11 у нас лежит странное на вид значение (55555556), как мы уже догадались – это на 1 больше трети от FFFFFFFF. Так что если в R4 будет значения 0, 1 или 2, регистр R1 останется пуст, а если в R4 будет 3, то в регистре R1 появится единица, так как результат будет больше 32 бит. Интересное наблюдение, но смотрим дальше. 2A039944 E0810FA1 add r0, r1, r1, lsr #31 - в R0 сохраняем сумму R1 и R1 сдвинутого на 31 бит вправо 2A039948 E0801080 add r1, r0, r0, lsl #1 - в R1 сохраняем сумму R0 и R0*2 Так как в R1 у нас 0, на данном этапе назначение этих команд неясно. Трассируем дальше. 2A03994C E0442001 sub r2, r4, r1 - отнимаем R1 от R4 и сохраняем в R2 2A039950 E2820001 add r0, r2, #1 - добавляем к R2 единицу, сохраняем в R0 2A039954 E0277390 mla r7, r0, r3, r7 - команда делает так R7 = R0*R3 + R7 А в R7 у нас лежит D9 (217), вот оно и пригодилось. Оказывается с этого числа начинается накопление произведения кода символа (R3) с каким-то множителем (R0). 2A039958 E2840001 add r0, r4, #1 - к R4 добавляем 1 и записываем результат в R0 2A03995C E1A01800 mov r1, r0, lsl #16 - знакомые непонятные сдвиги 2A039960 E1A02841 mov r2, r1, asr #16 - знакомые непонятные сдвиги 2A039964 E1A00802 mov r0, r2, lsl #16 - знакомые непонятные сдвиги 2A039968 E1A04840 mov r4, r0, asr #16 - знакомые непонятные сдвиги 2A03996C E1540005 cmp r4, r5 - сравнение R4 c R5 (длиной имени) 2A039970 BAFFFFEC blt 2A039928 - если меньше – переход на начало цикла Очевидно, что это самая главная функция генерации правильного ключа. Мы уже точно знаем, что команда smull фиксирует момент, когда R4 равно 3 и выше. При этом заполняется регистр R1. Сам R4 хранит номер текущего символа и сравнивается в конце с R5 (длиной строки). Накопление с помощью команды mla происходит в регистре R7. Также заметим, что если встречается символ пробел, осуществляется переход на инструкцию после mla (2A039958), то есть накопление не происходит (пробел пропускается). Единственным непонятным местом осталось формирование множителя R0 в команде mla. Начинаем отлаживать. Прокрутим цикл три раза, чтобы изучить, что происходит при работе с четвертым символом (то есть R4 = 3). Доходим от начала цикла до команды smull. R4 равно 3, проходим команду и смотрим значение регистров R0 и R1. R0 = 2, R1 = 1. После выполнения команд по адресам 39944 и 39945 получаем R0 = 1, R1 = 3. Дальше от R4 (номер текущего символа - 3) отнимаем R1 (тоже 3), и получаем 0 в регистре R2. Потом прибавляем к R1 единицу и записываем в R0. Так-так-так. Получается, что множитель меняется последовательно от 1 до 3. Проверим догадку трассируя символ 7. После smull имеем R0 = 4, R1 = 2. После выполнения команд по адресам 39944 и 39945 получаем R0 = 2, R1 = 6. После разности R4 (6) и R1 получаем R2 = 0, и добавив единицу имеем R0 = 1. Что и требовалось доказать. Если номер буквы в имени кратен трем сбрасываем множитель с единицу. Допустим слово кончилось, BLT не сработал и мы попадаем дальше: 2C039974 E157000A cmp r7, r10 - сравниваем R7 (накопитель) c R10 2C039978 0A000059 beq 2C039AE4 - если равны – переход Вспоминаем, что в R10 у нас лежит преобразованный в double введенный ключ. А в R7 правильное значение ключа, сгенерированное программой. Любопытная проверка, но что дальше? 2C039AE4 E3A00001 mov r0, #1 - в R0 пишем 1 2C039AE8 E8BD8FF0 ldmia sp!, {r4 - r11, pc} - выход из функции Таким образом, функция проверки вернет 1, то есть регистрация будет успешной. В принципе осталось перевести значение из R7 в десятичную систему счисления и зарегистрироваться заново. Но мы попытаемся разработать генератор ключей. АЛГОРИТМ ГЕНЕРАЦИИ КЛЮЧА Пора вспомнить все, что мы накопали и составить алгоритм генерации ключа. Обозначим переменные: String Name – имя пользователя Int Code – ключ Int Acc - накопитель ключа Int sCnt – счетчик символов Если длина имени больше 10 символов, замещаем символы с 6 по 10 включительно последними пятью символами. После замещения остальные символы после 10-го стираются. Пример: Vasya_Pupkin станет Vasyaupkin if (strlen(Name)>10) for (int i=0;i<5;i++) Name[5+i]=Name[strlen(Name)-5+i)]; Заносим начальные значения в накопитель кода и счетчик символов. Acc = 217; sCnt = 1; Далее накапливаем в Acc произведения кода символа и счетчика sCnt, меняющегося последовательно от 1 до 3. Символ пробела пропускается. for(int i=0; i