OFFSET |
OLD BYTE |
NEW BYTE |
0x00050C45 |
0x84 |
0x33 |
0x000797B6 |
0xFF |
0xE8 |
0x000797B7 |
0x52 |
0x29 |
0x000797B8 |
0x78 |
0x94 |
0x000797B9 |
0x8B |
0xBD |
0x000797BA |
0xF0 |
0x38 |
0x000797BB |
0x3B |
0x8B |
0x000797BC |
0xF3 |
0xF0 |
0x000797C0 |
0x75 |
0xEB |
0x0010C3E2 |
0x0F |
0xB3 |
0x0010C3E3 |
0x95 |
0x01 |
0x0010C3E4 |
0xC3 |
0x90 |
0x00121DF3 |
0x85 |
0x33 |
Так же как и в любом, уважающем себя кряке мы должны реализовать проверку на длину файла и CRC. В нашем случае длина оригинального файла составляет 4120576 байт, а CRC32 равно 1927184142.
Ну что же, примерный алгоритм таков:
- Прочитать файл Lingvo.exe;
- Проверить длину и CRC32;
- Сделать бэкап;
- Пропатчить файл;
Приведу сразу же получившийся код патчера и ниже приведу объяснения.
1 import java.io.*; 2 import java.nio.channels.FileChannel; 3 import java.nio.MappedByteBuffer; 4 import java.util.zip.Checksum; 5 import java.util.zip.CRC32; 6 7 class Patch { 8 static long FILE_SIZE = 4120576; 9 static long FILE_CRC32 = 1927184142; 10 static String FILE_NAME = "Lingvo.exe"; 11 static int BYTES_TO_CHANGE = 13; 12 static long[] offsets = new long[BYTES_TO_CHANGE]; 13 static byte[] newBytes = new byte[BYTES_TO_CHANGE]; 14 15 static { 16 int i = 0; 17 offsets[i] = 0x00050C45; newBytes[i++] = (byte)0x33; 18 offsets[i] = 0x000797B6; newBytes[i++] = (byte)0xE8; 19 offsets[i] = 0x000797B7; newBytes[i++] = (byte)0x29; 20 offsets[i] = 0x000797B8; newBytes[i++] = (byte)0x94; 21 offsets[i] = 0x000797B9; newBytes[i++] = (byte)0xBD; 22 offsets[i] = 0x000797BA; newBytes[i++] = (byte)0x38; 23 offsets[i] = 0x000797BB; newBytes[i++] = (byte)0x8B; 24 offsets[i] = 0x000797BC; newBytes[i++] = (byte)0xF0; 25 offsets[i] = 0x000797C0; newBytes[i++] = (byte)0xEB; 26 offsets[i] = 0x0010C3E2; newBytes[i++] = (byte)0xB3; 27 offsets[i] = 0x0010C3E3; newBytes[i++] = (byte)0x01; 28 offsets[i] = 0x0010C3E4; newBytes[i++] = (byte)0x90; 29 offsets[i] = 0x00121DF3; newBytes[i++] = (byte)0x33; 30 } 31 32 public static void main(String[] args) { 33 try { 34 File source = new File(FILE_NAME); 35 File dest = new File(FILE_NAME + ".bak"); 36 37 if (source.length() != FILE_SIZE) { 38 System.out.println("Incorrect file size"); 39 return; 40 } 41 42 if (FILE_CRC32 != getChecksumValue(new CRC32(), FILE_NAME)) { 43 System.out.println("CRC32 Error! Version missmatch or file is already patched"); 44 return; 45 } 46 47 copyFile(source, dest); 48 49 RandomAccessFile fis = new RandomAccessFile(source, "rw"); 50 for (int i = 0; i < BYTES_TO_CHANGE; i++) { 51 fis.seek(offsets[i]); 52 fis.write(newBytes[i]); 53 } 54 fis.close(); 55 System.out.println(FILE_NAME + " - successfully patched"); 56 } catch (Exception e) { 57 e.printStackTrace(); 58 } 59 } 60 61 public static void copyFile(File source, File dest) throws IOException { 62 FileChannel in = null, out = null; 63 try { 64 in = new FileInputStream(source).getChannel(); 65 out = new FileOutputStream(dest).getChannel(); 66 67 long size = in.size(); 68 MappedByteBuffer buf = in.map(FileChannel.MapMode.READ_ONLY, 0, size); 69 70 out.write(buf); 71 72 } finally { 73 if (in != null) in.close(); 74 if (out != null) out.close(); 75 } 76 } 77 78 static long getChecksumValue(Checksum checksum, String filename) { 79 try { 80 BufferedInputStream is = new BufferedInputStream(new FileInputStream(filename)); 81 byte[] bytes = new byte[1024]; 82 int len = 0; 83 84 while ((len = is.read(bytes)) >= 0) checksum.update(bytes, 0, len); 85 86 is.close(); 87 } catch (IOException e) { 88 System.out.println("Can't read from " + filename); 89 } 90 return checksum.getValue(); 91 } 92 93 }
Итак, что же мы
видим.Строки 1-5, как обычно, импорт классов, которыми мы будем пользоваться.
Далее (7) объявление класса. Строки с 8 по 13 - объявление глобальных переменных.
Все, что нам нужно знать: размер файла, его CRC, имя и кол-во байт которое мы
хотим изменить. За эти значения отвечают поля FILE_SIZE,
FILE_CRC32,
FILE_NAME и
BYTES_TO_CHANGE
соответственно.
С 15 по 30 строку следует статический блок, в котором происходит инициализация
смещений, по которым нужно будет писать, и собственно те байты, которые нужно
писать. Т.е. это представление таблицы 1 с отсутствующим за ненадобностью средним
столбцом.
32-59 основная часть программы. Итак. Для начала (34-35) создаем ссылки на файлы
(который хотим пропатчить и на бэкап). 37-45 проверяем совпадают ли размер и
CRC с теми что у нас есть? Если да то продолжаем работу. Итак остался последний
шаг, пропатчить файл, что мы успешно (50-53) и делаем, не забывая (47) сделать
бэкап, на всякий пожарный.
Ниже 61 - 91 представлены утилитные функции для выполнения таких стандартных
действий, как копирование файлов и вычисление CRC32 для файла. Не буду останавливаться
на особенностях их реализации.
Написав, сей код и сохранив его в файле Patch.java его нужно скомпилировать.
>javac Patch.java
Мы получили Patch.class - готовый патч для Лингво, который весит 3226 байт.
Помещаем его в директорию Лингвы и запускаем
>java Patch
Lingvo.exe - successfully patched
Ну что же. Хороший результат. Но мы его улучшим. Дело в том, что обычно Java
классы распространяются в виде одного или нескольких Джава Архивов или попросту
JAR'ов (Java ARchive). Jar формат абсолютно ничем не отличается от zip файлов.
В принципе, можно зазиповать наш Patch.class обычным зипом и потом поменять
расширение с zip на jar. Но мы воспользуемся стандартной утилитой поставляемой
вместе с JDK. Которая так и называется jar.
Для начала создадим файл манифест. Он необходим для решения ряда задач, связанных
с инициализацией приложения. В частности, в файле манифеста прописывается какой
класс является главным (main) - в него и будут переданы все параметры, если
таковые имеются.
Т.к. класс у нас один, ему и быть главным. Создаем файл MANIFEST.MF с одной
единственной строчкой
Main-Class:
Patch
Сохраняем и в командной строке выполняем команду
>jar cvfm patch.jar
MANIFEST.MF Patch.class
added manifest
adding: Patch.class(in = 3226) (out= 1835)(deflated 43%)
Получаем файл patch.jar размером 2313 байт. Давайте его запустим
>java -jar patch.jar
Lingvo.exe - successfully patched
Ну что же, работает! Да и в весе стало полегче. P.S. стандартный кряк к данной
версии весит порядка 17Кб. Безусловно, даже начинающий ассемблерщик напишет
подобный патч весом менее 300байт, а гуру сделают этот патч еще в два раза легче.
Однако, выше описанный способ тоже имеет право на жизнь =)
Часть вторая.
Кейгены.
После долгих часов ломания своей мудрой головы над тем какая же связь
есть между личными данными пользователя и серийным номером Вы, наконец-то, поняли
этот хитроумный алгоритм. Ну что-же, сделайте приятное миллионам халявщиков
во всем мире - напишите для них кейген.
Для примера возьмем абстрактную программу, которая при регистрации просит Вас ввести: Имя, Фамилию, Название Вашей Организации и серийный номер продукта. Вы выяснили, что программа считает, что серийник правильный, если он совпадает с CRC, взятым от конкатенации строк: Имени, Фамилии и Названия Предприятия. Т.е. если Вы введете серийник, равеный CRC32("ВасяПупкинЛарек"), то программа посчитает Вас честным пользователем, если Вы укажете соответствующие параметры в Имени, Фамилии и Организации.
Итак, наша задача написать простенький кейген, в котором будут присутствовать поля для ввода Имени, Фамилии, Организации. А так же большая кнопка "Сгенерировать серийник", для получения серийного номера на основе введенных Вами данных.
Сразу приведу исходный код Кейгена для разбора:
1 import java.awt.*; 2 import java.awt.event.*; 3 import javax.swing.*; 4 import java.util.zip.CRC32; 5 6 public class Keygen extends JFrame { 7 String fields[] = {"First name", "Last name", "Organization", "Serial number"}; 8 JLabel[] labels = new JLabel[fields.length]; 9 JTextField[] textfields = new JTextField[fields.length]; 10 11 public Keygen() { 12 super("Mazafaka.Ru - keygen"); 13 addWindowListener(new WindowAdapter() { 14 public void windowClosing(WindowEvent e) {System.exit(0);} 15 }); 16 17 JPanel mainPanel = new JPanel(false); 18 mainPanel.setLayout(new GridLayout(1,0)); 19 20 JPanel namePanel = new JPanel(false); 21 namePanel.setLayout(new GridLayout(0, 1)); 22 23 JPanel fieldPanel = new JPanel(false); 24 fieldPanel.setLayout(new GridLayout(0, 1)); 25 26 for (int i = 0; i < fields.length; i++) { 27 labels[i] = new JLabel(fields[i] + ": ", JLabel.RIGHT); 28 namePanel.add(labels[i]); 29 30 textfields[i] = new JTextField(); 31 fieldPanel.add(textfields[i]); 32 } 33 34 mainPanel.add(namePanel); 35 mainPanel.add(fieldPanel); 36 37 JPanel bottom = new JPanel(); 38 JButton generateButton = new JButton("Generate"); 39 generateButton.addActionListener(new ActionListener() { 40 public void actionPerformed(ActionEvent e) { 41 String[] params = new String[textfields.length]; 42 for (int i=0; i < params.length - 1; i++) 43 params[i] = textfields[i].getText(); 44 generate(params); 52 setBounds(300,200,300,21*fields.length + 65); 53 setVisible(true); 54 } 55 56 public void generate(String[] params) { 57 String crc32 = ""; 58 for (int i=0; i < params.length - 1; i++) crc32 += params[i]; 59 CRC32 crc = new CRC32(); 60 crc.update(crc32.getBytes()); 61 textfields[textfields.length - 1].setText("" + crc.getValue()); 62 } 63 64 public static void main(String[] args) { 65 Keygen kg = new Keygen(); 66 } 67 }
Ну что же, давайте разберем, что мы тут понаписали.
Основная задача при написании кейгена была такая: написать такой классик, чтобы на его основе можно было легко "клепать" в будущем кейгены, не заботясь о том, что надо нарисовать окошки и т.п. А что из себя обычно представляет кейген? В сущности, это алгоритм, который на основании N введенных строчек, выдает еще одну (две, три) строчки. Каждая строчка - это поле которое должен заполнить пользователь (или получить результат).
Ок, наш класс будет
разделен на несколько частей:
GUI - независимая от алгоритма часть и никогда не изменяемая
Алгоритм - тут крякер будет реализовывать алгоритм
Список полей - собственно то, что пользователь должен заполнить/получить
Итак, поехали!
Строки 1-4 импортирование нужных для работы классов
6 - наследуемся от JFrame т.к. наша программа будет представлять из себя окошко
(невашно какое - виндовое или гнилуксовое)
7 - очень важная строка. Тут мы перечисляем все поля, которые хотим представить
пользователю. Как уже говорилось, у нас будут Имя, Фамилия, Организация. Ну
и конечно поле для серийника - надо же куда-то результат выводить =) Чтобы изменить
(добавить/удалить) поля в кейгене, достаточно изменить массивчик fields. Для
людей, разбирающихся в джаве, сразу скажу, что я не уделял внимание архитектуре
самого кода, дабы не загромождать читателя. Поэтому Вы не увидете final
static перед полем fields =)
Далее (8-9) объявляем массивы графических компонентов: меток и контролов для ввода текста. Их колличество, конечно же, равно кол-ву строк в массиве fields
Конструктор (11-54) очень рутинная работа с гуйней. Определяем специфические менеджеры компоновки, расставляем метки и ТекстФилды (26-32). Наконец, создаем кнопку (38) и создаем для нее обработчик события на нажатие. Обработчик таков, что обходит все текстовые поля и собирает значения, которые в них содержаться. Далее, передает все это в метод generate, который уже решает, что делать со всем этим добром. В нашем случае, последний элемент в массиве параметров, будет всегда пустым, т.к. поле Serial Number пользователь заполнять не будет.
Ну и сердцевина нашего кейгена - метод generate (56). Как и говорилось выше, мы делаем конкатенацию всех строк, которые ввел пользователь (57-58) и вычисляем CRC32 получившейся строки (59-60). Далее, заполняем получившимся значением поле Serial Number (61). Теперь, проделаем операцию по компилированию и упаковке аналогично тому, как я делал это в первой части. Вот и все. Смотрим, что из этого всего получилось.
Различие
в упаковке в jar файл только в том, что теперь нам нужно засунуть целых три
класса, получившихся после компиляции. Но это не проблема
>jar cvfm keygen.jar MANIFEST.MF Keygen.class
Keygen$1.class Keygen$2.class
Итого, имеем jar файл размером 2,850 байт. Неплохо для мультиплатформенной, гуевой апликухи =)