Archive for August, 2014


First, thanks to johannesbader for his nice write-up.
Original link: http://www.johannesbader.ch/2014/07/crackmes-de-san01sukes-somecrypto01/

Crackme: http://www.crackmes.de/users/san01suke/somecrypto01/

Tools: IDA Pro, OllyDBG

1. Thông tin sơ lược

Chạy thử crackme, thấy có hai text box cho phép nhập name và serial, ở đây không thấy có bất kỳ button nào dùng để kiểm tra tính hợp lệ của thông tin nhập vào như thường gặp:

a

Vậy có thể đoán khả năng crackme sẽ thực hiện kiểm tra bất cứ khi nào nội dung của các text box thay đổi hoặc định kỳ kiểm tra theo một khoảng thời gian nào đó. Thử nhập thông tin như trên, tuy nhiên không thấy có bất kỳ thông tin phản hồi nào về sự đúng sai. Có vẻ hơi khoai!!

2. Phân tích

Load crackme vào IDA, chờ IDA phân tích xong, chuyển qua màn hình Strings (Shift+F12):

ida

Quan sát thấy có chuỗi Success, một dấu hiệu tốt để lần theo, nhấn đúp chuột tại đó sẽ tới đoạn code sau:

ida1

Bấm chuột vào dòng Caption và nhấn X, màn hình xrefs xuất hiện cho thấy chỉ có duy nhất một vị trí code tham chiếu tới chuỗi Success:

ida2

Nhấn OK sẽ tới đoạn code sau:

ida3

Dòng code thứ 4 và 5 sẽ thực hiện kiểm tra xem AL có bằng 0 hay không? Nếu có, cờ ZF sẽ được bật và lệnh nhảy sẽ nhảy tới loc_4012CA. Quan sát một chút, ta đoán thanh ghi eax sẽ thay đổi bởi lệnh call tại loc_401000. Tại sao lại đoán như vậy, là vì thanh ghi eax thường là thanh ghi chứa giá trị trả về sau lời gọi hàm. Vì vậy, nhiệm vụ đặt ra là phải tìm sự kết hợp giữa name/serial để làm sao qua subroutines loc_401000 sẽ không trả về giá trị null.

Nhấn đúp chuột tại loc_401000 ta sẽ tới đoạn code bên trong lời gọi hàm. Quan sát một cách tổng quan trước khi tiến hành bước phân tích chi tiết:

.text:00401000 loc_401000:                             ; CODE XREF: DialogFunc+1CDp
.text:00401000         push    ebp
.text:00401001         mov     ebp, esp
.text:00401003         mov     al, [ecx]
.text:00401005         sub     esp, 20h
.text:00401008         push    esi
.text:00401009         xor     esi, esi
.text:0040100B         test    al, al
.text:0040100D         jz      loc_4010C6
.text:00401013         lea     edx, [ebp-20h]
.text:00401016         sub     edx, ecx
.text:00401018 loc_401018:                             ; CODE XREF: .text:00401032j
.text:00401018         cmp     al, 'a'
.text:0040101A         jl      loc_4010C6
.text:00401020         cmp     al, 'z'
.text:00401022         jg      loc_4010C6
.text:00401028         mov     [edx+ecx], al
.text:0040102B         mov     al, [ecx+1]
.text:0040102E         inc     ecx
.text:0040102F         inc     esi
.text:00401030         test    al, al
.text:00401032         jnz     short loc_401018
.text:00401034         cmp     esi, 26
.text:00401037         jnz     loc_4010C6
.text:0040103D         xor     eax, eax
.text:0040103F         nop
.text:00401040 loc_401040:                             ; CODE XREF: .text:0040104Fj
.text:00401040         mov     cl, byte_403010[eax]
.text:00401046         mov     byte_403140[eax], cl
.text:0040104C         inc     eax
.text:0040104D         test    cl, cl
.text:0040104F         jnz     short loc_401040
.text:00401051         xor     ecx, ecx
.text:00401053         cmp     byte_403140, cl
.text:00401059         jz      short loc_401088
.text:0040105B         jmp     short loc_401060
.text:0040105B ; ---------------------------------------------------------------------------
.text:0040105D         align 10h
.text:00401060 loc_401060:                             ; CODE XREF: .text:0040105Bj
.text:00401060                                         ; .text:00401086j
.text:00401060         mov     al, byte_403140[ecx]
.text:00401066         cmp     al, 'a'
.text:00401068         jl      short loc_40107E
.text:0040106A         cmp     al, 'z'
.text:0040106C         jg      short loc_40107E
.text:0040106E*loc_40106E:                             ; DATA XREF: start:loc_4012D5w
.text:0040106E*        push    cs
.text:0040106F         mov     esi, 5948AC0h
.text:00401074 loc_401074:                             ; CODE XREF: .text:loc_401074j
.text:00401074         jg      short near ptr loc_401074+1
.text:00401074 ; ---------------------------------------------------------------------------
.text:00401076         dw 0FFFFh
.text:00401078 ; ---------------------------------------------------------------------------
.text:00401078         mov     byte_403140[ecx], dl
.text:0040107E loc_40107E:                             ; CODE XREF: .text:00401068j
.text:0040107E                                         ; .text:0040106Cj
.text:0040107E         inc     ecx
.text:0040107F         cmp     byte_403140[ecx], 0
.text:00401086         jnz     short loc_401060
.text:00401088 loc_401088:                             ; CODE XREF: .text:00401059j
.text:00401088         or      eax, 0FFFFFFFFh
.text:0040108B         mov     edx, offset byte_403140
.text:00401090         test    ecx, ecx
.text:00401092         jz      short loc_4010AD
.text:00401094 loc_401094:                             ; CODE XREF: .text:004010ABj
.text:00401094         movzx   esi, byte ptr [edx]
.text:00401097         xor     esi, eax
.text:00401099         and     esi, 0FFh
.text:0040109F         shr     eax, 8
.text:004010A2         xor     eax, ds:dword_402058[esi*4]
.text:004010A9         inc     edx
.text:004010AA         dec     ecx
.text:004010AB         jnz     short loc_401094
.text:004010AD loc_4010AD:                             ; CODE XREF: .text:00401092j
.text:004010AD         not     eax
.text:004010AF         cmp     eax, 0F891B218h
.text:004010B4         jnz     short loc_4010C6
.text:004010B6         mov     eax, [ebp+8]
.text:004010B9         mov     dword ptr [eax], offset byte_403140
.text:004010BF         mov     al, 1
.text:004010C1         pop     esi
.text:004010C2         mov     esp, ebp
.text:004010C4         pop     ebp
.text:004010C5         retn
.text:004010C6 ; ---------------------------------------------------------------------------
.text:004010C6 loc_4010C6:                             ; CODE XREF: .text:0040100Dj
.text:004010C6                                         ; .text:0040101Aj
.text:004010C6                                         ; .text:00401022j
.text:004010C6                                         ; .text:00401037j
.text:004010C6                                         ; .text:004010B4j
.text:004010C6         xor     al, al
.text:004010C8         pop     esi
.text:004010C9         mov     esp, ebp
.text:004010CB         pop     ebp
.text:004010CC         retn

Như đã thấy, đoạn code không quá dài và khá rõ ràng. Tuy nhiên, nếu quan sát kĩ sẽ thấy một exception từ dòng 51 đến 53:

ida4

Ở đây, IDA không thể chuyển đổi hai bytes này sang code mà chỉ thể hiện nó như dữ liệu. Vậy có thể tạm đoán khả năng đây là đoạn code thuộc loại tự thay đổi khi debug (self-modifying). Ta sẽ phân tích đoạn này sau. Giờ quay trở lại phân tích từ đầu từng dòng code của hàm.

9 dòng code đầu tiên như sau:

.text:00401000 loc_401000:                             ; CODE XREF: DialogFunc+1CDp
.text:00401000         push    ebp
.text:00401001         mov     ebp, esp
.text:00401003         mov     al, [ecx]
.text:00401005         sub     esp, 20h
.text:00401008         push    esi
.text:00401009         xor     esi, esi
.text:0040100B         test    al, al
.text:0040100D         jz      loc_4010C6

Có thể thấy sau function prologue (lưu và cập nhật frame pointer, ebp trỏ vào đỉnh stack) ta thấy có một tham chiếu tới thanh ghi ecx. Ta thấy rõ ràng thanh ghi ecx không được thiết lập bên trong subroutine loc_401000, vậy nó phải là một tham số của hàm. Quay trở lại đoạn có lời gọi call loc_401000, ta thấy thanh ghi ecx sẽ nạp giá trị là địa chỉ tại esp+104h+var_80 . Địa chỉ này cũng đã được sử dụng là tham số lpString cho lời gọi hàm GetDlgItemTextA (tại dòng code thứ 5):

ida5

Căn cứ vào thông tin có được, ta đoán ecx có thể trỏ tới chuỗi name hoặc chuỗi serial. Để biết chính xác thông tin là gì ta hãy dùng OllyDbg. Mở OllyDBG và load crackme, sau đó đặt một breakpoint tại 0040128F, sau đó nhấn F9, trace và quan sát giá trị thanh ghi ecx:

Olly1

Kết luận ecx trỏ tới chuỗi serial. Quay trở lại với loc_401000, tại dòng thứ 5 ta thấy hàm thực hiện tạo một stack frame dành cho các biến local. Đoạn code mov al, [ecx] sẽ lấy kí tự đầu tiên của serial nạp vào thanh ghi al, sau đó sẽ kiểm tra xem kí tự đầu tiên này có phải là 0 hay không, ví dụ: nếu serial number là một chuỗi rỗng. Nếu chuỗi serial là rỗng, sẽ thực hiện lệnh nhảy tới loc_4010C6 và thanh ghi al sẽ được thiết lập là 0 (đồng nghĩa với việc ta sẽ không thành công). Giả sử, ta tạm coi chuỗi serial không phải là chuỗi rỗng, tiếp tục phân tích code tiếp theo:

ida6

Dòng đầu tiên sẽ thực hiện nạp địa chỉ của đỉnh stack vào thanh ghi edx, rồi sau đó trừ đi giá trị thanh ghi ecx. Tiếp theo đó sẽ là hai đoạn so sánh thanh ghi al với các giá trị 61h và 7Ah, tương ứng với chữ cái ‘a’ và ‘z’. Giá trị của al có được tại dòng code thứ 4 (mov al, [ecx]), và lúc này al đang chứa kí tự đầu tiên của chuỗi serial. Vậy có thể thấy hai đoạn so sánh là để chắc chắn rằng kí tự trong al là một trong số 26 chữ cái thường. Nếu không đúng sẽ nhảy tới đoạn code loc_4010C6. Sau hai lần kiểm tra, kí tự tại al sẽ được copy vào [edx+ecx] (dòng 17).

Tại các dòng 10 và 11, ta thấy [ebp-20h] chính là đỉnh của stack, do đó thanh ghi edx sẽ được nạp giá trị đỉnh của stack . Dòng 18 sẽ nạp kí tự tiếp theo của chuỗi serial vào thanh ghi al, và dòng 19 sẽ thực hiện việc trỏ ecx vào chính kí tự này. Dòng 20 tiến hành tăng giá trị esi lên một (trước đó esi đã được gán là 0 tại dòng 7). Dòng 21 thực hiện kiểm tra xem kí tự có phải là null-byte. Nếu không phải, sẽ nhảy về địa chỉ loc_401018 để thực hiện lần lặp tiếp theo tương tự như trên. Ta có thể tóm tắt đoạn code trên ở mã giả như sau:

char serial_copy[32] // in [ebp-20h]
esi = 0
DO
    c = serial[esi]
    IF NOT 'a' <= c <= 'z' THEN
        RETURN 0 // failure
    ENDIF
    serial_copy[esi] = c
    esi += 1
WHILE c != '\0'

Tóm lại đoạn code trên sẽ thực hiện copy toàn bộ serial vào [ebp-20h] (chính là đỉnh của stack), đồng thời đảm bảo serial chỉ chứa các kí tự chữ cái thường.
Đoạn code tiếp theo:

ida7

Dòng đầu tiên thực hiện so sánh esi với 1Ah = 26. Dựa vào quá trình phân tích ở bên trên, có thể hiểu esi sẽ chứa thông tin về độ dài của chuỗi Serial. Như vậy mục tiêu của đoạn code là kiểm tra xem serial có đủ 26 kí tự hay không? Nếu không đủ, sẽ nhảy tới đoạn code tại loc_4010C6. Ta có mã giả của đoạn trên như sau:

IF len(serial) != 26 THEN
        RETURN 0 // failure
ENDIF

Tiếp tục phân tích đoạn code sau:

ida8
Đoạn code này đơn giản thực hiện việc sao chép chuỗi được lưu tại byte_403010 vào byte_403140. Dòng 34 và 35 cũng kiểm tra xem kí tự đầu tiên trong byte_403140 có phải là null hay không, ví dụ., kiểm tra trường hợp chuỗi rỗng. Nếu đúng, sẽ nhảy tới đoạn code tại địa chỉ loc_401088. Đoạn kiểm tra này hời thừa vì byte_403140 chứa bản sao của chuỗi tại byte_403010, là một chuỗi cố định nên sẽ không có null byte, do vậy ta xem như lệnh nhảy sẽ không bao giờ thực hiện. Ta có đoạn mã giả của đoạn code trên như sau:

STRCPY(byte_403140, byte_403010) // copy string byte_403010 to byte_403140
IF byte_403140[0] == '\0' THEN
    GOTO loc_401088 \\ should never happen
ENDIF

Tiếp tục phân tích:

ida9

Để ý ở trên, thanh ghi ecx được gán bằng 0 tại dòng 33. Do vậy thanh ghi al sẽ chưa kí tự đầu tiên của byte_403140. Dòng code từ 42 tới 45 thực hiện kiểm tra nếu kí tự không phải là chữ cái thường thì sẽ nhảy tới loc_40107E. Ta có mã giả như sau:

IF NOT 'a' <= byte_403140[ecx] <= 'z'  THEN
    GOTO loc_40107E
ENDIF

Khi lệnh nhảy không thực hiện, ta sẽ tiếp tục với đoạn code khả hay như sau:

ida10

Như đã đề cập ở trước, đoạn code khá khó hiểu:

  • push cs : lệnh này nhìn vào chả hiểu ý nghĩa là gì.
  • Hằng số được gán cho thanh ghi esi : mov esi, 5948AC0h trông cũng chả có gì đặc biệt.
  • Lệnh nhảy jg short near ptr loc_401074+1 nhảy tới ví trí không có.
  • Phần định nghĩa dữ liệu dw 0FFFFh cũng gây khó hiểu.

Vậy khả năng đây là đoạn code sẽ được modified trong quá trình thực thi. Cách nhanh nhất để kiểm chứng là thiết lập một breakpoint tại .text:0040106C trong OllyDbg và kiểm tra xem đoạn code sẽ thay đổi như thế nào lúc runtime. Chú ý phải nhập một serial hợp lệ (26 chữ cái thường), nếu không sẽ không OllyDBG sẽ không dừng ở bp đã đặt:

Olly2

Điều này khẳng định rằng đoạn code đã thay đổi lúc runtime. Cùng tại vị trí đó lúc chưa tiến hành debug:

Olly3

Ta có thể thấy các bytes ban đầu 0E BE C0 đã được thay đổi thành 0F BE C0 lúc runtime. Vậy ta sẽ đặt một breakpoint “Memory, on write” tại 0040106E để xem đoạn code này sẽ thực hiện việc chỉnh sửa. Ta có được đoạn code sau:

Olly4

Như vậy, tác giả của crackme sử dụng một lệnh INC đơn giản để tăng 1 byte tại 0040106E. Đối với các phiên bản mới hơn của IDA Pro ta có thể thực hiện patch để có được đoạn code chuẩn:

ida11

ida12

Để ý tới vị trí bộ nhớ [ebp+eax-81h], như trên ta đã biết [ebp-20h] trỏ tới vùng copy của chuỗi serial mà ta nhập vào. Do đó, ta có thể viết lại thành serial_copy[eax – 61h]. Hơn nữa, 61h = 97, chiếu theo bảng mã thì là chữ “a”, vậy ta có đoạn mã giả sau:

// al = byte_403140[ecx]
dl = serial_copy[eax - 'a']

Với phân tích như trên thì DL sẽ chứa kí tự thứ n của chuỗi serial, trong đó n bằng 0 khi eax = ‘a’, 1 khi eax= ‘b’, …, 25 khi eax= ‘z’. Điều này cũng cho thấy tại sao chuỗi serial cần phải có 26 characters: nó phục vụ như là một key cho mật mã thay thế (substitution cipher).

Đoạn code tiếp theo cho thấy kết thúc của vòng lặp mã hóa. Đoạn code này đơn giản thực hiện việc tăng chỉ số ecx và lặp lại đoạn code bắt đầu từ loc_401060 nếu kí tự tiếp theo tại byte_403140 không phải là null byte:

ida13

Đây là toàn bộ mã giả của đoạn code liên quan đến quá trình giải mã:

FOR i = 0 TO LEN(byte_403140) DO
    ch = byte_403140[i]
    IF 'a' <= ch <= 'z'  THEN
         byte_403140[i] = serial_copy[ch - 'a']
    ENDIF
END FOR

Toàn bộ các kí tự tại byte_403140, nếu không phải là chữ cái thường sẽ được giữ nguyên, ngược lại nếu là chữ cái thường sẽ được thay thế bằng kí tự tại vị trí thứ n của chuỗi serial. Đoạn code tiếp theo khá phức tạp:

ida14

Đoạn code này thực hiện việc tính toán giá trị hash của chuỗi tại địa chỉ byte_403140 (plaintext message). Nếu giá trị hash tính được bằng 0F891B218h (tại đoạn dòng 85), ta sẽ nhận được thông báo thành công của crackme, ngược lại sẽ nhảy tới đoạn code gán al = 0x0 tại loc_4010C6. Ta chỉ có thể giả định rằng giá trị 0F891B218h được tạo ra khi quá trình giải mã cho ra đúng plaintext.

Đến đây tóm tắt lại toàn bộ thông tin có được:

  • Bước đầu tiên, ngay đầu loc_401000 đã có đoạn code kiểm tra xem serial có rỗng hay không bằng cách kiểm tra kí tự đầu tiên của chuỗi serial, nếu kí tự đầu tiên là null byte –> nhảy tới vị trí gán al = 0x0.
  • Đoạn code tiếp sẽ thực hiện copy toàn bộ chuỗi serial vào [ebp-20h] (chính là đỉnh của stack), đồng thời đảm bảo chuỗi serial chỉ chứa các kí tự chữ cái thường và có độ dài là 26.
  • Đoạn code tiếp theo tiến hành sao chép chuỗi hardcoded được lưu tại byte_403010 vào byte_403140.

Olly5 Olly6

  • Tiếp theo, toàn bộ các kí tự tại byte_403140, nếu không phải là chữ cái thường sẽ được giữ nguyên, ngược lại nếu là chữ cái thường sẽ được thay thế bằng kí tự tại vị trí thứ n của chuỗi serial. Một dạng mật mã thay thế (substitution cipher) nhằm mục đích giải mã chuỗi tại byte_403140 sang dạng clear text.
  • Đoạn code tiếp sẽ thực hiện việc tính toán giá trị hash của chuỗi tại địa chỉ byte_403140 (plaintext/clear message). Nếu giá trị hash tính được bằng 0F891B218h, ta sẽ nhận được thông báo thành công của crackme, ngược lại sẽ nhảy tới đoạn code gán al = 0x0 tại

