Shellcode

hnahkcad

W-------
22/10/2013
0
9 bài viết
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ố.

Shellcode.png

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’.
Số lượng ít ỏi các lệnh alphanumeric gây ra nhiều hạn chế cho việc viết một shellcode :
  • Không có lệnh mov.
Sử dụng các lệnh push,pop các thanh ghi để di chuyển dữ liệu thông qua stack. Ví dụ nếu muốn :
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…
_ Chúng ta có thể sử dụng các lệnh inc,dec để thay thế.

_ 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.
_ Ta có lệnh XOR để thực hiện điều này,ví dụ edx chứa giá trị cần lấy 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 :
1.png

Đâ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ý :
Để xây dựng được một AS có cấu trúc như trên, phương pháp mã hóa cần phải đảm bảo :

_ 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 ( InitializationPatcher) 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 :
_ Một byte bất kỳ (từ 0x00 đến 0xFF) luôn có thể xếp được vào một trong bốn categories trên. Vì thế, theo phương pháp này, mọi shellcode đầu vào (non-alphanumeric) đều có thể được mã hóa thành một AS hoàn chỉnh.

_ 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ỳ :
_ Chia thành hai số 4 bit : a ứng với 4 bit cao, b ứng với 4 bit thấp, như vậy a, b có thể mang giá trị từ 0 đến F.

_ 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 :
_ Mọi shellcode đầu vào đều có thể được mã hóa thành AS.

_ 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 :

[imath]+0: E8 00000000 CALL[/imath]+5 ; PUSH $+5 onto thestack
[imath]+5: 59 POP ECX ; ECX =[/imath]+5
$+6: ...shellcode...

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 ) :

[imath]+0 EB XX JMP SHORT[/imath]+N ; Jump to the call instruction
[imath]+5: 59 POP ECX ; ECX =[/imath]+N+5
$+6: ...shellcode...
[imath]+N: E8 FFFFFFXX CALL[/imath]+5 ; PUSH [imath]+N+5 onto the stack and jump back to[/imath]+5

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 :

[imath]+0 EB FFFFFFFF CALL[/imath]+4 ; PUSH [imath]+5 onto the stack and jump to[/imath]+4
$+5: C3 RETN ; Does not get executed like this; see below.
$+6: 59 POP ECX
$+7: … Shellcode...

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 :

$+4: FFC3 INC EBX ; Does nothing useful; can be considered a NOP
[imath]+6: 59 POP ECX ; ECX =[/imath]+5
$+7: ...shellcode...

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” :

[imath]+0 D9EE FLDZ ; Floating point stores[/imath]+0 in its environment
[imath]+2 D974E4 F4 FSTENV SS:[ESP-0xC] ; Save environment at ESP-0xC; now [ESP] =[/imath]+0
[imath]+6 59 POP ECX ; ECX =[/imath]+0

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
Gây exception : Chỉ cần một lệnh đơn giản, chẳng hạn thao tác đến một vùng nhớ không hợp lệ.

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.
2. Nguyên lý
  • 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.
3. Omelet Shellcode
  • 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) :
_SEH Handler : Đoạn mã xử lý exception, tăng trang nhớ để duyệt tiếp.

_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:
Mời các bạn tham gia Group WhiteHat để thảo luận và cập nhật tin tức an ninh mạng hàng ngày.
Lưu ý từ WhiteHat: Kiến thức an ninh mạng để phòng chống, không làm điều xấu. Luật pháp liên quan
Thẻ
shellcode
Bên trên