
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 байт. Неплохо для мультиплатформенной, гуевой апликухи =)