3. Tìm Plaintext

Giờ ta sẽ tìm hiểu kĩ hơn về chuỗi hardcoded tại địa chỉ byte_403010. Chuỗi này có thông tin như sau (có thể quan sát trên hình minh họa):

Ix lzctusdzetgc, ex n-fsb (nvfnujuvujsx-fsb) jn e fenjl lsatsxrxu sw ncaaruzjl qrc ehdszjugan pgjlg trzwszan nvfnujuvujsx. Ix fhslq ljtgrzn, ugrc ezr uctjlehhc vnrm us sfnlvzr ugr zrheujsxngjt fruprrx ugr qrc exm ugr ljtgrzurbu.

Quan sát toàn bộ chuỗi trên có thể thấy các kí tự trong chuỗi xuất phát từ một mật mã thay thế đơn giản (simple substitution cipher). Giờ nếu ta nhập chính xác key vào textbox serial thì sau quá trình tính toán ở trên sẽ giải mã ra một chuỗi có nghĩa (được coi là bản rõ) và như vậy ta có thể nhận được thông báo thành công. Việc phá vỡ substitution ciphers khá dễ dàng. Ở đây tác giả của bài viết sử dụng script break_simplesub được viết bằng ngôn ngữ Python đăng tại trang web Practical Cryptography. Đoạn mã sử dụng một giải thuật tối ưu hóa ngẫu nhiên và kết quả đầu ra thay đổi mỗi khi ta thực thi script. Dưới đây là kết qua mà tôi đã thử chạy:

kali1

Như thấy trên hình, kết quả chuỗi plaintext có vẻ hơi khó đọc, đó là vì tác giả của script đã không thực hiện xử lý các kí tự đặc biệt như dấu cách. Tuy nhiên, ta vẫn hoàn toàn có thể nhận dạng được ý nghĩa của đoạn plaintext. Khóa (key) để mã hóa thông điệp (message) vậy là EFLMRWDGJYQHAXSTOZNUVIPBCK. Để giải mã nó, ta phải nhập cần nhập chuỗi serial là đảo ngược của khóa (key). Đoạn Python script dưới đây sẽ sinh ra khóa giải mã (decryption key), và đồng thời cũng hiện thị kết quả của:

import string

crypt = """Ix lzctusdzetgc, ex n-fsb (nvfnujuvujsx-fsb) jn e fenjl lsatsxrxu sw
ncaaruzjl qrc ehdszjugan pgjlg trzwszan nvfnujuvujsx. Ix fhslq ljtgrzn, ugrc
ezr uctjlehhc vnrm us sfnlvzr ugr zrheujsxngjt fruprrx ugr qrc exm ugr
ljtgrzurbu.""".replace('\n', '')

key = 'EFLMRWDGJIQHAXSTOZNUVYPBCK'.lower()
mapping = {}
for k, c in zip(key, string.lowercase):
    mapping[k] = c

msg = ""
for c in crypt:
    msg += mapping.get(c, c)

print("the key is: {}".format(''.join([mapping[x] for x in string.lowercase])))
print("the plaintext is: {}".format(msg))

Kết quả sau khi thực hiện script trên:

kali2

Chuỗi crypt sau khi được giải mã sẽ như sau:

In cryptography, an s-box (substitution-box) is a basic component of symmetric key algorithms which performs substitution. In block ciphers, they are typically used to obscure the relationship between the key and the ciphertext.

Vậy chuỗi serial tìm được là mxygabhljizcdsqwkeoptufnvr, run crackme và nhập serial ta sẽ nhận được thông báo sau:

done

Done!!!!

Regards

m4n0w4r