Công cụ Chúng ra sẽ cần Stripper, IDA, và một chương trình biên dịch ngôn ngữC.
Việc giải nén khá là đơn giản. Ta sẽ dùng Stripper v2.0.7 để giải nén crackme này. Bài viết này không chú trọng vào việc giải nén.
12 trang |
Chia sẻ: haohao89 | Lượt xem: 2292 | Lượt tải: 1
Bạn đang xem nội dung tài liệu Tài liệu Hướng dẫn Crackme 1 của Mer, để tải tài liệu về máy bạn click vào nút DOWNLOAD ở trên
Hướng dẫn Crackme 1 của Merc
Introduction:
Giới thiệu Bài viết này sẽhướng dẫn người đọc cách đọc code crackme
đầu tiên của Merc đểtừđó viết chương trình keygen.
Tools:
Công cụChúng ra sẽcần Stripper, IDA, và một chương trình biên dịch
ngôn ngữC.
Việc giải nén khá là đơn giản. Ta sẽdùng Stripper v2.0.7 đểgiải nén
crackme này. Bài viết này không chú trọng vào việc giải nén.
Guess Phỏng đoán:
Chạy thửcrackme, ta thấy crackme đòi nhập một chuỗi nào đó rồi
nhấn nút Check. Nhấn nút Check thì crackme tựđộng thoát ra.
Việc kết thúc chương trình một cách ép buộc này làm ta nghĩngay
đến các hàm ExitProcess, TerminateProcess, SendMessage,
PostMessage và các hàm tương tự.
Disasm:
Dịch ngược và hiểu mã
Ta dùng IDA đểdịch ngược. Sau khi đã dịch ngược xong, chọn cửa
sổNames window, tìm hàm ExitProcess hoặc PostQuitMessage. Tại
sao? Vì khi ởcông ty thì tác giảbài viết phát hiện ra từhàm
ExitProcess có thểlần ngược lại hàm kiểm tra chuỗi. Nhưng đến khi về
nhà thì tác giảkhông còn lần ngược lại được từExitProcess nữa mà
phải từPostQuitMessage.
Quay trởlại cửa sổIDA View. Cuộn lên trên cho tới khi thấy đoạn
mã sau.
.text:00401440 83+ sub esp, 7Ch
.text:00401443 53 push ebx
.text:00401444 8B+ mov ebx, ecx
.text:00401446 33+ xor ecx, ecx
.text:00401448 56 push esi
.text:00401449 89+ mov [esp+9], ecx
.text:0040144D 57 push edi
.text:0040144E 89+ mov [esp+11h], ecx
.text:00401452 A0+ mov al, byte_418148
.text:00401457 89+ mov [esp+15h], ecx
.text:0040145B BE+ mov esi, offset aGoodJobTheSolu ;
"Good Job! The Solution Is : "
.text:00401460 66+ mov [esp+19h], cx
.text:00401465 8D+ lea edi, [esp+2Ch]
.text:00401469 88+ mov [esp+1Bh], cl]]>
Đưa con trỏtới dòng .text:00401440, nhấn phím P đểkhai báo một
hàm mới bắt đầu từđịa chỉđó. Tại sao chúng ta biết? Vì ngay trên đó
ta thấy align 10h chia cách các hàm.
Cũng đặt con trỏtại đây, nhấn N, nhập vào check vì ta sẽthấy
trong vài đoạn sau đây chính là hàm kiểm tra chuỗi.
Lần theo mã đến một đoạn có rất nhiều lệnh mov sau.
.text:00401482 B8+ mov eax, 58h
.text:00401487 89+ mov [esp+88h+var_67], edx
.text:0040148B 89+ mov [esp+88h+var_30], eax
.text:0040148F 89+ mov [esp+88h+var_20], eax
.text:00401493 89+ mov [esp+88h+var_63], edx
.text:00401497 8D+ lea eax, [esp+88h+String]
.text:0040149B 6A+ push 3Ch ; nMaxCount
.text:0040149D 66+ mov [esp+8Ch+var_5F], dx
.text:004014A2 A4 movsb
.text:004014A3 50 push eax ; lpString
.text:004014A4 68+ push 3E8h ; nIDDlgItem
.text:004014A9 8B+ mov ecx, ebx
.text:004014AB 88+ mov [esp+94h+var_5D], dl
.text:004014AF C7+ mov [esp+94h+var_3C], 31h
.text:004014B7 C7+ mov [esp+94h+var_38], 47h
.text:004014BF C7+ mov [esp+94h+var_34], 23h
.text:004014C7 C7+ mov [esp+94h+var_2C], 38h
.text:004014CF C7+ mov [esp+94h+var_28], 74h
.text:004014D7 C7+ mov [esp+94h+var_24], 27h
.text:004014DF C7+ mov [esp+94h+var_1C], 39h
.text:004014E7 C7+ mov [esp+94h+var_18], 73h
.text:004014EF C7+ mov [esp+94h+var_14], 76h
.text:004014FA C7+ mov [esp+94h+var_10], 43h
.text:00401505 C7+ mov [esp+94h+var_C], 60h
.text:00401510 C7+ mov [esp+94h+var_8], 2Fh
.text:0040151B C7+ mov [esp+94h+var_4], 79h
.text:00401526 33+ xor esi, esi
.text:00401528 E8+ call sub_40E21E]]>
Đầu tiên, eax được gán giá trị0x58. Sauđó var_30 và var_20 được
gán giá trịcủa eax. Ởcác dòng mov sau, ta thấy một loạt liên tiếp từ
var_3C tới var_4 được gán các giá trị"cứng".
Chú ý kỹta thấy rằng từvar_3C tới var_4 là một mảng 15 biến kiểu
int tất cả. Nhấn kép chuột vào var_3C, sau đó nhấn chuột vào chuỗi
var_3C, nhấn * và nhập 15 vào ô Array size. Ta có thểđổi tên biến
var_3C thành target bằng cách nhấn chuột vào chuỗi var_3C, nhấn N
và nhập vào target.
Nhấn kép vào sub_40E21E tại dòng lệnh call ởcuối ta thấy rằng
hàm này thực chất là hàm gói GetDlgItemTextA. Đổi tên sub_40E21E
thành getInputText. Vì đây là hàm nhận dữliệu nhập, ta cũng nên đổi
tên biến String thành input.
Đoạn mã C tương tựnhưsau.
int target[] = {0x31, 0x47, 0x23, 0x58, 0x38, 0x74, \
0x27, 0x58, 0x39, 0x73, 0x76, 0x43, 0x60, 0x2F, 0x79};
char input[60];
getInputText(input, 0x3C); // đây chỉlà hàm giảđểnhận dữliệu nhập
]]>
Chúng ta sẽgặp đoạn mã kếtiếp thường xuyên nên bài viết sẽchỉ
nói qua một lần.Đoạn mã kếbao gồm bốn lệnh lea edi, X, tới repe
scasb, rồi not ecx, và dec ecx. Tất cảbốn lệnh này chỉthực hiện một
việc là tính chiều dài chuỗi được chỉtới bởi edi.
.text:0040152D 8D+ lea edi, [esp+88h+input]
.text:00401531 83+ or ecx, 0FFFFFFFFh
.text:00401534 33+ xor eax, eax
.text:00401536 33+ xor edx, edx
.text:00401538 F2+ repne scasb
.text:0040153A F7+ not ecx
.text:0040153C 49 dec ecx]]>
Chúng ta bắt đầu tìm hiểuđoạn mã kếtiếp có mục đích gì.
.text:0040152D 8D+ lea edi, [esp+88h+input]
.text:00401531 83+ or ecx, 0FFFFFFFFh
.text:00401534 33+ xor eax, eax
.text:00401536 33+ xor edx, edx
.text:00401538 F2+ repne scasb
.text:0040153A F7+ not ecx
.text:0040153C 49 dec ecx
.text:0040153D 74+ jz short loc_40155A
.text:0040153F
.text:0040153F loc_40153F: ; CODE XREF:
check+118]]>↓<![CDATA[j
.text:0040153F 8A+ mov cl, [esp+edx+88h+input]
.text:00401543 8D+ lea edi, [esp+88h+input]
.text:00401547 88+ mov byte ptr
[esp+edx+88h+var_6C], cl
.text:0040154B 83+ or ecx, 0FFFFFFFFh
.text:0040154E 33+ xor eax, eax
.text:00401550 42 inc edx
.text:00401551 F2+ repne scasb
.text:00401553 F7+ not ecx
.text:00401555 49 dec ecx
.text:00401556 3B+ cmp edx, ecx
.text:00401558 72+ jb short loc_40153F]]>
Nhưđã nói, từdòng .text:0040152D tới .text:0040153C tìm chiều
dài của chuỗi input. Nếu chiều dài đó khác 0 thì sẽthực hiện phần kế
tiếp, còn không thì bỏqua. Kèm trong mã tìm chiều dài là việc khởi
tạo eax và edx thành 0. Phần mã từ.text:0040153F đến cuối thực hiện
việc chép tuần tựcác ký tựtừinput vào var_6C. Đổi tên var_6C thành
copiedInput. Ta có thểbiểu diễn đoạn mã hợp ngữnày bằng mã C như
sau.
char copiedInput[60];
int counter;
if (strlen(input) != 0)
{
for (counter = 0; counter < strlen(input); counter++)
{
copiedInput[counter] = input[counter];
}
}]]>
Chúng ta tiếp tục tìm hiểu phần kế.
.text:0040155A loc_40155A: ; CODE XREF:
check+FD]]>↑<![CDATA[j
.text:0040155A 8D+ lea edi, [esp+88h+input]
.text:0040155E 83+ or ecx, 0FFFFFFFFh
.text:00401561 33+ xor eax, eax
.text:00401563 33+ xor edx, edx
.text:00401565 F2+ repne scasb
.text:00401567 F7+ not ecx
.text:00401569 49 dec ecx
.text:0040156A 74+ jz short loc_401589
.text:0040156C
.text:0040156C loc_40156C: ; CODE XREF:
check+147]]>↓<![CDATA[j
.text:0040156C 8A+ mov cl, [esp+edx+88h+input]
.text:00401570 8D+ lea edi, [esp+88h+input]
.text:00401574 02+ add cl, dl
.text:00401576 33+ xor eax, eax
.text:00401578 88+ mov [esp+edx+88h+input], cl
.text:0040157C 83+ or ecx, 0FFFFFFFFh
.text:0040157F 42 inc edx
.text:00401580 F2+ repne scasb
.text:00401582 F7+ not ecx
.text:00401584 49 dec ecx
.text:00401585 3B+ cmp edx, ecx
.text:00401587 72+ jb short loc_40156C]]>
Nếu độdài chuỗi input khác 0 thì lần lượt sửa từng ký tựtrong
input với giá trịlà giá trịcũcộng thêm vịtrí của ký tựđó trong chuỗi.
Ta thấy trong đoạn tìmđộdài chuỗi ởtrên có khởi tạo edx thành 0 và
đoạn ởdưới có thêm một lệnh inc edx đểtăng edx. Đoạn mã C tương
tựnhưsau.
if (strlen(input) != 0)
{
for (counter = 0; counter < strlen(input); counter++)
{
input[counter] += counter;
}
}]]>
Phần kếtiếp có vẻhơi phức tạp hơn một chút.
.text:00401589 loc_401589: ; CODE XREF:
check+12A]]>↑<![CDATA[j
.text:00401589 8D+ lea edi, [esp+88h+input]
.text:0040158D 83+ or ecx, 0FFFFFFFFh
.text:00401590 33+ xor eax, eax
.text:00401592 33+ xor edx, edx
.text:00401594 F2+ repne scasb
.text:00401596 F7+ not ecx
.text:00401598 49 dec ecx
.text:00401599 49 dec ecx
.text:0040159A 74+ jz short loc_4015C5
.text:0040159C
.text:0040159C loc_40159C: ; CODE XREF:
check+183]]>↓<![CDATA[j
.text:0040159C 8A+ mov al, [esp+edx+88h+input]
.text:004015A0 8A+ mov cl, [esp+edx+0Dh]
.text:004015A4 3A+ cmp al, cl
.text:004015A6 7E+ jle short loc_4015B0
.text:004015A8 88+ mov [esp+edx+88h+input], cl
.text:004015AC 88+ mov [esp+edx+0Dh], al
.text:004015B0
.text:004015B0 loc_4015B0: ; CODE XREF:
check+166]]>↑<![CDATA[j
.text:004015B0 8D+ lea edi, [esp+88h+input]
.text:004015B4 83+ or ecx, 0FFFFFFFFh
.text:004015B7 33+ xor eax, eax
.text:004015B9 42 inc edx
.text:004015BA F2+ repne scasb
.text:004015BC F7+ not ecx
.text:004015BE 83+ add ecx, 0FFFFFFFEh
.text:004015C1 3B+ cmp edx, ecx
.text:004015C3 72+ jb short loc_40159C]]>
Chú ý kỹđoạn mã loc_40159C, IDA đã không giúp chúng ta biết
esp+edx+0Dh chỉtới giá trịnào. Nhưng IDA có thểcho chúng ta biết
esp+edx+88h+input thực chất là gì. Đưa con trỏvào vùng
esp+edx+88h+input, nhấn chuột phải và quan sát kỹta sẽthấy
esp+edx+0Ch. Có nghĩa là đoạn mã này sẽlần lượt so sánh hai ký tự
kếtiếp nhau trong input. Nếu ký tựđứng trước nhỏhơn ký tựđứng
sau thì hoán đổi vịtrí của chúng. Chú ý kỹlà vòng lặp chỉlặp tới chiều
dài của input trừmột. Chúng ta biết điều này vì tại .text:004015BE,
thay vì dec ecx ta lại có add ecx, 0FFFFFFFFh (hay ecx = ecx - 2, hay
ecx chứa chiều dài chuỗi input giảm một). Đoạn mã C tương tựnhư
sau.
if (strlen(input) != 0)
{
for (counter = 0; counter < strlen(input) - 1; counter++)
{
char a = tmp[counter];
char b = tmp[counter + 1];
if (a > b)
{
tmp[counter] = b;
tmp[counter + 1] = a;
}
}
}
Phần cuối cùng của hàm này khá đơn giản.
.text:004015C5 loc_4015C5: ; CODE XREF:
check+15A]]>↑<![CDATA[j
.text:004015C5 8D+ lea edi, [esp+88h+input]
.text:004015C9 83+ or ecx, 0FFFFFFFFh
.text:004015CC 33+ xor eax, eax
.text:004015CE F2+ repne scasb
.text:004015D0 F7+ not ecx
.text:004015D2 49 dec ecx
.text:004015D3 74+ jz short loc_40163E
.text:004015D5 8D+ lea edx, [esp+88h+target]
.text:004015D9
.text:004015D9 loc_4015D9: ; CODE XREF:
check+1B6]]>↓<![CDATA[j
.text:004015D9 0F+ movsx eax, [esp+esi+88h+input]
.text:004015DE 3B+ cmp eax, [edx]
.text:004015E0 75+ jnz short loc_4015F8
.text:004015E2 8D+ lea edi, [esp+88h+input]
.text:004015E6 83+ or ecx, 0FFFFFFFFh
.text:004015E9 33+ xor eax, eax
.text:004015EB 46 inc esi
.text:004015EC 83+ add edx, 4
.text:004015EF F2+ repne scasb
.text:004015F1 F7+ not ecx
.text:004015F3 49 dec ecx
.text:004015F4 3B+ cmp esi, ecx
.text:004015F6 72+ jb short loc_4015D9
.text:004015F8
.text:004015F8 loc_4015F8: ; CODE XREF:
check+1A0]]>↑<![CDATA[j
.text:004015F8 83+ cmp esi, 0Fh
.text:004015FB 75+ jnz short loc_40163E]]>
Đoạn mã này so sánh từng ký tựcủa chuỗi input với giá trịcó sẵn
trong target. Giá trịcủa esi là vịtrí của sựkhác nhau đầu tiên. Giá trị
của esi phải là 15 thì khóa nhập vào mới đúng. Mã C tương tựnhưsau.
if (strlen(input) != 0)
{
for (counter = 0; counter < strlen(input); counter++)
{
if (input[counter] != target[counter])
{
break;
}
}
}
if (counter == 0x0F)
{
// phần mã hiển thịhộp thoại thông báo copiedInput là khóa
đúng
// phần này không được xem xét trong bài viết
}
// PostQuitMessage]]>
Nhưvậy ta đã có đầy đủthông tin đểviết lại hàm kiểm tra khóa
nhập của crackme này. Việc tìm ra một khóa thỏa mã điều kiện của
hàm kiểm tra này cũng khá đơn giản. Bài viết sẽkhông đềcập tới vấn
đềtìm ra một khóa mà sẽtập trung vào việc tìm ra tất cảcác khóa
trong phần tới.
id="brute">
Với một hàm kiểm tra đơn giản nhưvậy ta có rất nhiều kết quảthỏa
mã điều kiện. Tuy nhiên, tác giảcrackme này nhất định đòi người thử
sức phải tìm ra khóa có ý nghĩa. Vì hàm kiểm tra quá đơn giản nênđể
tìm ra tất cảmọi khóa ta chỉcòn cách vét cạn các hoán vịcủa target.
Lưu ý rằng sinh ra tất cảhoán vịcủa một chuỗi 15 ký tựsẽsinh ra 15!
(15 giai thừa) chuối mới. Ai cũng nhận ra 15! là một con sốrất lớn nên
trong khi sinh ra hoán vị, chúng ta phải có cách đểloại bỏtất cảcác
hoán vịmà ta biết là sẽkhông thểnào dẫn tới đáp án đúng.
Trước khi bắt tay vào tìm kết quả, ta cần phải sửa hàm kiểm tra để
tốiưu tốc độchạy nhưsau.
static char target[] = "1G#X8t'X9svC`/y";
#define TARGET_LEN 15
int check(char *input, register int length)
{
char tmp[16];
register char a, b;
register int counter;
strncpy(tmp, input, length);
length--;
for (counter = 0; counter < length; counter++)
{
a = tmp[counter];
b = tmp[counter + 1];
if (a > b)
{
tmp[counter] = b;
tmp[counter + 1] = a;
}
if (tmp[counter] != target[counter])
{
break;
}
}
if (counter == length && tmp[counter] == target[counter])
{
counter++;
}
return counter;
}]]>
Hàm này kiểm tra chuỗi nhập vào với độdài length. Giá trịtrảvề
là vịtrí sai biệt đầu tiên. Vì việc kiểm tra khá tốn thời gian so với việc
sinh hoán vịnên hàm kiểm tra này đã được tối ưu dẫnđến việc mã
hàm hơi khó hiểu. Ta cần hàm này vì trong quá trình sinh hoán vịta sẽ
kiểm tra ngay lập tức và loại bỏcác hoán vịcon không cần thiết.
Hàm sinh hoán vịkhá đơn giản. Nó là một hàm đệquy nhưsau.
static int used[TARGET_LEN];
void printKey(char *dst)
{
int i = 0;
for (i = 0; i < TARGET_LEN; i++)
{
printf("%c", (char) (dst[i] - i));
}
printf("\n");
}
void permute(register char *dst, register index)
{
register int i;
if (index >= TARGET_LEN)
{
if (check(dst, TARGET_LEN) >= TARGET_LEN)
{
printKey(dst);
}
return;
}
for (i = 0; i < TARGET_LEN; i++)
{
if (!used[i])
{
dst[index] = target[i];
used[i] = 1;
if (check(dst, index + 1) >= index) // cắt nhánh
{
permute(dst, index + 1);
}
dst[index] = '\x00';
used[i] = 0;
}
}
}]]>
Điều đáng lưu ý ởhàm permute là dòng if đểcắt nhánh. Nếu ký tự
mới thêm vào dst không qua được hàm kiểm tra nhưcác ký tựđã có
trong dst thì bỏtoàn bộnhánh đó.
Đểxửdụng hàm permute ta sẽkhai báo một mảng ký tựvà khởi
tạo mảng used nhưsau.
void main(void)
{
char tmp[16];
memset(used, 0, sizeof(used));
memset(tmp, 0, sizeof(tmp));
permute(tmp, 0);
}]]>
Đoạn mã trên sẽin lên màn hình tất cảcác giá trịthỏa hàm kiểm
trađã nói tớiởdisasm. Đểbiên dịch các đoạn mã này, ngườiđọc cần
thêm stdio.h, và string.h.
Lời kết:
Tác giảhy vọng bài viết đã hướng dẫn người đọc cách đọc và hiểu
mã hợp ngữcùng IDA đểtừđó cấu trúc lại crackme đầu tiên của Merc
và phương pháp vét cạn cắt nhánh tìm các khóa thỏa mãn crackme
này.