Shellcode
Tổng quan
Khái niệm : Alphanumeric shellcode (AS) là loại shellcode chỉ chứa các ký tự chữ cái hoặc chữ số.
Ví dụ :
Vai trò : Một số chương trình sẽ lọc tham số đầu vào trước khi xử lý. Các tham số ứng với tên file, tên đường dẫn, tên item … thường chỉ được cho phép gồm các ký tự chữ cái và chữ số. Theo đó, các shellcode thông thường (Non-alphanumeric) sẽ không được truyền vào đầy đủ. Vì thế cần các AS tương ứng để thay thế.
Đặc điểm : AS được thực hiện dựa vào bản thân các mã lệnh binary ứng với các kí tự alphanumeric. Chẳng hạn :
Kí tự mã hexa lệnh
‘A’ 41 inc ecx
‘B’ 42 inc edx
… … …
‘Z’ 5A pop edx
Với bộ lệnh alphanumeric, ta có :
mov edx, ecx
ta phải thực hiện
push ecx( mã 51 – ‘Q’ )
pop edx( mã 5A – ‘Z’ ).
Ví dụ : lệnh mov ebx, eax.
push eax ( 50 – ‘P’) // Lệnh popa sẽ pop giá trị này ra eax=> giữ nguyên eax
push ecx (51 – ‘Q’) // giữ nguyên ecx sau popa
push edx(52 – ‘R’) // giữ nguyên edx sau popa
push eax (50 – ‘P’) // Vị trí này trên stack sẽ được lệnh popa pop ra ebx thực hiện mov ebx, eax.
push esp (54 – ‘T’) // giữ nguyên esp
push ebp(55 –‘U’) // giữ nguyên ebp
push esi (56 – ‘V’) // giữ nguyên esi
push edi (57 – ‘W’) // giữ nguyên edi
popa (61 – ‘a’) //pop 8 giá trị ra 8 thanh ghi theo thứ tự.
_ Không có lệnh inc eax (mã 40 – non-alpha) .chỉ có từ inc ecx ( mã 41 – ‘A’ ) trở đi => muốn tăng eax lại phải vòng vèo qua thanh ghi khác L
Mô phỏng lệnh NOT edx
XOR ebx,ebx // ebx = 0
DEC ebx // ebx = FFFFFFFF
XOR edx,ebx // xor edx,FFFFFFFF tương đương với NOT edx.
Trên đây là vài hạn chế dễ gặp ,và cách thay thế. Ngoài ra, tất nhiên còn có rất nhiều khó khăn nữa nếu viết trực tiếp AS, mà dễ thấy nhất là hạn chế : thiếu các lệnh nhảy (call,jmp…). Do đó việc sử dụng tập lệnh alphanumeric để viết mới một shellcode hoàn chỉnh là không khả thi. Cách thức để có được AS là sử dụng tập lệnh alphanumeric để mã hóa một non-AS thành AS.
Sau đây, ta sẽ bàn đến các phương pháp thông dụng để mã hóa ra được một AS
1. Phương pháp XOR_PATCH cổ điển
Đây là cấu trúc phổ biến, thường thấy ở các đoạn code tự giải mã. Việc mã hóa để tạo ra shellcode, cũng như quá trình giải mã khi chạy shellcode được thực hiện bởi các lệnh XOR. Theo như cấu trúc 3 phần trên, khi vào đầu AS, đoạn Initialization sẽ khởi tạo môi trường (giá trị các thanh ghi, trạng thái stack … ) để chuẩn bị cho quá trình giải mã. Tiếp theo, phần Patcher sẽ thực hiện giải mã vùng Data thành dữ liệu gốc trước khi mã hóa( non-alpha). Sau đó, điều khiển chương trình sẽ chạy đến vùng dữ liệu đã được giải mã và thực thi như shellcode thông thường.
_ Dữ liệu sau mã hóa ( vùng Data ) phải hoàn toàn là alphanumeric.
_ Các lệnh làm nhiệm vụ giải mã ngược lại ( Initialization và Patcher) cũng đều là alphanumeric.
Và ngài Rix đã đưa ra phương pháp mã hóa như sau : Từ dữ liệu non-alphanumeric đầu vào, phân loại ra 4 trường hợp :
_ CATEGORY_ALPHA : Các bytes đã sẵn là các ký tự chữ cái hoặc chữ số.
_ CATEGORY_ALPHA_NOT : Các bytes mà NOT của nó sẽ là alphanumeric.
_ CATEGORY_XOR : Các bytes mà XOR với một giá trị alpha nào đó, được một giá trị alpha khác.
_ CATEGORY_XOR_NOT : Các bytes mà NOT của nó mang đi XOR với một giá trị alpha sẽ được một giá trị alpha khác.
Các bytes liền nhau có cùng category, tùy vào số lượng mà được nhóm thành một group 4 bytes (DWORD) hoặc 2 bytes (WORD) hay 1 byte (BYTE).
Như vậy, sau khi phân chia, shellcode đầu vào ( non-alphanumeric) sẽ là một dãy các groups. Mỗi group là một số DWORD hoặc WORD hay BYTE thuộc một trong 4 cetagories nêu trên. Tùy theo phân loại mà mỗi group sẽ được xử lý ( giữ nguyên, NOT, XOR) thành các bytes alphanumeric và lưu vào phần Data, đồng thời các lệnh để giải mã lại (NOT, XOR ngược lại) được ghi vào vùng Patcher. Do các lệnh tăng giảm thanh ghi, và NOT hay XOR (DWORD,WORD, BYTE) cùng với các dữ liệu để XOR lại, đều là alphanumeric nên vùng Patcher hoàn toàn alphanumeric. Việc khởi tạo các thanh ghi chuẩn bị cho quá trình giải mã cũng có thể được thực hiện bởi tập lệnh alphanumeric.
_ Nhược điểm : Do việc mã hóa và giải mã phụ thuộc vào từng groups nên đoạn patcher phải thực hiện tuần tự và tùy ứng qua từng groups. Dẫn đến độ dài của patcher là khá lớn, và không những phụ thuộc vào độ dài shellcode đầu vào, mà còn liên quan đến tính chất các bytes bên trong shellcode đó. Kết quả trả ra một AS có độ dài lớn hơn nhiều lần so với độ dài shellcode gốc. Đây thực sự là vấn đề trong các trường hợp shellcode truyền vào bị hạn chế độ dài.
2. Phương pháp cải tiến
_ Tìm một số alphanumeric cũng có 4 bit thấp là b ( luôn tìm được số như vậy, bởi ít nhất có từ 0x41 đến 0x50 là alphanumeric).
Giả sử số alpha đó là eb.
_ Lấy d = a XOR e
_ Tìm một số alphanumeric khác có 4 bit thấp là d . Chẳng hạn đó là số cd .
_ Lưu 2 số alphanumeric : cd, eb vào vùng Data . ( Hai số alpha này sẽ là nguyên liệu để khôi phục lại số non-alphanumeric ab ban đầu)
_ Đoạn Patcher khi chạy sẽ thực hiện giải mã : Lấy d dịch trái 4 bit thành d0, đem XOR với eb . Vì (d XOR e = a) và (0 XOR b = b) nên : d0 XOR eb = ab (chính là byte gốc ban đầu). Patcher thực hiện các phép toán này, giống nha đối với mọi cặp alpha trên vùng Data. Nên nó được xây dựng thành một vòng lặp cho đến khi giải mã hết vùng Data.
_ Vùng Data dài gấp đôi độ dài shellcode đầu vào do dùng 2 bytes alpha để mã hóa 1 byte thường.
_ Vùng Patcher chỉ là một vòng lặp ngắn ngủi trong khi theo phương pháp cũ, Patcher dài gấp nhiều lần so với độ dài shellcode gốc. Bởi vậy, phương pháp cải tiến này đã khắc phục được nhược điểm của cách cũ, cho ra một AS có độ dài chấp nhận được (chỉ hơn hai lần độ dài shellcode đầu vào).
GetPC
1. Khái niệm
GetPC (get Program Counter) hay còn gọi GetEIP là đoạn mã dùng để định vị chính nó trong không gian bộ nhớ. Thường là thiết lập một thanh ghi nào đó trỏ đến địa chỉ vùng nhớ chứa bản thân. Hay được sử dụng trong các tiến trình tự giải mã, mà cụ thể, như ở dưới đây, ta sẽ xét đến GetPC với vai trò xác định vị trí của shellcode.
2. Phân loại
a) CALL GetPC
Là phương pháp đơn giản nhất, việc định vị dựa trên cơ sở : địa chỉ lệnh sau lời gọi hàm ( call ) được lưu trên stack.
Chẳng hạn :
Như ta thấy, sau khi đoạn mã trên ( 6 bytes ) được thực hiện thì thanh ghi
ECX sẽ trỏ đến địa chỉ $+5, nơi mà có shellcode nằm ngay sau nó. Tuy nhiên, đoạn GetPC này lại có chứa các bytes NULL( 0x00) làm ngắt xâu. Vì thế giải pháp thay thế sẽ sử dụng lệnh JMP xuống dưới rồi CALL ngược lại ( khoảng cách âm, có giá trị 0xFFFFFFxx – không còn byte NULL ) :
Phương án này không còn byte NULL nào nhưng lại gặp phải nhược điểm khác : Do sử dụng lệnh JMP SHORT nên khoảng cách từ lệnh POP đến CALL ( tương ứng với độ dài shellcode) chỉ giới hạn trong 126 bytes. Phương pháp sau sẽ khắc phục nhược điểm này :
Lệnh CALL -1 ( 0xFFFFFFFF ) sẽ gọi đến địa chỉ $+4, lúc này trạng thái địa chỉ lệnh sẽ như sau :
b) FSTENV GetPC
Sử dụng lệnh FSTENV để lưu trữ các thông tin về môi trường xử lý số thực (FPU operating environment) lên stack, trong đó có thông tin về con trỏ lệnh(instruction pointer), lấy được giá trị của con trỏ lệnh này tức là đã “GetEIP” :
c) SEH GetPC
Đây là phương pháp dài và phức tạp nhất nhưng cũng là phương pháp duy nhất cho một GetPC code hoàn toàn alphanumeric ( điều cần thiết để có được một alphanumeric shellcode hoàn chỉnh).
Dựa trên cơ chế xử lý exception của Window, đoạn mã GetPC theo phương pháp này, một mặt tạo ra một cấu trúc xử lý exception ( structure exception handler – SEH ) có nhiệm vụ lấy thông tin về sự kiện exception. Mặt khác, chủ động gây exception để điều khiển chương trình chuyển đến handler lấy địa chỉ của chính lệnh vừa gây exception, qua đó định vị được shellcode.
Cấu trúc : Gồm các đoạn code có nhiệm vụ :
Chuẩn bị một exception handler để xác định địa chỉ lệnh gây exception (chính là thực hiện GetPC) , và chuyển điều khiển chương trình đến sau lệnh đó ( nhảy đến shellcode). Cở sở lý thuyết để thực hiện việc GetPC khi có exception xảy ra là :
Dựa trên cơ sở này, ta dễ dàng lấy được địa chỉ lệnh gây exception vào một thanh ghi nào đó, đồng thời nhảy về shellcode, chỉ bằng một đoạn mã ngắn ngủi. Và công việc cần làm:
Ví dụ : Đây là đoạn GetPC đặt ECX trỏ đến vùng địa chỉ ngay sau nó ( thường là shellcode) và nhảy đến địa chỉ này :
Egg-hunt shellcode
1. Khái niệm
_Create SEH Handler : Lấy địa chỉ của đoạn mã trên ghi vào vị trí của handler trên FS:[0] .
_Scan_Loop : Vòng lặp tìm kiếm các phần của shellcode và đặt vào đáy stack ( FS:[8]) và call tới đó khi kết thúc vòng lặp.
Cụ thể, dưới đây là toàn bộ mã asembly của Omelet Shellcode :
Khái niệm : Alphanumeric shellcode (AS) là loại shellcode chỉ chứa các ký tự chữ cái hoặc chữ số.
Ví dụ :
PHP:
V34dVVVXH49HHHPhYAAQhZYYYYAAQQDDDjPXP4Hd30V3v034dYV34014dZV34dNj334dXXXX3D241D24X3D281D28FFFX3D29f1D29XX3Dqb3Tpf1Tpf96IIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJItqkymSXi9N9IjNZysDTdyd3kmQrSVsbBQoKFlLmSXkylm2l4MNNwNRNl1rcO1mhI1NJTOzK9UJQn7iwwTmQw3MjspBanuMKukYBkamnmNlbnGFgeJnkK4vKHquON7LK1BCOM7mPxv9tkn2MXvwojsMN3qVan7mhjYyPsOMEW7MCSXwu7kYpcCMNmlIZVNJOXqvMlkwpL7v8tnVOLKWOONnK0zIwOZZgwb2U3ckvgUZ47UMouYyKOw6LL1rCOamytcnepKwBZwrGY1oPtM1moLMYLUZLO3LmXem27bNqbKXrQY1QcouELOgipwqsOzVlLA
Vai trò : Một số chương trình sẽ lọc tham số đầu vào trước khi xử lý. Các tham số ứng với tên file, tên đường dẫn, tên item … thường chỉ được cho phép gồm các ký tự chữ cái và chữ số. Theo đó, các shellcode thông thường (Non-alphanumeric) sẽ không được truyền vào đầy đủ. Vì thế cần các AS tương ứng để thay thế.
Đặc điểm : AS được thực hiện dựa vào bản thân các mã lệnh binary ứng với các kí tự alphanumeric. Chẳng hạn :
Kí tự mã hexa lệnh
‘A’ 41 inc ecx
‘B’ 42 inc edx
… … …
‘Z’ 5A pop edx
Với bộ lệnh alphanumeric, ta có :
- Một số lệnh xor và cmp tương ứng với các kí tự ‘0’ – ‘9’
- Một số lệnh tăng, giảm,push,pop thanh ghi tương ứng với các kí tự ‘A’ – ‘Z’
- Một số lệnh nhảy có điều kiện,lệnh vào ra : insb, insd, outsb, outsd tương ứng với các kí tự ‘a’ – ‘z’.
- Không có lệnh mov.
mov edx, ecx
ta phải thực hiện
push ecx( mã 51 – ‘Q’ )
pop edx( mã 5A – ‘Z’ ).
- Bản thân việc thực hiện push,pop này cũng có hạn chế : chỉ có đến pop edx (mã 5A – ‘Z’), từ lệnh pop ebx (mã 5B – non-Alphanumeric) trở đi không thuộc tập lệnh Alphanumeric. Do đó các lệnh mov đến các thanh ghi ebx, esp,ebp, esi,edi không thê thực hiện bằng 2 lệnh push pop như trên. Ta phải dung lệnh popa (mã 61 – ‘a’ ) để pop ra cả 8 thanh ghi.
Ví dụ : lệnh mov ebx, eax.
push eax ( 50 – ‘P’) // Lệnh popa sẽ pop giá trị này ra eax=> giữ nguyên eax
push ecx (51 – ‘Q’) // giữ nguyên ecx sau popa
push edx(52 – ‘R’) // giữ nguyên edx sau popa
push eax (50 – ‘P’) // Vị trí này trên stack sẽ được lệnh popa pop ra ebx thực hiện mov ebx, eax.
push esp (54 – ‘T’) // giữ nguyên esp
push ebp(55 –‘U’) // giữ nguyên ebp
push esi (56 – ‘V’) // giữ nguyên esi
push edi (57 – ‘W’) // giữ nguyên edi
popa (61 – ‘a’) //pop 8 giá trị ra 8 thanh ghi theo thứ tự.
- Không có các lệnh toán học như add,sub…
_ Không có lệnh inc eax (mã 40 – non-alpha) .chỉ có từ inc ecx ( mã 41 – ‘A’ ) trở đi => muốn tăng eax lại phải vòng vèo qua thanh ghi khác L
- Không có toán tử phủ định NOT.
Mô phỏng lệnh NOT edx
XOR ebx,ebx // ebx = 0
DEC ebx // ebx = FFFFFFFF
XOR edx,ebx // xor edx,FFFFFFFF tương đương với NOT edx.
Trên đây là vài hạn chế dễ gặp ,và cách thay thế. Ngoài ra, tất nhiên còn có rất nhiều khó khăn nữa nếu viết trực tiếp AS, mà dễ thấy nhất là hạn chế : thiếu các lệnh nhảy (call,jmp…). Do đó việc sử dụng tập lệnh alphanumeric để viết mới một shellcode hoàn chỉnh là không khả thi. Cách thức để có được AS là sử dụng tập lệnh alphanumeric để mã hóa một non-AS thành AS.
Sau đây, ta sẽ bàn đến các phương pháp thông dụng để mã hóa ra được một AS
1. Phương pháp XOR_PATCH cổ điển
- Đưa ra bởi Rix – người đi đầu trong việc nghiên cứu alphanumeric shellcode.
- Cấu trúc :
Đây là cấu trúc phổ biến, thường thấy ở các đoạn code tự giải mã. Việc mã hóa để tạo ra shellcode, cũng như quá trình giải mã khi chạy shellcode được thực hiện bởi các lệnh XOR. Theo như cấu trúc 3 phần trên, khi vào đầu AS, đoạn Initialization sẽ khởi tạo môi trường (giá trị các thanh ghi, trạng thái stack … ) để chuẩn bị cho quá trình giải mã. Tiếp theo, phần Patcher sẽ thực hiện giải mã vùng Data thành dữ liệu gốc trước khi mã hóa( non-alpha). Sau đó, điều khiển chương trình sẽ chạy đến vùng dữ liệu đã được giải mã và thực thi như shellcode thông thường.
- Nguyên lý :
_ Dữ liệu sau mã hóa ( vùng Data ) phải hoàn toàn là alphanumeric.
_ Các lệnh làm nhiệm vụ giải mã ngược lại ( Initialization và Patcher) cũng đều là alphanumeric.
Và ngài Rix đã đưa ra phương pháp mã hóa như sau : Từ dữ liệu non-alphanumeric đầu vào, phân loại ra 4 trường hợp :
_ CATEGORY_ALPHA : Các bytes đã sẵn là các ký tự chữ cái hoặc chữ số.
_ CATEGORY_ALPHA_NOT : Các bytes mà NOT của nó sẽ là alphanumeric.
_ CATEGORY_XOR : Các bytes mà XOR với một giá trị alpha nào đó, được một giá trị alpha khác.
_ CATEGORY_XOR_NOT : Các bytes mà NOT của nó mang đi XOR với một giá trị alpha sẽ được một giá trị alpha khác.
Các bytes liền nhau có cùng category, tùy vào số lượng mà được nhóm thành một group 4 bytes (DWORD) hoặc 2 bytes (WORD) hay 1 byte (BYTE).
Như vậy, sau khi phân chia, shellcode đầu vào ( non-alphanumeric) sẽ là một dãy các groups. Mỗi group là một số DWORD hoặc WORD hay BYTE thuộc một trong 4 cetagories nêu trên. Tùy theo phân loại mà mỗi group sẽ được xử lý ( giữ nguyên, NOT, XOR) thành các bytes alphanumeric và lưu vào phần Data, đồng thời các lệnh để giải mã lại (NOT, XOR ngược lại) được ghi vào vùng Patcher. Do các lệnh tăng giảm thanh ghi, và NOT hay XOR (DWORD,WORD, BYTE) cùng với các dữ liệu để XOR lại, đều là alphanumeric nên vùng Patcher hoàn toàn alphanumeric. Việc khởi tạo các thanh ghi chuẩn bị cho quá trình giải mã cũng có thể được thực hiện bởi tập lệnh alphanumeric.
- Đặc điểm :
_ Nhược điểm : Do việc mã hóa và giải mã phụ thuộc vào từng groups nên đoạn patcher phải thực hiện tuần tự và tùy ứng qua từng groups. Dẫn đến độ dài của patcher là khá lớn, và không những phụ thuộc vào độ dài shellcode đầu vào, mà còn liên quan đến tính chất các bytes bên trong shellcode đó. Kết quả trả ra một AS có độ dài lớn hơn nhiều lần so với độ dài shellcode gốc. Đây thực sự là vấn đề trong các trường hợp shellcode truyền vào bị hạn chế độ dài.
2. Phương pháp cải tiến
- Tác giả : Berend-Jan Wever.
- Cấu trúc : Tương tự phương pháp XOR_PATCH của Rix đã đề cập ở trên.
- Nguyên lý : Có chung một công thức để mã hóa cho tất cả các bytes chứ không phân loại như phương pháp cũ. Cụ thể, đối với một byte bất kỳ :
_ Tìm một số alphanumeric cũng có 4 bit thấp là b ( luôn tìm được số như vậy, bởi ít nhất có từ 0x41 đến 0x50 là alphanumeric).
Giả sử số alpha đó là eb.
_ Lấy d = a XOR e
_ Tìm một số alphanumeric khác có 4 bit thấp là d . Chẳng hạn đó là số cd .
_ Lưu 2 số alphanumeric : cd, eb vào vùng Data . ( Hai số alpha này sẽ là nguyên liệu để khôi phục lại số non-alphanumeric ab ban đầu)
_ Đoạn Patcher khi chạy sẽ thực hiện giải mã : Lấy d dịch trái 4 bit thành d0, đem XOR với eb . Vì (d XOR e = a) và (0 XOR b = b) nên : d0 XOR eb = ab (chính là byte gốc ban đầu). Patcher thực hiện các phép toán này, giống nha đối với mọi cặp alpha trên vùng Data. Nên nó được xây dựng thành một vòng lặp cho đến khi giải mã hết vùng Data.
- Đặc điểm : Theo như nguyên lý trên, ta thấy rằng :
_ Vùng Data dài gấp đôi độ dài shellcode đầu vào do dùng 2 bytes alpha để mã hóa 1 byte thường.
_ Vùng Patcher chỉ là một vòng lặp ngắn ngủi trong khi theo phương pháp cũ, Patcher dài gấp nhiều lần so với độ dài shellcode gốc. Bởi vậy, phương pháp cải tiến này đã khắc phục được nhược điểm của cách cũ, cho ra một AS có độ dài chấp nhận được (chỉ hơn hai lần độ dài shellcode đầu vào).
GetPC
1. Khái niệm
GetPC (get Program Counter) hay còn gọi GetEIP là đoạn mã dùng để định vị chính nó trong không gian bộ nhớ. Thường là thiết lập một thanh ghi nào đó trỏ đến địa chỉ vùng nhớ chứa bản thân. Hay được sử dụng trong các tiến trình tự giải mã, mà cụ thể, như ở dưới đây, ta sẽ xét đến GetPC với vai trò xác định vị trí của shellcode.
2. Phân loại
a) CALL GetPC
Là phương pháp đơn giản nhất, việc định vị dựa trên cơ sở : địa chỉ lệnh sau lời gọi hàm ( call ) được lưu trên stack.
Chẳng hạn :
|
Như ta thấy, sau khi đoạn mã trên ( 6 bytes ) được thực hiện thì thanh ghi
ECX sẽ trỏ đến địa chỉ $+5, nơi mà có shellcode nằm ngay sau nó. Tuy nhiên, đoạn GetPC này lại có chứa các bytes NULL( 0x00) làm ngắt xâu. Vì thế giải pháp thay thế sẽ sử dụng lệnh JMP xuống dưới rồi CALL ngược lại ( khoảng cách âm, có giá trị 0xFFFFFFxx – không còn byte NULL ) :
|
Phương án này không còn byte NULL nào nhưng lại gặp phải nhược điểm khác : Do sử dụng lệnh JMP SHORT nên khoảng cách từ lệnh POP đến CALL ( tương ứng với độ dài shellcode) chỉ giới hạn trong 126 bytes. Phương pháp sau sẽ khắc phục nhược điểm này :
|
Lệnh CALL -1 ( 0xFFFFFFFF ) sẽ gọi đến địa chỉ $+4, lúc này trạng thái địa chỉ lệnh sẽ như sau :
|
b) FSTENV GetPC
Sử dụng lệnh FSTENV để lưu trữ các thông tin về môi trường xử lý số thực (FPU operating environment) lên stack, trong đó có thông tin về con trỏ lệnh(instruction pointer), lấy được giá trị của con trỏ lệnh này tức là đã “GetEIP” :
|
c) SEH GetPC
Đây là phương pháp dài và phức tạp nhất nhưng cũng là phương pháp duy nhất cho một GetPC code hoàn toàn alphanumeric ( điều cần thiết để có được một alphanumeric shellcode hoàn chỉnh).
Dựa trên cơ chế xử lý exception của Window, đoạn mã GetPC theo phương pháp này, một mặt tạo ra một cấu trúc xử lý exception ( structure exception handler – SEH ) có nhiệm vụ lấy thông tin về sự kiện exception. Mặt khác, chủ động gây exception để điều khiển chương trình chuyển đến handler lấy địa chỉ của chính lệnh vừa gây exception, qua đó định vị được shellcode.
Cấu trúc : Gồm các đoạn code có nhiệm vụ :
Chuẩn bị một exception handler để xác định địa chỉ lệnh gây exception (chính là thực hiện GetPC) , và chuyển điều khiển chương trình đến sau lệnh đó ( nhảy đến shellcode). Cở sở lý thuyết để thực hiện việc GetPC khi có exception xảy ra là :
- Tại thời điểm điểu khiển chương trình được chuyển đến exception handler , ô nhớ thứ hai tính từ đỉnh stack sẽ lưu địa chỉ cấu trúc struct_exception_info .
- Trong cấu trúc struct_exception_info này, DWORD thứ tư sẽ trỏ đến địa chỉ lệnh gây exception.
Dựa trên cơ sở này, ta dễ dàng lấy được địa chỉ lệnh gây exception vào một thanh ghi nào đó, đồng thời nhảy về shellcode, chỉ bằng một đoạn mã ngắn ngủi. Và công việc cần làm:
- Tìm được một vùng nhớ thích hợp để ghi đoạn mã ấy lên. Để đảm bảo vùng nhớ là ghi được, và không bị chặn bởi cơ chế SafeSEH, nên chọn vùng nhớ heap của tiến trình ( bắt đầu từ địa chỉ trỏ bởi PEB[0x18:0x1B], địa chỉ của PEB được trỏ bởi FS:[30] )
- Đăng ký địa chỉ heap này thành exception handler (để được gọi khi có exception xảy ra) bằng cách ghi đè địa chỉ vào FS:[0]+4
Ví dụ : Đây là đoạn GetPC đặt ECX trỏ đến vùng địa chỉ ngay sau nó ( thường là shellcode) và nhảy đến địa chỉ này :
PHP:
V34dVVVXH49HHHPhYAAQhZYYYYAAQQDDDjPXP4Hd30V3v034dYV34014dZV34dNj334dXXXX3D241D24X3D281D28FFFX3D29f1D29XX3Dqb3Tpf1Tpf96
Egg-hunt shellcode
1. Khái niệm
- Là loại shellcode gồm phần mã truy tìm và các phần dữ liệu được phân nhỏ.
- Thường dùng với mục đích che giấu shellcode, hoặc trong các trường hợp bị hạn chế về độ dài dữ liệu truyền vào.
- Trong Egg-hunt shellcode, đoạn mã trùy tìm, cũng như các thành phần phân nhỏ, được đưa vào bộ nhớ dưới dạng dữ liệu của chương trình nào đó.
- Các phần dữ liệu của shellcode bắt đầu bằng những byte đánh dấu và byte chỉ số thứ tự. Chúng có thể nằm rải rác trong không gian bộ nhớ.
- Đoạn mã truy tìm có nhiệm quét trong bộ nhớ, từ địa chỉ 0 đến 0x80000000 để tìm những thành phần dữ liệu phân nhỏ dựa vào các byte đánh dấu. Và ghép các phần này lại trên một vùng nhớ nào đó theo thứ tự. Kết thúc việc tìm kiếm, điều khiển chương trình sẽ nhảy đến vùng nhớ đó và thực thi shellcode đã được lắp ghép. Tuy nhiên, việc duyệt bộ nhớ lần lượt như thế sẽ liên tục xảy ra eception khi đọc đến các vùng nhớ không hợp lệ. Vì thế, trước khi bắt đầu công việc dò tìm, đoạn mã hunting cần phải cài đặt một exception handler để chuyển đến duyệt khối nhớ tiếp theo khi gặp exception.
- Là một loại egg-hunt shellcode được viết bởi Berend-Jan Wever.
- Gồm 3 phần ( như nguyên lý đã nêu) :
_Create SEH Handler : Lấy địa chỉ của đoạn mã trên ghi vào vị trí của handler trên FS:[0] .
_Scan_Loop : Vòng lặp tìm kiếm các phần của shellcode và đặt vào đáy stack ( FS:[8]) và call tới đó khi kết thúc vòng lặp.
Cụ thể, dưới đây là toàn bộ mã asembly của Omelet Shellcode :
Mã:
BITS 32
; egg:
; LL II M1 M2 M3 DD DD DD ... (LL * DD)
; LL == Size of eggs (same for all eggs)
; II == Index of egg (different for each egg)
; M1,M2,M3 == Marker byte (same for all eggs)
; DD == Data in egg (different for each egg)
marker equ 0x280876
egg_size equ 0x3
max_index equ 0x5
start:
XOR EDI, EDI
jmp SHORT reset_stack
create_SEH_handler:
PUSH ECX ; SEH_frames[0].nextframe == 0xFFFFFFFF
MOV [FS:EAX], ESP ; SEH_chain -> SEH_frames[0]
CLD ; SCAN memory upwards from 0
scan_loop:
MOV AL, egg_size ; EAX = egg_size
egg_size_location equ $-1 - $$
REPNE SCASB ; Find the first byte
PUSH EAX ; Save egg_size
MOV ESI, EDI
LODSD ; EAX = II M2 M3 M4
XOR EAX, (marker SEH_frames[X]
find_last_SEH_loop:
MOV ESP, ECX ; ESP = SEH_frames[X]
POP ECX ; ECX = SEH_frames[X].next_frame
CMP ECX, 0xFFFFFFFF ; SEH_frames[X].next_frame == none ?
JNE find_last_SEH_loop ; No "X -= 1", check next frame
POP EDX ; EDX = SEH_frames[0].handler
CALL create_SEH_handler ; SEH_frames[0].handler == SEH_handler
SEH_handler:
POPA ; ESI = [ESP + 4] -> struct exception_info
LEA ESP, [BYTE ESI+0x18] ; ESP = struct exception_info->exception_address
POP EAX ; EAX = exception address 0x????????
OR AX, 0xFFF ; EAX = 0x?????FFF
INC EAX ; EAX = 0x?????FFF + 1 -> next page
Chỉnh sửa lần cuối bởi người điều hành: