REVERSING WITH IDA FROM SCRATCH (P18)

m4n0w4r
tradahacking
Published in
16 min readJul 19, 2019

Ở phần trước, tôi đã thực hiện unpack file thành công và file sau khi unpack có thể thực thi bình thường. Trong phần này, chúng ta sẽ tiến hành reverse nó để tìm hiểu cách thức hoạt động cũng như xem xét có thể viết keygen đơn giản bằng Python hay không.

Bạn nên nhớ rằng nếu chỉ để phân tích tĩnh chương trình thì ta không cần phải thực hiện đầy đủ các bước unpack như phần trước. Chúng ta chỉ cần tới được OEP và tạo một bản snapshot ( Take Memory Snapshot), sau đó chép file idb sang chỗ khác và mở nó. Bằng cách này ta có thể tiếp tục quá trình phân tích tĩnh. Tuy nhiên, việc unpack file hoàn chỉnh sẽ hỗ trợ chúng ta rất nhiều, cho phép ta có thể debug được chương trình dễ dàng hơn.

OK, mở IDA và load file đã unpacked vào. Sau khi IDA phân tích xong, chuyển tới cửa sổ Strings để tìm kiếm các chuỗi:

Chúng ta đã biết được đầu tiên chương trình thực hiện in ra chuỗi yêu cầu người dùng nhập vào một tên bất kỳ:

Vì vậy, tại màn hình Strings ở trên, ta nhấp đúp vào chuỗi đó sẽ chuyển qua màn hình IDA Disassembly:

Tiếp theo nhấn “ x “ để tìm kiếm các đoạn code sử dụng tới chuỗi này. Kết quả có được:

Ta đi tới địa chỉ trên:

Từ đây, ta sẽ bắt đầu quá trình phân tích tĩnh.

Đầu tiên, các bạn sẽ thấy đoạn code prologue khi bắt đầu của một hàm, thanh ghi EBPbase frame của hàm trước đó sẽ được lưu vào ngăn xếp bằng câu lệnh PUSH EBP, sau đó hàm sử dụng lệnh MOV EBP, ESP để thiết lập cho thanh ghi EBP trở thành base frame cho hàm hiện tại đang phân tích ( thanh ghi EBP kể từ đây sẽ được sử dụng để tham chiếu tới các biến cục bộ và tham số truyền vào cho hàm).

Câu lệnh tiếp theo SUB ESP, 94h làm nhiệm vụ dành riêng ra một khoảng không gian là 0x94 bytes cho các biến cục bộ và các buffers, kể từ giá trị base của thanh ghi EBP.

Bằng cách nhấp đúp vào bất kỳ biến hoặc tham số nào của hàm, IDA sẽ đưa ta tới cửa sổ biểu diễn thông tin về Stack của hàm đó:

Theo quan sát tại Stack của hàm, ta thấy rằng đây là một hàm không nhận bất kì tham số nào, bởi vì tham số thường sẽ được truyền vào thông qua các lệnh PUSH trước khi thực hiện lời gọi hàm, và các tham số này sẽ phải nằm bên dưới địa chỉ trở về ( r). Trong trường hợp hàm mà ta đang phân tích, bên dưới r không có thông tin gì và đó là lý do tại sao ta biết được đây là hàm không nhận tham số truyền vào.

Đây cũng chính là hàm main() của chương trình và thường hàm main() sẽ có các tham số là: env, argv argc, vv. Nhưng vì các tham số này không được sử dụng bên trong hàm nên IDA sẽ không quan tâm đến các tham số này. Nhấn x tại đầu hàm để tìm xrefs:

Nhấn OK, ta sẽ tới đây:

Với kết quả như trên hình, tôi sẽ đổi tên hàm thành main(). Sau khi đổi tên như vậy, IDA sẽ tự động thêm ba tham số vào cho hàm như hình dưới đây:

Nếu chúng ta nhấn x tại bất kỳ một trong ba tham số này, ta sẽ nhận được thông báo như bên dưới. Đó là bởi vì trong hàm main() không sử dụng tới chúng:

Vì các tham số này không được sử dụng nên chúng ta sẽ không cần quan tâm đến chúng nữa. Ta quay trở lại với cửa sổ Stack của hàm để phân tích tiếp:

Lúc này tại cửa sổ Stack, ta thấy các tham số đã được bổ sung thêm và nằm bên dưới của địa chỉ trở về. Phía trên địa chỉ trở về là “ s”, hàm ý rằng đó chính là giá trị EBP của hàm trước khi gọi hàm main() được lưu vào Stack. Như đã đề cập ở trên, giá trị EBP này được lưu lại thông qua lệnh PUSH EBP, bên trên “ s” sẽ là không gian dành cho các biến cục bộ được khai báo trong main, thông thường các biến này có dấu hiệu nhận biết là var_4, var_x v..v… Biến var_4 này được sử dụng để bảo vệ ngăn xếp khỏi lỗi tràn bộ đệm ( buffer overflow). Ta chọn biến này và nhấn x sẽ có kết quả như sau:

