Reverse Engineering - Đồ án hướng ngành A "Hụt" của tôi
Sinh viên thực hiện: Nguyễn Đăng Khương
I would love to do this but the lecture who
Đồ Án Hướng ngành A
Sinh viên thực hiện: Nguyễn Đăng Khương
Đồ án hướng ngành A - Reverse Engineering được tôi nghiên cứu và biên soạn dựa trên những kiến thức có liên quan đến Dịch ngược Đồng thời, căn cứ vào nền tảng kế thừa những thành tựu của các chuyên gia đi trước trong lĩnh vực tìm hiểu (đào sâu) về Dịch ngược, cũng như áp dụng các thành tựu mà ngành CNTT đạt được vào việc phân tích, hệ thống hóa những tri thức khoa học thuộc về hướng ngành này.
Bên cạnh đó, Đồ án này được cấu trúc theo tất cả 9 phần, tuân thủ theo nguyên tắc trật tự của một bài Đồ án hoàn chỉnh. Ngoài những phần có tính phân tích kỹ lưỡng các khái niệm về dịch ngược được biên soạn và tham khảo dựa theo rất nhiều nguồn, và kiến thức cá nhân, thì Đồ án còn bổ sung và cung cấp những dẫn chứng, ví dụ cụ thể minh họa cho các lý thuyết được đề cập.
Phần
Tiêu Đề
1
Dịch Ngược - Reverse Engineering
2
x86, Assembly, Stack, Call Conventions
3
PE Format
4
Disassambler - IDA
5
Debugger - OllyDbg
6
Decompiler - Hex Ray
7
Những Kỹ Thuật Dịch Ngược
8
Những kỹ Thuật Chống Dịch Ngược
9
Refs
Đây toàn bộ đều là công sức miệt mài, nghiêm túc nghiên cứu của tôi trong suốt thời gian vừa qua. Đồ án Hướng ngành A tuy đã được cố gắng viết hoàn chỉnh nhưng chắc chắn không thể tránh khỏi các sai sót trong quá trình soạn thảo. Kính mong quý Thầy (Cô) trong quá trình hướng dẫn, phê duyệt sẽ nhiệt tình, công tâm góp ý để sản phẩm khoa học của tôi được hoàn thiện hơn.
Tôi xin chân thành cảm ơn lời đóng góp và sự đón nhận từ quý Thầy (Cô) và những người quan tâm đến Đồ án này!
Dịch Ngược - Reverse Engineering
Đây là quá trình mà phân tích, mổ xẻ một sự vật hiện tượng (cụ thể ở đây là phần mềm) để hiểu rõ cách nó được thiết kế, cách thức hoạt động của nó, làm sao nó đạt được những trạng thái như vậy và mục đích của nó là gì?
Trong trường hợp của chúng ta thì chúng ta sẽ RE phần mềm cho nên là chúng ta cần phải hiểu rõ luồng logic và instruction của Assembly, điều này nói thì dễ hơn làm, tại vì cơ bản RE là một quá trình đòi hỏi nhiều thời gian và công sức.
Mục tiêu trong RE phần mềm là hiểu cách nó hoạt động và làm thế nào nó hoạt động như vậy.
x86, Assembly, Stack, Call Conventions
Theo những kiến thức tôi đọc được từ sách và các tài liệu tham khảo đáng tin cậy, thì trước khi bắt đầu RE, chúng ta cần củng cố các kiến thức nền có liên quan đến các khái niệm sau đây
x86
Assembly
Stack
Call Conventions
x86, Assembly - Tổng quan
Để muốn có thể RE thì trước tiêng ta phải biết x86 và Assembly, Vậy tại sao chúng ta cần phải biết những thứ này? Liệu những khái niệm này có liên quan mật thiết đến RE hay không?
Nếu bạn hỏi, tất cả chương trình của bạn viết ra đều phải được dịch ra ngôn ngữ Assembly. Nói đúng hơn thì Assembly là một nhóm ngôn ngữ "class of languages" Trong các tài liệu nghiên cứu của những chuyên gia, nhà nghiên cứu đi trước họ đều đã đề cập đến những vấn đề này, tôi chính là đang thuật lại các khái niệm đã được chứng minh sẵn.
Ví dụ đây là một đoạn code C nhỏ
Sau khi được compile, link và assemble nó sẽ thành một file .exe (trong thế giới Windows) hoặc .out (nếu ở trong nền tảng Linux), nhìn cái hình dưới đây là bạn sẽ hiểu tại sao chúng ta nên biết Assembly, vì cơ bản mọi phần mềm đều dịch ra nó và nó cũng dễ đọc hơn so vói machine code và binary, nếu muốn dịch ngược thì bạn bắt buộc phải biết nó, tại vì đa số bạn sẽ được giao cho một file thực thi và phải dịch ngược nó về trạng thái High level language.
Assembly nó sẽ giống như thế này, Assembly instruction tượng trưng cho đoạn code C helloworld bên trên, dưới đây thì là intel x86
Nhiệm vụ của mình trong RE là đọc Assembly và hiểu nó, bạn làm thì sau khi đọc hiểu nó thì sẽ tuỳ vào trường hợp và hoàn cảnh công việc của bạn, nếu như bạn crack phần mềm thì bạn sẽ hiểu cách nó hoạt động để lách luật, còn nếu bạn là malware analyst thì bạn sẽ hiểu để mà ngăn chặn mã độc, ...
Assembly - Kiến thức cơ bản
Tôi sẽ để một số link ở dưới phần Refs để bạn có thể tự học. Đây là những khái niệm khoa học đòi hỏi có sự nghiên cứu, suy ngẫm và liên hệ đến các khái niệm thuộc nhiều chủ đề cho nên tốt nhất người đọc cần tự trang bị các hành trang cần thiết về mặt kiến thức nền tảng.
Đầu tiên chúng ta phải hiểu về những nstruction trong Assembly, thì cơ bản khi mà một chương trình được chạy thì nó sẽ được load lên RAM thì có những giai đoạn xảy ra như sau
Những cái instruction của chương trình sẽ dc CPU đọc thông qua Control Unit
Những instruction sẽ được xử lý và thực thi thông qua ALU (Arithmetic Logic unit) cùng với những input từ người dùng (Bàn phím, chuột, ...) hoặc là những Register (Thanh ghi)
Sau khi mà xử lý xong thì nó sẽ một là lưu giá trị ở trên Register hoặc là xuất ra Output device (màn hình, ...)
Nếu bạn muốn biết thêm về John Von neumann và kiến trúc của ông thì đây là một số thông tin
Những instruction đó như thế nào?
Đa số những instruction này sẽ có dạng mnemonic + optional operands
ví dụ như ở helloworld assembly ở ví dụ trên.
Mnemonic thì sẽ tuỳ vào loại chip, tuy nhiên chức năng có thể sẽ giống nhau, chỉ ghi khác nhau, ở intel x86 thì syntax nó như thế này, chức năng của từng mnemonic sẽ được ghi rõ trong hướng dẫn sử dụng
Operand thì bao gồm có
Immidiate (kiểu như số) vd: 0x10
Register (Thanh ghi) vd: EAX
Memory address (địa chỉ bộ nhớ) [0x0401000 + 4]
Một số instruction nổi tiếng và thường gặp. Ngoài ra, còn nhiều những instruction khác nhưng trong giới hạn cho phép của Đồ án, tôi sẽ nêu ra một vài dẫn chứng mà tôi cho là tiêu biểu, có tính thực tiễn cao như sau:
Data Storage
mov
lea
Arithmetic
add
sub
mul
div
inc
dec
Logic
or
xor
add
shr
shl
Stack
push
pop
call
ret
Control Flow
Cơ bản là assembly nó không có if - else gì cả và cũng chả có for loop, while loop nên cách nó có thể điều khiển flow của chương trình là bằng những flag và jump
jump
test
cmp
jcc (cái này gọi là jump khi condition is met í)
Một số khá nhiều để bạn tự tìm hiểu đấy
Register - Khái niệm
Đây là đơn vị nhỏ nhất chứa dữ liệu trong CPU cho nên là cái tốc độ truy cập dữ liệu của nó à cực kì nhanh, đây là chúng nó nhá
x86 là 32bit cho nên những register này đều là 32bit (AX, AH và AL là 16,8 bit)
Những general register thì đều tương tự nhau. Tuy nhiên, nếu mà không có Convention hay gì hết thì giai đoạn trước, code assembly sẽ rất là loạn (hỗn tạp, và khó kiểm soát, do general register đều tương tự nhau) cho nên người ta cần thiết phải đặt ra một số quy chuẩn ngầm, và nó được áp dụng cho đến ngày nay.
EAX thì dành cho giá trị trả về của hàm (giá trị return của function í)
ECX thì là counters, có nghĩa là biến trong vòng lặp í, mỗi lần lặp thì
ECX sẽ tăng lên EDX hoặc EAX cũng có thể là thương số hoặc là số dư trong phép chia và phép nhân
ESP thì dành cho stack pointer
EBP thì là base pointer
Hai khái niệm ESP và EBP sẽ chịu trách nhiệm cho việc tạo ra stack frame ở phần bên dưới mà tôi chuẩn bị phân tích dưới đây.
EIP là Instruction pointer, trỏ tới instruction tiếp theo chuẩn bị thực thi
EFLAGS dùng để thể hiện thông tin của phép toán vừa được thực hiện ở instruction trước, những cờ hiệu hay sử dụng là zero flag, sign flag, carry flag (đây là những điều kiện jump mà tôi nói ở phần Control Flow ở )
Chi tiết hơn thì có thể tìm ở đây
The Stack - Khái niệm
Như bạn thấy thì chương trình sẽ load lên RAM và CPU chủ yếu sẽ nói chuyện với RAM (như trên kiến trúc von neumann) vậy thì chương trình sẽ load những gì lên RAM?
Đa số các chương trình sẽ load những thứ gọi là "section" lên trên memory (trong windows file thực thi thì sẽ load PE lên, linux thì sẽ load ELF, ... Trong đó sẽ có những thông tin để mà cho loader có thể gọi là thực thi chương trình của bạn), nó sẽ giống giống như thế này
Hi vọng các bạn đã biết stack là gì rồi, thì ở đây bạn có thể thấy khi chương trình được load lên main memory hay còn gọi là RAM thì nó bao gồm:
Stack là LIFO (có nghĩa là Last in first out) là nơi mà những function những biến local được load lên
Heap là dynamic memory
Code là well dễ hiểu mà đúng không "Code", đây là instruction của bạn đấy
Data khởi tạo những biến tĩnh
Stack Frame - Khái Niệm
Như chúng ta đã thấy ở trên hình trên và hình dưới thì stack có fixed size và sẽ lớn lên và hướng về địa chỉ thấp nhất, tuy nhiên thì tại sao chúng có stack frame để làm gì?
Stack frame là là sự kết hợp giữa EBP và ESP, trong đó EBP sẽ là base pointer có nghĩa là trỏ tới đáy của stack còn ESP sẽ là stack pointer trỏ đến đỉnh của stack. Sẽ rất khó để dùng lời văn mà mô tả, trực quan nhất sẽ là một video clip này [video] trong video này ông ấy đã nói rõ những giai đoạn của stack và assembly khi mà một hàm được gọi sẽ như thế nào. Như hình bên trên cái bạn sẽ thấy rằng có những khái niệm như:
Return Address
Parameters
Saved EBP (Old EBP)
Local Variables
Trong đoạn code bên dưới ta thấy rằng có hàmint add(int a, int b)
, trong đó a và b chính là parameters, sum chính là local variables. Vậy còn Saved EBP (Old EBP) và Return Address nằm ở đâu?
Nếu như trong đoạn C code này thì sau khi thực thi xong hàm add(int a, int b)
thì nó sẽ trả về tổng rồi thoát ra khỏi hàm đó và tiếp tục hàm printf() phải không?
Ai học lập trình cũng biết nó sẽ thực thi từ trên xuống, tuy nhiên đối với assembly thì nó không dễ như vậy, mỗi hàm có địa chỉ riêng và nằm đâu đó trên RAM (Nếu chương trình được compile ở x64 thì mọi chuyện còn khó hơn, do ASLR, PIE,… ) chúng ta sẽ phải sử dụng instruction call để nhảy tới hàm đó, mỗi hàm đó sẽ tạo ra một stack frame riêng, cũng giống như hàm main là một stack frame.
Câu hỏi tiếp theo đặt ra là làm thế nào nó quay lại stack frame cũ sau khi thực thi xong? Đó chính là Saved EBP hay còn gọi là Old EBP, sau khi hàm thực hiện xong thì stack của hàm đó sẽ pop 2 giá trị ra, đó là Saved EBP (Old EBP) và Return Address. Old EBP sẽ là EBP của người gọi hàm add(int a, int b) đó chính là hàm main() và Return Address đó chính là địa chỉ của instruction tiếp theo (có nghĩa Return Address sẽ cập nhật EIP với giá trị của instruction tiếp theo)
Vậy làm sao mà chúng ta có Return Address và Saved EBP? Đó là do trước khi gọi bất kì một hàm nào thì chúng ta phải push lên stack 2 giá trị này, như trên hình thì bạn sẽ thấy parameters của hàm sẽ push lên trước rồi tới 2 giá trị này
Hình này sẽ cho bạn thấy tổng quan hơn
Calling Conventions
Nhưng nãy tôi có nhắc tới Caller (người gọi hàm) thì trong phần Calling Convention này tôi sẽ nói về trạng thái của Caller và Callee (hàm được gọi) cùng với đó là sự tương tác giữa chúng.
Vẫn là câu hỏi tại sao chúng ta cần biết calling conventions?
Thứ nhất đây là một bước rất quan trọng trong việc hình thành stack frame, tại vì nó sẽ ảnh hưởng đến việc vị trí của parameter (sẽ được đẩy lên stack hay là chứa ở trong register hay là cả hai)
Thứ hai calling convention sẽ chỉ ra caller và callee rằng ai sẽ dọn dẹp sau khi hoàn thành xong nhiệm vụ của nó, ai sẽ là người dọn dẹp stack frame.
Những Calling Convention:
__cdecl
Đây gọi là C Declaration, đây là convention xuất hiện và dùng trong compiler của Microsoft đồng thời cũng được dùng trong các C compiler khác ở x86 __cdecl sẽ có những thuộc tính sau đây
Caller sẽ là người dọn dẹp stack
Caller sẽ push parameter lên stack theo thứ tự từ phải sang trái
Caller cũng sẽ là người pop parameter ra khỏi stack
Hình trên cho ta thấy rõ là __cdecl push từ phải qua trái và thực hiện dọn dẹp stack bằng cách add esp, 12
thông qua caller main()
__stdcall
Đây gọi là standard call , điều khác biệt ở đây đó chính là callee sẽ phải dọn dẹp stack chứ không phải là caller, nhìn như trên hình chúng ta sẽ thấy rằng main() không còn add esp, 12 nữa mà thay vào đó demo() sẽ phải ret 12
Ngoài ra stdcall phải biết rõ số parameter truyền vào mới có thể sử dụng được, một ví dụ là printf() không thể dùng convention này là do parameter truyền vào printf() nó có thể giao động cho nên printf() bắt buộc phải dùng cdecl
Theo như docs của Microsoft ghi thì cdecl sẽ cho file thực thi có size lớn hơn so với stdcall
__fastcall
Đúng như tên gọi của nó. Thì nó sử dụng register EDX, ECX để chứa 2 parameter đầu tiên từ trái sang phải, những parameter còn lại sẽ được push lên stack theo thứ tự từ phải qua trái, nhìn như phía bên dưới
Và callee sẽ dọn dẹp stack
__thiscall
Đây là convention dành riêng cho C++, nó dành cho những thành viên class của C++ ở x86, dưới __thiscall thì Callee sẽ dọn dẹp stack, paramater cũng sẽ push lên stack theo thứ tự từ phải sang trái, còn pointer this sẽ được đẩy vào register ECX chứ không phải stack
PE Format
Cái chuẩn này xứng đáng có một bài viết riêng về nó, cơ bản là có rất nhiều thứ để nói về nó, tuy nhiên thì tôi sẽ cố gắng tóm gọn nó trong phần này của tôi, mặc dù có rất nhiều người chắc chắn sẽ nói về vấn đề này hay hơn và chi tiết hơn tôi rất nhiều. Nhưng không sao đây là cách tôi học bằng cách giải thích và ghi blog ra tôi sẽ nhớ lâu hơn. hi vọng mọi người thích cách diễn đạt của tôi. Dưới đây là phần chi tiết về PE Format dưới lời văn của tôi.
PE là viết tắt cho Portable Executable (không phải Physical Education đâu) ~~!
Disassembler - IDA
Đây là dụng cụ không thể nào thiếu đối với một Reverse Engineer, đây là sản phẩm được phân phối bởi HexRay, mặc dù có rất nhiều Disassembler khác tuy nhiên rất nhiều chuyên gia sử dụng IDA.
Vậy thì Disassembler là gì?
Disassembly là quá trình dịch từ machine code thành assembly, cho nên disassembler là chương trình dịch machine code thành assembly một cách tự động. IDA còn làm hơn thế nữa, nó sẽ cho bạn thấy những biểu đồ như thế này
Nó có bản free và bản trả phí, ở đây thì tôi sẽ sử dụng bản free, do bản trả phí quá mắc, tôi không có tiền mua. Ngoài ra còn rất nhiều Disassembler khác như Binary Ninja, Ghidra, Hopper, Radare2, ... hầu như chúng đều phục vụ 1 mục đích, tuy nhiên có thể sẽ sử dụng những thuật toán disassembly khác nhau. Tôi sẽ nhắc đến 2 thuật toán dùng khá nhiều trong disassembly đó là:
Linear Sweep Disassembly
Recursive Descent Disassembly (IDA)
Trong cuốn The IDA Pro book: The unofficial guide to the world's most popular disassembler đã có cho chúng ta thấy một thuật toán disassembly sẽ như thế nào và việc phát triễn một thuật toán như vậy sẽ có những khó khăn như thế nào?
Bước 1
Chúng ta phải tìm được phần chứa code (hay instruction) để có thể disassemble, thường thì những instruction sẽ hòa trộn với dữ liệu cho nên phải thuật toán phải phân biệt được 2 thứ đó. Trong trường hợp cơ bản nhất hay còn gọi là thông thường nhất đó chính là 2 dạng file nổi tiếng đó là PE (dành cho Windows) và ELF (Dành cho những hệ Unix-based) những định dạng file này thường sẽ có kiến trúc dạng cây cùng với phương thức định vị entry point (như phần PE Format ở trên có nói) của vị trị code nằm ở đâu.
Bước 2
Sau khi chúng ta tìm được vị trí bắt đầu (entry point) thì việc tiếp theo sẽ là phải đọc những giá trị ở đó, quá trình này rất phức tạp do chúng ta cần phải parse (dịch) những opcode đó sao cho đúng với assembly mnemonic. Không chỉ có thế, chúng ta cần phải biết những instruction cần bao nhiêu operand, ... và chúng ta cũng cần nên nhớ rằng mỗi hệ máy khác nhau lại có chuẩn khác nhau.
Bước 3
Sau khi mọi thứ đã được hiểu và dịch, operand và mnemonic thì chúng sẽ xuất nó ra màn hình dưới dạng assembly, chúng ta có thể chọn giữa 2 định dạng được sử dụng nhiều nhất đó là AT&T hoặc Intel.
Bước 4
Chúng tự cứ lặp đi lặp lại quá trình này với những instruction tiếp theo cho đến khi hết toàn bộ file
Tại sao chúng ta lại disassembly chương trình làm gì?
Phân tích mã độc
Phân tích những chương trình có mã nguồn đóng để tìm lỗi
Phân tích code do compiler tạo ra có tương thích với hiệu năng compiler và tính đúng đắn của nó không
Hiển thị instruction khi debug
Tôi sẽ không đi sâu vào những vấn đề này, vì nó là những chủ đề rất rộng để có thể vừa những trang giấy này, nó cần rất nhiều kinh nghiệm
Debugger - OllyDbg
Một trong những debugger nổi tiếng nhất sử dụng cho x86, nó đã tồn tại hơn 10 năm và điều hay nhất là nó miễn phí. Rất nhiều nhà nghiên cứu đã sử dụng trong quá trình phân tích mã độc và RE. Ở đây tôi sẽ không hướng dẫn cách sử dụng nó.
Ngoài ra chúng ta còn những Debugger nổi tiếng khác như WinDbg, GDB, x64dbg,...
Decompiler - Hex-Rays
Những Kỹ Thuật Dịch Ngược
Những gì tôi ghi bên trên là những điều cơ bản và còn khá là nhỏ nhắn trong một lĩnh vực rộng như thế này, thú thật tôi cũng không phải là một Reverse Engineer thần thánh gì. Tuy nhiên những điều tôi ghi dưới đây chính là những kinh nghiệm của những người đi trước tôi và là những chuyên viên bảo mật có tiếng tăm trong ngành
Tôi sẽ chia làm 3 phần đó là phân tích tĩnh (static analysis) và phân tích động (dynamic analysis) sau đó tôi sẽ nói về một số vấn đề liên quan tới dịch ngược ở hệ điều hành windows
Static Analysis
Đây sẽ thường là bước đầu tiên để thực hiện những phân tích về phần mềm, đây gọi là phân tích tĩnh. Phân tích tĩnh có nghĩa là không chạy chương trình là mà chỉ đọc hiểu luồng của chương trình qua assembly
Dynamic Analysis
Reverse Engineering - Windows
Những Kỹ Thuật Chống Dịch Ngược
Nếu như dịch ngược quá dễ dàng thì ai cũng sẽ học dịch ngược như thế không còn phần mềm nào sẽ được gọi là mã nguồn đóng cả, cho nên để làm cho cuộc đời của một Reverse Engineer khó khăn hơn và khổ sở hơn thì những lập trình viên đã phát triễn những kỹ thuật chống dịch ngược, sau đây tôi sẽ nói về một số kỹ thuật
Packer/ Crypter/ Protector
Đây là một trong những phương pháp dễ dùng và cũng rất hiệu quả để làm tăng thời gian RE lên
Anti-Disassembly
-- Những phần này tôi sẽ nghiên cứu và viết thêm.
Anti-Debugging
Anti-Virtual Machine
Refs
PE Format
Assembly
The Stack
Calling Convention
Những Kỹ Thuật Chống Dịch Ngược
Last updated
Was this helpful?