Ta thấy có hai chỗ sử dụng tới biến var_4. Một là tại chỗ bắt đầu của hàm khi hàm lấy giá trị của security cookie vào thanh ghi eax, rồi gán lại giá trị đó cho biến var_4:

Cookie security là một giá trị ngẫu nhiên, được đem XOR với thanh ghi EBP, kết quả được bao nhiêu sẽ lưu lại vào biến var_4 khi hàm bắt đầu. Ta phân tích đoạn code thứ hai sử dụng tới biến var_4:

Tại đoạn code trên, ta thấy nó thực hiện lấy ra giá trị đã lưu tại biến var_4, sau đó XOR lại với EBP để phục hồi giá trị ban đầu trong ECX và bên trong lệnh CALL bên dưới sẽ thực hiện kiểm tra giá trị này:

Nếu như mọi thứ đều hợp lệ, hàm sẽ return bình thường, nhưng nếu thanh ghi không giữ giá trị ban đầu của _security_cookie, nó sẽ rẽ nhánh sang lệnhJMP để exit và không cho phép ta thực hiện lệnhRET của hàm.

Chúng ta sẽ thấy rằng việc thực hiện exit nó chỉ có thể xảy ra khi có Overflow làm ghi đè lên giá trị của biến var_4 bên trong hàm. Do đó, chúng ta có thể đổi tên biến var_4 thành CANARY hoặc SECURITY COOKIE và đổi tên hàm thành Check_Canary():

Sau khi đổi tên như trên, ta thấy code trông cải thiện hơn một chút:

Tiếp theo, chúng ta thấy 3 biến mà chưa rõ mục đích sử dụng, hai biến được khởi gán bằng 0, và một biến IDA nhận diện được tên là Size, được khởi gán bằng 8. Quan sát các tham chiếu đến biến var_7d, chúng ta thấy nó được sử dụng ở đây:

Biến này sẽ nhận giá trị được lưu tại thanh ghi AL ( giá trị của được gán thông qua lệnh ở bên trên) và sau đó lại gán lại vào thanh ghi EDX. Tiếp theo chương trình kiểm tra xem có bằng 0 hay không để quyết định rẽ nhánh code theo hướng “” hoặc “”. Do đó, đây là một biến kiểu byte. Ta sẽ đổi tên biến này thành SUCCESS_FLAG. Tại cửa sổ Stack của hàm, ta thấy rằng IDA đã nhận diện được đây là biến có kích thước một byte.

Nhấn N để đổi lại tên biến. Sau đó, tôi đổi màu lại các khối code trong IDA như trên hình để dễ dàng hơn trong việc nhận diện và phân tích bằng IDA:

Rõ ràng là nếu tôi chỉ cần patch tại lệnh nhảy JZ thì tôi sẽ đạt được mục đích của mình, nhưng chúng ta sẽ không làm như thế. Chúng ta cần phân tích sâu hơn để đạt được mục tiêu đã đề ra. Tiếp tục phân tích biến tiếp theo là var_90:

Ta thấy rằng, ban đầu biến này được khởi gán bằng 0 ở đầu hàm, sau đó nó được sử dụng tại đoạn code như trên hình. Đoạn code này nằm trong một vòng lặp. Phân tích sâu hơn ta thấy nó sẽ đọc lần lượt từng byte một từ biến Buf tại 0x231109 vào EDX, sau đó cộng với biến var_90 ( ban đầu là 0) và lưu lại vào EDX, cuối cùng lại lưu lại vào biến var_90. Như vậy, ta nhận thấy thanh ghi EDX luôn là tổng của tất cả các bytes, do vậy ta có thể đổi tên biến này thành SUMMARY:

Cùng với quá trình phân tích trên, ta cũng có thể nhận ra biến var_84 chính là bộ đếm của vòng lặp, sau mỗi lần lặp biến này sẽ được tăng thêm 1, và thoát khỏi vòng lặp nếu như giá trị của biến này lớn hơn hoặc bằng 4. Đoạn code thực hiện tăng biến đếm này như hình dưới và tôi sẽ đổi tên nó thành COUNTER:

Biến COUNTER này cũng được sử dụng tại 0x231109 như là một index để đọc các bytes từ biến Buf:

Tiếp theo chúng ta sẽ nghiên cứu biến Buf để xem nó sẽ chứa nội dung gì:

Tại đoạn code trên ta có thể thấy chương trình sử dụng hàm gets_s() để nhận thông tin mà người dùng nhập vào từ bàn phím. Qua đó, ta biết được biến Buf sẽ là nơi chứa chuỗi tên của người dùng nhập vào và chuỗi này có kích thước tối đa là 8 byte ( được gán bằng 8 ở đầu hàm).

Trước đó, ta thấy một hàm tại địa chỉ 0x2310A0 bên dưới chuỗi yêu cầu nhập tên người dùng, ta có thể khẳng định luôn đây là hàm printf. Do đó, ta đổi tên cho hàm này:

Chuyển qua cửa số Stack, có thể thấy độ lớn của biến Buf này bằng cách nhấn chuột phải và chọn Array. Kết quả, biến Buf sẽ có kích thước là 120 bytes :

Hoàn toàn khớp với khai báo của nó trong mã nguồn của thầy Ricardo:

Sau khi chuyển đổi như trên, ta thấy thông tin về tham số và các biến tại cửa sổ Stack đã trở nên rõ ràng hơn rất nhiều. Tiếp theo, sau khi nhận thông tin do người dùng nhập vào và lưu vào trong biến Buf, chương trình sẽ sử dụng hàm strlen() để lấy ra chiều dài của chuỗi đã nhập vào:

Chiều dài chuỗi thu được qua hàm strlen() sẽ được lưu vào biến var_88, do vậy ta đổi tên biến này thành string_length:

Nếu biến này nhỏ hơn 4, tức là chiều dài của chuỗi nhập vào nhỏ hơn 4 thì chương trình sẽ gọi hàm exit() để thoát luôn. Tiếp theo là vòng lặp ( mà ta đã phân tích ở trên) thực hiện cộng 4 byte đầu tiên của chuỗi mà chúng ta nhập vào, vì vậy ta sẽ nhóm các khối lệnh này lại để dễ nhìn hơn. Ta nhóm bằng cách chọn từng khối và nhấn Ctrl. Đặt tên cho khối đã nhóm. Để quay trở về trạng thái cũ chỉ việc nhấn chuột phải tại đó và chọn Ungroup.

Tiếp theo, ta thấy rằng chương trình lại sử dụng lại biến Buf để nhận mật khẩu mà người dùng nhập vào. Có thể sử dụng lại biến Buf này là vì chương trình đã tính toán xong.

Cũng tương tự như trên, chương trình sử dụng hàm strlen() để tính toán độ dài của mật khẩu và nếu nhỏ hơn 4 thì cũng sẽ exit() luôn:

Tôi đổi màu các khối như trên hình để dễ nhìn hơn. Như vậy, nếu chiều dài mật khẩu là 4 hoặc lớn hơn, chúng ta sẽ rẽ nhánh theo khối màu xanh lam như trên hình. Tại khối lệnh màu xanh lam này sẽ lấy mật khẩu và chuyển đổi nó sang dạng thập lục phân bằng hàm atoi(). Tại thanh Python của IDA hành động trên sẽ tương đương với hàm hex(). Ví dụ, tôi thực hiện như sau:

Sau khi chuyển đổi bằng hàm atoi() thì chương trình lưu kết quả chuyển đổi vào biến var_8C, do đó tôi đổi tên biến này như hình:

Sau đó, ta thấy rằng mật khẩu ở dạng Hexa này sẽ được đem XOR với một giá trị mặc định của chương trình là 0x1234. Kết quả sau khi thực hiện lệnh XOR sẽ được lưu lại vào cùng một biến. Tiếp tục quá trình phân tích, ta thấy biến lưu tổng 4 byte đầu tiên của chuỗi tên người dùng và giá trị hex đã tính toán trước đó được truyền cho một hàm bên dưới tại 0x2311A4. Kết quả trả về của hàm này sẽ lưu vào thanh ghi AL để từ đó đưa ra quyết định nhảy tới “ good” hay “ bad”. Do đó, ta sẽ đổi tên hàm thành CHECK_EXIT():

Như vậy, có thể thấy hàm CHECK_EXIT() sẽ nhận hai tham số truyền vào thông qua hai lệnh PUSH. Vì vậy, chúng ta sẽ đổi tên cả hai tham số trong hàm này tương ứng với các biến ta đã phân tích như sau:

Sau khi thay tên cho các tham số để dễ hiểu hơn, ta nhấn chuột phải tại hàm để đặt lại kiểu cho hàm như sau:

Làm như vậy hàm sẽ được khai báo lại như hình:

Quay trở lại nơi gọi hàm sẽ thấy IDA tự động thêm các chú thích cho các tham số tương ứng:

Có thể thấy IDA đã thực hiện công việc rất tuyệt vời, giờ đi vào phân tích kĩ hơn nhiệm vụ của hàm CHECK_EXIT():

Quan sát code của hàm, trước khi làm phép so sánh thì hàm này thực hiện lệnh SHL EAX, 1, tương đương với việc đem giá trị tại EAX nhân 2. Vì vậy, nếu hai giá trị tại lệnh so sánh là bằng nhau, ta sẽ đi đến khối màu xanh lam, nơi code của hàm thiết lập cho thanh ghi AL bằng 1 ( mà thanh ghi này sẽ được gán cho biến SUCCESS_FLAG để quyết định ta sẽ nhận thông báo “good” hay là “bad”).

Như vậy, tóm tắt lại toàn bộ quá trình đã phân tích ở trên:

  • Chương trình sẽ lấy 4 byte đầu tiên từ tên của người dùng nhập vào và thực hiện phép cộng dồn.
  • Tiếp theo sẽ nhận Password mà người dùng nhập vào, chuyển đổi sang dạng Hexa và đem thực hiện XOR với giá trị mặc định 0x1234. Cuối cùng sẽ đem kết quả tính toán này nhân với 2.

Chúng ta sẽ xây dựng một công thức tính toán với giả định rằng ta có tên của người dùng nhập vào bởi vì keygen sẽ dựa vào thông tin này để tính toán. Với tên của người dùng bất kỳ thì keygen sẽ sinh ra một mật mật khẩu tương ứng. Ta có như sau:

  • X = PASSWORD đã được chuyển đổi sang HEXA
  • (X ^ 0x1234) * 2 = SUMMARY
  • X ^ 0x1234 = (SUMMARY / 2)
  • X = (SUMMARY/2) ^ 0x1234

Ta đã biết cách hoạt động của lệnh XOR ở các phần trước rồi, tôi xin nhắc lại như sau: A ^ B = C -> A = B ^ C;

Do đó để tìm X thì công thức sẽ là: X = (SUMMARY/2) ^ 0x1234

Tôi viết thử một script bằng python như bên dưới, trong script này tôi cố định tên của người dùng là “ manowar”, có chiều dài nhỏ hơn 8 bytes, vậy tổng của chuỗi này sẽ như sau:

Nhớ lại code của chương trình, ta không cộng toàn bộ chuỗi mà chỉ tính tổng có bốn kí tự đầu mà thôi. Do đó, tôi giới hạn lại như sau:

Với đoạn code trên, ta có thể tạo một keygen cho bất kỳ tên người dùng nào, việc lấy thông tin người dùng nhập vào sẽ thông qua hàm raw_input():

Kết quả cho chuỗi “ manowar “ vẫn giống như trên nhưng thay vào đó ta có thể tính toán cho bất kỳ tên người dùng nào. Ví dụ:

Chúng ta có biểu thức tính toán mật khẩu như sau:

X = (SUMATORIA/2) ^ 0x1234

Do vậy, ta viết lại biểu thức này bằng python như bên dưới đây:

Ta thử nhập tên và password tìm được ở trên, kết quả như hình minh họa dưới đây:

Chúng ta đã có keygen hoàn chỉnh, ở đây ta không cần phải thực hiện chuyển đổi mật khẩu từ dạng hexa sang thập phân bởi vì Python mặc định khi xuất ra màn hình là ở dạng thập phân rồi.

Kiểm tra với một chuỗi name dài hơn như trên hình, ta thấy rằng nó chỉ cộng 4 ký tự đầu tiên của tên người dùng, do đó kết quả tính toán ra password sẽ là như nhau. Bài tập này sẽ bị crash khi ta nhập vào chuỗi name có 8 kí tự bởi vì 8 kí này sẽ bao gồm cả kí tự kết thúc chuỗi. Chuỗi mà có 7 kí tự thì sẽ chạy bình thường. Chỉ có một vấn đề khi nếu tổng 4 kí tự cho ra kết quả là một số lẻ:

Sẽ không có giải pháp vì mật khẩu cuối cùng sẽ được nhân với 2 và được kết quả sẽ không bao giờ là số lẻ. Do vậy, ta cần phải bổ sung thêm đoạn kiểm tra sau:

Trong đoạn code trên, chúng ta sẽ kiểm tra phần dư khi thực hiện phép chia cho 2. Nếu kết quả bằng 0 thì là hợp lệ, còn khác 0 thì sẽ hiển thị thông báo để thử một chuỗi tên khác:

Tôi nghĩ rằng keygen hoạt động như vậy là ổn rồi, do đó tôi xin được kết thúc bài viết này tại đây.

Hẹn gặp lại các bạn ở phần 19!!!

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Ủng hộ tác giả

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

--

--