Introduction
This blog tells the story of a failed Samba exploitation attempt. The goal was to assess what it would take for an adversary to weaponize publicly disclosed vulnerabilities in Samba. The targeted bugs were an info leak and a use-after-free flaw that when combined, seemed like a good candidate for building a reliable exploit. However, while trying to leverage these bugs for code execution, several obstacles were encountered, which in the end hindered successful exploitation. While this was not the expected outcome when we started looking into the bugs, and despite the fact that unsuccessful exploitation attempts usually remain untold, we decided to publish our analysis because this exercise resulted in some valuable takeaways. At the very least we were once again reminded that the phrase, "the devil's in the details," aptly applies to the exploitation of memory corruption bugs.
Targeted Software
We chose to target Ubuntu 17.10 since it's a rather mainstream distro and therefore a realistic target for an adversary. To make exploitation of memory corruption vulnerabilities harder, Ubuntu makes use of several compile-time hardening flags such as -DFORTIFY_SOURCE=2, -z norelro, and -PIE for most of its binaries. Since the memory leak described in CVE-2017-15275 did not seem to be exploitable, the Samba version we targeted was downgraded to samba_4.6.7+dfsg-1ubuntu2_amd64 in order to use the memory leak documented as CVE-2017-12163.
Memory Leak (CVE-2017-12163)
Bug Overview
The fix for this bug is pretty straightforward and involves the SMBv1 SMB_COM_WRITE command (among other vulnerable ones), which is used to write bytes to files on the server.
The patch is as follows:
@@ -4783,6 +4803,7 @@ void reply_write(struct smb_request *req)
{
connection_struct *conn = req->conn;
size_t numtowrite;
+ size_t remaining;
ssize_t nwritten = -1;
off_t startpos;
const char *data;
@@ -4823,6 +4843,17 @@ void reply_write(struct smb_request *req)
numtowrite = SVAL(req->vwv+1, 0);
startpos = IVAL_TO_SMB_OFF_T(req->vwv+2, 0);
data = (const char *)req->buf + 3;
+ /*
+ * Ensure client isn't asking us to write more than
+ * they sent. CVE-2017-12163.
+ */
+ remaining = smbreq_bufrem(req, data);
+ if (numtowrite > remaining) {
+ reply_nterror(req, NT_STATUS_INVALID_PARAMETER);
+ END_PROFILE(SMBwrite);
+ return;
+ }
+
The underlying problem is that the length of the data to be written (numtowrite) was directly taken from the attacker's SMB request, without ensuring that the request actually holds enough bytes. Therefore, simply setting numtowrite to a value larger than the data carried in the SMB request allowed you to copy parts of the heap into the targeted file. Subsequently, reading the created file's contents allows you to retrieve the leaked data.
Talloc 101
As described in talloc's documentation:
talloc is a hierarchical, reference counted memory pool system with destructors.
It is the core memory allocator used in Samba.
Due to the way the talloc memory allocator is designed, no special crafting is required to retrieve interesting data, which will be critical for the exploitation. Talloc is built on top of the glibc malloc allocator and simply adds a talloc_chunk header structure in front of allocated chunks.
The talloc_chunk structure is defined as follows in lib/talloc/talloc.c:
struct talloc_chunk {
/*
* flags includes the talloc magic, which is randomised to
* make overwrite attacks harder
*/
unsigned flags;
/*
* If you have a logical tree like:
*
* <parent>
* / | \
* / | \
* / | \
* <child 1> <child 2> <child 3>
*
* The actual talloc tree is:
*
* <parent>
* |
* <child 1> - <child 2> - <child 3>
*
* The children are linked with next/prev pointers, and
* child 1 is linked to the parent with parent/child
* pointers.
*/
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;
struct talloc_memlimit *limit;
struct talloc_pool_hdr *pool;
};
Several fields are interesting from an exploitation perspective. In particular, the destructor function pointer, which, if overwritten, gives the attacker a powerful primitive to take control of the execution flow. Since pretty much every chunk in the Samba process is allocated with the talloc allocator, an attacker can almost blindly get control of the instruction pointer by overwriting an adjacent chunk and hoping that it gets freed before the process crashes. As noted in the comments above the flags field though, the field is XORed with a random magic value to prevent this technique. However, this is nothing that a good memory leak can't get around.
Speaking of which, exploiting the memory leak vulnerability described in the previous section allows you to reliably retrieve a talloc_chunk structure adjacent to the memory of the vulnerable SMB_COM_WRITE request. Under normal circumstances, no prior memory crafting is required to do so, as these structures are omnipresent on the heap. The value of the name field from such a leaked talloc_chunk is never NULL, contrary to the optional destructor. The name field points to a hardcoded string in the libsmbd-base.so library and is used for debugging purposes. It therefore allows an attacker to derive the library's base address. In addition, the parent and next pointers allow you to get a sense of where the heap is located — taking care of two birds with one stone:
$ ./leak.py
<*> heap address: 0x55f7384fe0d0
<*> .text address: 0x7faccca48572
<*> libsmbd-base @ 0x7faccc81d000
Use-After-Free (CVE-2017-14746)
SMBv1 Request Handling
In order to get an understanding of the vulnerability, we first have to understand how SMBv1 commands are processed. When a packet is received by Samba, the process_smb() function is entered:
1941 /****************************************************************************
1942 Process an smb from the client
1943 ****************************************************************************/
1944 static void process_smb(struct smbXsrv_connection *xconn,
1945 uint8_t *inbuf, size_t nread, size_t unread_bytes,
1946 uint32_t seqnum, bool encrypted,
1947 struct smb_perfcount_data *deferred_pcd)
1948 {
...
1983 /* Make sure this is an SMB packet. smb_size contains NetBIOS header
1984 * so subtract 4 from it. */
1985 if ((nread < (smb_size - 4)) || !valid_smb_header(inbuf)) {
1986 DEBUG(2,("Non-SMB packet of length %d. Terminating server\n",
1987 smb_len(inbuf)));
...
1998
1999 exit_server_cleanly("Non-SMB packet");
2000 }
2001
2002 show_msg((char *)inbuf);
2003
2004 if ((unread_bytes == 0) && smb1_is_chain(inbuf)) {
2005 construct_reply_chain(xconn, (char *)inbuf, nread,
2006 seqnum, encrypted, deferred_pcd);
2007 } else {
2008 construct_reply(xconn, (char *)inbuf, nread, unread_bytes,
2009 seqnum, encrypted, deferred_pcd);
2010 }
...
After enforcing minimal SMB request size and header validity, the function will call the smb1_is_chain() function to verify whether multiple SMB commands are chained inside the received packet:
2267 bool smb1_is_chain(const uint8_t *buf)
2268 {
2269 uint8_t cmd, wct, andx_cmd;
2270
2271 cmd = CVAL(buf, smb_com);
2272 if (!is_andx_req(cmd)) {
2273 return false;
2274 }
2275 wct = CVAL(buf, smb_wct);
2276 if (wct < 2) {
2277 return false;
2278 }
2279 andx_cmd = CVAL(buf, smb_vwv);
2280 return (andx_cmd != 0xFF);
2281 }
Only the following SMB commands can be chained: SMBtconX, SMBlockingX, SMBopenX, SMBreadX, SMBwriteX, SMBsesssetupX, SMBulogoffX, SMBntcreateX. An exception to this is the very last command contained in a chain, which can be chosen arbitrarily.
Chained Requests (And_X requests)
In case the SMBtconX command is issued, the server will proceed to call construct_reply_chain() to handle the chained request:
1777 static void construct_reply_chain(struct smbXsrv_connection *xconn,
1778 char *inbuf, int size, uint32_t seqnum,
1779 bool encrypted,
1780 struct smb_perfcount_data *deferred_pcd)
1781 {
1782 struct smb_request **reqs = NULL;
1783 struct smb_request *req;
1784 unsigned num_reqs;
1785 bool ok;
1786
1787 ok = smb1_parse_chain(xconn, (uint8_t *)inbuf, xconn, encrypted,
1788 seqnum, &reqs, &num_reqs);
...
1800
1801 req = reqs<0>;
1802 req->inbuf = (uint8_t *)talloc_move(reqs, &inbuf);
1803
1804 req->conn = switch_message(req->cmd, req);
1805
1806 if (req->outbuf == NULL) {
1807 /*
1808 * Request has suspended itself, will come
1809 * back here.
1810 */
1811 return;
1812 }
1813 smb_request_done(req);
1814 }
The smb1_parse_chain() function will parse and perform sanity checks on the SMB query and return an array of individual struct smb_request objects in the reqs array. Afterwards, switch_message() will process the very first request of the chain, and if successful, the function smb_request_done() will take care of the remaining requests of the chain:
1816 /*
1817 * To be called from an async SMB handler that is potentially chained
1818 * when it is finished for shipping.
1819 */
1820
1821 void smb_request_done(struct smb_request *req)
1822 {
1823 struct smb_request **reqs = NULL;
1824 struct smb_request *first_req;
1825 size_t i, num_reqs, next_index;
1826 NTSTATUS status;
1827
...
1832
1833 reqs = req->chain;
1834 num_reqs = talloc_array_length(reqs);
1835
1836 for (i=0; i<num_reqs; i++) {
1837 if (reqs<i> == req) {
1838 break;
1839 }
1840 }
...
1848 next_index = i+1;
1849
1850 while ((next_index < num_reqs) && (IVAL(req->outbuf, smb_rcls) == 0)) {
1851 struct smb_request *next = reqs<next_index>;
1852 struct smbXsrv_tcon *tcon;
1853 NTTIME now = timeval_to_nttime(&req->request_time);
1854
1855 next->vuid = SVAL(req->outbuf, smb_uid);
1856 next->tid = SVAL(req->outbuf, smb_tid);
1857 status = smb1srv_tcon_lookup(req->xconn, req->tid,
1858 now, &tcon);
1859 if (NT_STATUS_IS_OK(status)) {
1860 req->conn = tcon->compat;
1861 } else {
1862 req->conn = NULL;
1863 }
1864 next->chain_fsp = req->chain_fsp;
1865 next->inbuf = req->inbuf;
1866
1867 req = next;
1868 req->conn = switch_message(req->cmd, req);
1869
1870 if (req->outbuf == NULL) {
1871 /*
1872 * Request has suspended itself, will come
1873 * back here.
1874 */
1875 return;
1876 }
1877 next_index += 1;
1878 }
...
The function's while loop will iterate over the chain's SMB requests and pass each of them to the function switch_message(), which takes care of the actual command processing.
Bug Overview
With the SMBv1 chained request handling in mind, we examine the fix for CVE-2017-14746, which touches two different parts of Samba's source code. The first part is the most relevant for comprehending the bug:
@@ -1855,12 +1855,13 @@ void smb_request_done(struct smb_request *req)
next->vuid = SVAL(req->outbuf, smb_uid);
next->tid = SVAL(req->outbuf, smb_tid);
- status = smb1srv_tcon_lookup(req->xconn, req->tid,
+ status = smb1srv_tcon_lookup(req->xconn, next->tid,
now, &tcon);
+
if (NT_STATUS_IS_OK(status)) {
- req->conn = tcon->compat;
+ next->conn = tcon->compat;
} else {
- req->conn = NULL;
+ next->conn = NULL;
}
next->chain_fsp = req->chain_fsp;
next->inbuf = req->inbuf;
The patch makes sure that the next->conn field is updated rather than req->conn, both of which hold a pointer to a connection_struct. With the fact in mind that this is supposed to be a use-after-free bug, let's consider the whole while loop in smb_request_done() where the fix is applied:
1816 /*
1817 * To be called from an async SMB handler that is potentially chained
1818 * when it is finished for shipping.
1819 */
1820
1821 void smb_request_done(struct smb_request *req)
1822 {
1823 struct smb_request **reqs = NULL;
1824 struct smb_request *first_req;
1825 size_t i, num_reqs, next_index;
1826 NTSTATUS status;
1827
...
1850 while ((next_index < num_reqs) && (IVAL(req->outbuf, smb_rcls) == 0)) {
1851 struct smb_request *next = reqs<next_index>;
1852 struct smbXsrv_tcon *tcon;
...
1857 status = smb1srv_tcon_lookup(req->xconn, req->tid,
1858 now, &tcon);
1859 if (NT_STATUS_IS_OK(status)) {
1860 req->conn = tcon->compat;
1861 } else {
1862 req->conn = NULL;
1863 }
...
1867 req = next;
1868 req->conn = switch_message(req->cmd, req);
1869
1870 if (req->outbuf == NULL) {
1871 /*
1872 * Request has suspended itself, will come
1873 * back here.
1874 */
1875 return;
1876 }
1877 next_index += 1;
...
As mentioned earlier, the patch makes sure that the field next->conn is updated rather than req->conn. The problem addressed by the patch becomes somewhat clearer when looking at line 1867, in which next is assigned to req, which is passed to the immediately following call to switch_message(). In case next->conn has been freed earlier, further processing in switch_message() may therefore access a dangling connection_struct pointer.
The second part of the fix helps in finding the function which allows us to trigger the suspected use-after-free on the next->conn field:
@@ -923,6 +923,11 @@ void reply_tcon_and_X(struct smb_request *req)
}
TALLOC_FREE(tcon);
+ /*
+ * This tree id is gone. Make sure we can't re-use it
+ * by accident.
+ */
+ req->tid = 0;
}
if ((passlen > MAX_PASS_LEN) || (passlen >= req->buflen)) {
The reply_tcon_and_X is an and_X request, which can be chained, so this looks promising. The code of the reply_tcon_and_X() function is as follows:
865 void reply_tcon_and_X(struct smb_request *req)
866 {
867 connection_struct *conn = req->conn;
...
897 /* we might have to close an old one */
898 if ((tcon_flags & TCONX_FLAG_DISCONNECT_TID) && conn) {
899 struct smbXsrv_tcon *tcon;
900 NTSTATUS status;
901
902 tcon = conn->tcon;
903 req->conn = NULL;
904 conn = NULL;
905
906 /*
907 * TODO: cancel all outstanding requests on the tcon
908 */
909 status = smbXsrv_tcon_disconnect(tcon, req->vuid);
910 if (!NT_STATUS_IS_OK(status)) {
911 DEBUG(0, ("reply_tcon_and_X: "
912 "smbXsrv_tcon_disconnect() failed: %s\n",
913 nt_errstr(status)));
914 /*
915 * If we hit this case, there is something completely
916 * wrong, so we better disconnect the transport connection.
917 */
918 END_PROFILE(SMBtconX);
919 exit_server(__location__ ": smbXsrv_tcon_disconnect failed");
920 return;
921 }
922
923 TALLOC_FREE(tcon);
924 }
...
COM_TREE_CONNECT_ANDX command in an SMBv1 chained request, with the TCONX_FLAG_DISCONNECT_TID flag set, leads to the smbXsrv_tcon_disconnect() function being called:
891 NTSTATUS smbXsrv_tcon_disconnect(struct smbXsrv_tcon *tcon, uint64_t vuid)
892 {
...
968 if (tcon->compat) {
969 bool ok;
970
971 ok = set_current_service(tcon->compat, 0, true);
972 if (!ok) {
973 status = NT_STATUS_INTERNAL_ERROR;
974 DEBUG(0, ("smbXsrv_tcon_disconnect(0x%08x, '%s'): "
975 "set_current_service() failed: %s\n",
976 tcon->global->tcon_global_id,
977 tcon->global->share_name,
978 nt_errstr(status)));
979 tcon->compat = NULL;
980 return status;
981 }
982
983 close_cnum(tcon->compat, vuid);
984 tcon->compat = NULL;
985 }
...
Inside the function, close_cnum() is called with tcon->compat as its first parameter, which happens to point to the same connection_struct object as next->conn in the switch_message() while loop. Within close_cnum(), the passed connection_struct pointer is freed:
1084 void close_cnum(connection_struct *conn, uint64_t vuid)
1085 {
...
1136
1137 conn_free(conn);
1138 }
We can see that after the call to close_cnum(), the tcon->compat pointer is set to NULL in smbXsrv_tcon_disconnect. However, the next->conn pointer is not updated. The problem actually lies in the way connection_struct pointers are created, as well as chained requests. Indeed, when successfully connecting to an SMB share, by issuing a COM_TCON_AND_X command (TCON stands for Tree Connect), a connection_struct is created and associated with a unique tid (Tree ID). This tid is then reused in all subsequent SMBv1 commands involving the same share, e.g. for opening, writing and deleting files. A connection to an SMB share is dropped when the COM_TCON_AND_X command is invoked with the TCONX_FLAG_DISCONNECT_TID flag being set, leading to the connection_struct being freed.
When performing this operation in the context of a chained SMBv1 request, the following code is run when the request array is created inside smb1_parse_chain():
2480 bool smb1_parse_chain(TALLOC_CTX *mem_ctx, const uint8_t *buf,
2481 struct smbXsrv_connection *xconn,
2482 bool encrypted, uint32_t seqnum,
2483 struct smb_request ***reqs, unsigned *num_reqs)
2484 {
2485 struct smbd_server_connection *sconn = NULL;
...
2502 if (!smb1_walk_chain(buf, smb1_parse_chain_cb, &state)) {
2503 TALLOC_FREE(state.reqs);
2504 return false;
2505 }
...
2512 }
The smb1_walk_chain() function will take care of creating an array of struct smb_request objects out of the chained request. Each of those objects will then be handed over to smb1_parse_chain_cb() for initialization:
2441 static bool smb1_parse_chain_cb(uint8_t cmd,
2442 uint8_t wct, const uint16_t *vwv,
2443 uint16_t num_bytes, const uint8_t *bytes,
2444 void *private_data)
2445 {
2446 struct smb1_parse_chain_state *state =
2447 (struct smb1_parse_chain_state *)private_data;
2448 struct smb_request **reqs;
2449 struct smb_request *req;
2450 bool ok;
2451
2452 reqs = talloc_realloc(state->mem_ctx, state->reqs,
2453 struct smb_request *, state->num_reqs+1);
2454 if (reqs == NULL) {
2455 return false;
2456 }
2457 state->reqs = reqs;
2458
2459 req = talloc(reqs, struct smb_request);
2460 if (req == NULL) {
2461 return false;
2462 }
2463
2464 ok = init_smb_request(req, state->sconn, state->xconn, state->buf, 0,
2465 state->encrypted, state->seqnum);
...
2478 }
After reallocating the reqs array to store an additional struct smb_request pointer, req is initialized in init_smb_request():
584 static bool init_smb_request(struct smb_request *req,
585 struct smbd_server_connection *sconn,
586 struct smbXsrv_connection *xconn,
587 const uint8_t *inbuf,
588 size_t unread_bytes, bool encrypted,
589 uint32_t seqnum)
590 {
591 struct smbXsrv_tcon *tcon;
...
606 req->cmd = CVAL(inbuf, smb_com);
607 req->flags2 = SVAL(inbuf, smb_flg2);
608 req->smbpid = SVAL(inbuf, smb_pid);
609 req->mid = (uint64_t)SVAL(inbuf, smb_mid);
610 req->seqnum = seqnum;
611 req->vuid = SVAL(inbuf, smb_uid);
612 req->tid = SVAL(inbuf, smb_tid);
613 req->wct = CVAL(inbuf, smb_wct);
614 req->vwv = (const uint16_t *)(inbuf+smb_vwv);
615 req->buflen = smb_buflen(inbuf);
616 req->buf = (const uint8_t *)smb_buf_const(inbuf);
617 req->unread_bytes = unread_bytes;
618 req->encrypted = encrypted;
619 req->sconn = sconn;
620 req->xconn = xconn;
621 req->conn = NULL;
622 if (xconn != NULL) {
623 status = smb1srv_tcon_lookup(xconn, req->tid, now, &tcon);
624 if (NT_STATUS_IS_OK(status)) {
625 req->conn = tcon->compat;
626 }
627 }
...
The function initializes all required fields of the structure and calls smb1srv_tcon_lookup() to look up an existing connection corresponding to the struct's req->tid field. In case a connection is found, its connection_struct pointer is assigned to the request's conn field, which will eventually become the value of next->conn in the while loop of switch_message() when dispatching chained SMBv1 requests.
Even if the vulnerable path involves a lot of back and forth between files and functions, triggering the bug is rather easy:
- Perform a COM_TREE_CONNECT_ANDX request to connect to a share and retrieve an associated tid.
- Perform a chained SMBv1 query with another COM_TREE_CONNECT_ANDX request having the TCONX_FLAG_DISCONNECT_TID flag set, in order to trigger the use-after-free and have an additional chained command to use the dangling next->conn pointer.
However, when doing that in practice, things don't go as smoothly as planned. Right after triggering the deallocation of the conn pointer inside reply_tcon_and_X(), a new connection_struct object is allocated a few function calls later in make_connection(), ending up reallocating the dangling pointer:
865 void reply_tcon_and_X(struct smb_request *req)
866 {
867 connection_struct *conn = req->conn;
...
897 /* we might have to close an old one */
898 if ((tcon_flags & TCONX_FLAG_DISCONNECT_TID) && conn) {
899 struct smbXsrv_tcon *tcon;
900 NTSTATUS status;
901
902 tcon = conn->tcon;
903 req->conn = NULL;
904 conn = NULL;
905
906 /*
907 * TODO: cancel all outstanding requests on the tcon
908 */
909 status = smbXsrv_tcon_disconnect(tcon, req->vuid);
...
1055 conn = make_connection(req, now, service, client_devicetype,
1056 req->vuid, &nt_status);
1057 req->conn =conn;
...
Therefore, merely triggering the use-after-free does not lead to a crash of the Samba server, as the newly created connection_struct object is allocated in the exact same memory location as the previous one, before being reused. Running Samba with AddressSanitizer enabled makes the problem apparent:
=================================================================
==10974==ERROR: AddressSanitizer: heap-use-after-free on address 0x613000006f80 at pc 0x7f66c4602153 bp 0x7ffdfe434e40 sp 0x7ffdfe434e30
READ of size 4 at 0x613000006f80 thread T0
#0 0x7f66c4602152 in switch_message (/home/<...>/samba-install/lib/private/libsmbd-base-samba4.so+0x526152)
#1 0x7f66c46057b5 in smb_request_done (/home/<...>/samba-install/lib/private/libsmbd-base-samba4.so+0x5297b5)
#2 0x7f66c4607beb in process_smb (/home/<...>/samba-install/lib/private/libsmbd-base-samba4.so+0x52bbeb)
#3 0x7f66c460aa93 in smbd_server_connection_read_handler (/home/<...>/samba-install/lib/private/libsmbd-base-samba4.so+0x52ea93)
#4 0x7f66c460affa in smbd_server_connection_handler (/home/<...>/samba-install/lib/private/libsmbd-base-samba4.so+0x52effa)
#5 0x7f66c38bdcfe in epoll_event_loop_once (/home/<...>/samba-install/lib/private/libtevent.so.0+0x1ccfe)
#6 0x7f66c38b6c57 in std_event_loop_once (/home/<...>/samba-install/lib/private/libtevent.so.0+0x15c57)
#7 0x7f66c38ab10f in _tevent_loop_once (/home/<...>/samba-install/lib/private/libtevent.so.0+0xa10f)
#8 0x7f66c38ab7d4 in tevent_common_loop_wait (/home/<...>/samba-install/lib/private/libtevent.so.0+0xa7d4)
#9 0x7f66c38b6b6c in std_event_loop_wait (/home/<...>/samba-install/lib/private/libtevent.so.0+0x15b6c)
#10 0x7f66c38ab885 in _tevent_loop_wait (/home/<...>/samba-install/lib/private/libtevent.so.0+0xa885)
#11 0x7f66c460e38f in smbd_process (/home/<...>/samba-install/lib/private/libsmbd-base-samba4.so+0x53238f)
#12 0x55d7fa7510e6 in smbd_accept_connection (/home/<...>/samba-install/sbin/smbd+0x1c0e6)
#13 0x7f66c38bdcfe in epoll_event_loop_once (/home/<...>/samba-install/lib/private/libtevent.so.0+0x1ccfe)
#14 0x7f66c38b6c57 in std_event_loop_once (/home/<...>/samba-install/lib/private/libtevent.so.0+0x15c57)
#15 0x7f66c38ab10f in _tevent_loop_once (/home/<...>/samba-install/lib/private/libtevent.so.0+0xa10f)
#16 0x7f66c38ab7d4 in tevent_common_loop_wait (/home/<...>/samba-install/lib/private/libtevent.so.0+0xa7d4)
#17 0x7f66c38b6b6c in std_event_loop_wait (/home/<...>/samba-install/lib/private/libtevent.so.0+0x15b6c)
#18 0x7f66c38ab885 in _tevent_loop_wait (/home/<...>/samba-install/lib/private/libtevent.so.0+0xa885)
#19 0x55d7fa7547e8 in main (/home/<...>/samba-install/sbin/smbd+0x1f7e8)
#20 0x7f66bff37f69 in __libc_start_main (/usr/lib/libc.so.6+0x20f69)
#21 0x55d7fa746e09 in _start (/home/<...>/samba-install/sbin/smbd+0x11e09)
0x613000006f80 is located 256 bytes inside of 336-byte region <0x613000006e80,0x613000006fd0)
freed by thread T0 here:
#0 0x7f66c556f711 in __interceptor_free /build/gcc-multilib/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:45
#1 0x7f66c02d2c93 in _talloc_free (/usr/lib/libtalloc.so.2+0x3c93)
previously allocated by thread T0 here:
#0 0x7f66c556fae9 in __interceptor_malloc /build/gcc-multilib/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:62
#1 0x7f66c02d4f71 in _talloc_zero (/usr/lib/libtalloc.so.2+0x5f71)
SUMMARY: AddressSanitizer: heap-use-after-free (/home/<...>/samba-install/lib/private/libsmbd-base-samba4.so+0x526152) in switch_message
Shadow bytes around the buggy address:
0x0c267fff8da0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c267fff8db0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c267fff8dc0: 00 00 fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c267fff8dd0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0c267fff8de0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
=>0x0c267fff8df0:fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa
0x0c267fff8e00: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x0c267fff8e10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c267fff8e20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c267fff8e30: 00 00 fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c267fff8e40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==10974==ABORTING
Exploitation Hurdles
We will now discuss the problems faced when trying to exploit this bug, and why we finally gave up trying to build a working exploit. Surmounting all the problems we faced would require a significant amount of work, and would probably result in a loss of reliability.
Time Window
The time window between the connection_struct object being freed and reallocated doesn't allow the attacker to reallocate the freed object with arbitrary data. Instead, the vulnerable object is claimed by the subsequent connection_struct allocation in the make_connection() call:
void reply_tcon_and_X(struct smb_request *req)
{
...
/* we might have to close an old one */
if ((tcon_flags & TCONX_FLAG_DISCONNECT_TID) && conn) {
struct smbXsrv_tcon *tcon;
NTSTATUS status;
tcon = conn->tcon;
req->conn = NULL;
conn = NULL;
/*
* TODO: cancel all outstanding requests on the tcon
*/
status = smbXsrv_tcon_disconnect(tcon, req->vuid); // <1> Our vulnerable object is freed here
...
}
...
p += srvstr_pull_req_talloc(ctx, req, &path, p, STR_TERMINATE);
if (path == NULL) {
reply_nterror(req, NT_STATUS_INVALID_PARAMETER);
END_PROFILE(SMBtconX);
return;
}
...
conn = make_connection(req, now, service, client_devicetype, // <2> The vulnerable object is reallocated here
req->vuid, &nt_status);
req->conn =conn;
...
}
It might be possible to make use of the path variable allocation made by calling the srvstr_pull_req_talloc(), which reads bytes from the attacker's request until a null byte is encountered or it reaches the end of the request. That would potentially allow reclaiming the vulnerable connection_struct object before the make_connection() function call. However, the path variable only allows for null terminated strings. This means that it's impossible to craft 64-bit userland pointers with this method, which is crucial for exploitation. Moreover, the path cannot be arbitrary, since it must be a valid IPC name or share.
A way to avoid this problem is to play with the heap before triggering the use-after-free bug. As an allocation primitive, the SMB_COM_READ_ANDX command was used, which allows you to allocate arbitrarily sized chunks of memory with arbitrary data. Using this command, the heap was first defragmented, followed by an allocation of a 0x1000 bytes block:
+------------------------------------------------+
| || any |
| 0x1000 bytes block || allocated|
| || block |
+------------------------------------------------+
When the chained request processing is finished, the 0x1000 bytes block is freed:
+------------------------------------------------+
| || any |
| 0x1000 bytes block (Free) || allocated|
| || block |
+------------------------------------------------+
A subsequent chained request is then performed with another SMB_COM_READ_ANDX request, with a read size of 0xea0, followed by an SMB_COM_TCON_ANDX request. The 0xea0 request will be allocated in place of the previously freed 0x1000 bytes block, splitting it in two, with a remainder chunk of 0x160 bytes:
+------------------------+-----------------------+
| 0xea0 bytes | 0x160 || any |
| (Allocated) | bytes || allocated|
| | (free) || block |
+------------------------+-----------------------+
The subsequent SMB_COM_TCON_ANDX request will then proceed to allocate a new connection_struct in the 0x160 bytes hole, created by the previous block splitting:
+------------------------+-----------------------+
| 0xea0 bytes | 0x160 || any |
| (Allocated) | bytes || allocated|
| | (conn) || block |
+------------------------+-----------------------+
Once again the 0xea0 bytes block is freed as part of the chained request memory being claimed, after its processing:
+------------------------+-----------------------+
| 0xea0 bytes | 0x160 || any |
| (Free) | bytes || allocated|
| | (conn) || block |
+------------------------+-----------------------+
A last SMB_COM_TCON_ANDX query with the disconnect flag is sent. When the connection_struct is freed, forward coalescing will occur with the 0xea0 bytes block, creating a 0x1000 bytes free block. Since this is no longer the best fit free block to satisfy the subsequent connection_struct allocation requested by the make_connection() function, the new conn pointer will be reallocated somewhere else. This opens the possibility of reallocating the vulnerable conn object to take control of the execution flow. However, a second problem needs to be addressed.
Vulnerable Object Life Cycle and Validity
The vulnerable object is only valid in the context of the following while loop:
1816 /*
1817 * To be called from an async SMB handler that is potentially chained
1818 * when it is finished for shipping.
1819 */
1820
1821 void smb_request_done(struct smb_request *req)
1822 {
1823 struct smb_request **reqs = NULL;
1824 struct smb_request *first_req;
1825 size_t i, num_reqs, next_index;
1826 NTSTATUS status;
1827
...
1850 while ((next_index < num_reqs) && (IVAL(req->outbuf, smb_rcls) == 0)) {
1851 struct smb_request *next = reqs<next_index>;
1852 struct smbXsrv_tcon *tcon;
...
1857 status = smb1srv_tcon_lookup(req->xconn, req->tid,
1858 now, &tcon);
1859 if (NT_STATUS_IS_OK(status)) {
1860 req->conn = tcon->compat;
1861 } else {
1862 req->conn = NULL;
1863 }
...
1867 req = next;
1868 req->conn = switch_message(req->cmd, req);
1869
1870 if (req->outbuf == NULL) {
1871 /*
1872 * Request has suspended itself, will come
1873 * back here.
1874 */
1875 return;
1876 }
1877 next_index += 1;
...
This has the following implications:
- We can only use a single chained request to trigger and exploit the use-after-free bug.
- We can only use chainable commands (And_X) to do so, except for the last command in the chain, which may be a non And_X.
The idea here would be to simply use an SMB_COM_READ_ANDX command again to take control over the freed object's memory. However, the program crashes before reaching the handler for the SMB_COM_READ_ANDX request:
Program received signal SIGSEGV, Segmentation fault.
<----------------------------------registers----------------------------------->
RAX: 0x7fde7dbf2dd8 --> 0x7fde7dbf2dc8 --> 0x7fde7dbf2db8 --> 0x7fde7dbf2da8 --> 0x7fde7dbf2d98 --> 0x7fde7dbf2d88 (--> ...)
RBX: 0x564d11e7ad40 --> 0x564d11e6fde8 --> 0x564d11e77b30 --> 0x6e6164726f6a ('jordan')
RCX: 0x0
RDX: 0x0
RSI: 0x7134 ('4q')
RDI: 0x40 ('@')
RBP: 0x7134 ('4q')
RSP: 0x7ffda9c3e9e8 --> 0x7fde80e3b26f (<change_to_user+31>: mov rdx,QWORD PTR # 0x7fde81281840)
RIP: 0x7fde80df9569 (<get_valid_user_struct_internal+9>: mov r8,QWORD PTR )
R8 : 0x7ffda9c3e9c0 --> 0x564d11e13720 --> 0x564d11e010f0 --> 0x564d11e1c4c0 --> 0x7fde7b229940 (<db_rbt_fetch_locked>: push r12)
R9 : 0x564d11e551f5 --> 0x4d11e548f0000000
R10: 0x13
R11: 0x1
R12: 0x7dbf2dc8
R13: 0x2e ('.')
R14: 0x7134 ('4q')
R15: 0x564d11e78280 --> 0x46c84801002e
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
<-------------------------------------code------------------------------------->
0x7fde80df955c: nop DWORD PTR
0x7fde80df9560 <get_valid_user_struct_internal>: test rsi,rsi
0x7fde80df9563 <get_valid_user_struct_internal+3>: je 0x7fde80df9670 <get_valid_user_struct_internal+272>
=> 0x7fde80df9569 <get_valid_user_struct_internal+9>: mov r8,QWORD PTR
0x7fde80df956c <get_valid_user_struct_internal+12>: test r8,r8
0x7fde80df956f <get_valid_user_struct_internal+15>: je 0x7fde80df9670 <get_valid_user_struct_internal+272>
0x7fde80df9575 <get_valid_user_struct_internal+21>: mov rax,r8
0x7fde80df9578 <get_valid_user_struct_internal+24>: xor ecx,ecx
<------------------------------------stack------------------------------------->
0000| 0x7ffda9c3e9e8 --> 0x7fde80e3b26f (<change_to_user+31>: mov rdx,QWORD PTR # 0x7fde81281840)
0008| 0x7ffda9c3e9f0 --> 0x564d11e7ad40 --> 0x564d11e6fde8 --> 0x564d11e77b30 --> 0x6e6164726f6a ('jordan')
0016| 0x7ffda9c3e9f8 --> 0x564d11e190d0 (0x0000564d11e190d0)
0024| 0x7ffda9c3ea00 --> 0x2e ('.')
0032| 0x7ffda9c3ea08 --> 0x7fde80e6180c (<switch_message+348>: test al,al)
0040| 0x7ffda9c3ea10 --> 0x911e7fb70
0048| 0x7ffda9c3ea18 --> 0x1d3c5cd6eb4a1f0
0056| 0x7ffda9c3ea20 --> 0x564d11e71410 --> 0x424d53ff9b020000
<------------------------------------------------------------------------------>
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
get_valid_user_struct_internal (vuid=vuid@entry=0x7134, server_allocated=server_allocated@entry=server_allocated_state
SERVER_ALLOCATED_REQUIRED_YES, sconn=<optimized out>) at ../source3/smbd/password.c:55
55 ../source3/smbd/password.c: No such file or directory.
gdb-peda$ bt
#0 get_valid_user_struct_internal (vuid=vuid@entry=0x7134, server_allocated=server_allocated@entry=server_allocated_state
SERVER_ALLOCATED_REQUIRED_YES, sconn=<optimized out>) at ../source3/smbd/password.c:55
#1 0x00007fde80df968b in get_valid_user_struct (sconn=0x0, vuid=vuid@entry=0x7134) at ../source3/smbd/password.c:90
#2 0x00007fde80e3b26f in change_to_user (conn=conn@entry=0x564d11e7ad40, vuid=vuid@entry=0x7134) at ../source3/smbd/uid.c:378
#3 0x00007fde80e6180c in switch_message (type=<optimized out>, req=req@entry=0x564d11e78280) at ../source3/smbd/process.c:1610
...
The reason is that some pointers from the freed object are being reused in switch_message() before the next request handler is invoked. Under normal circumstances, this would probably not be a problem since, even though the structure is freed, it should still survive dereferencing pointers to the now freed chunks. However, right before actually freeing the connection_struct object, the whole structure content is zeroed-out in the conn_free_internal() function via the ZERO_STRUCTP macro:
148 static void conn_free_internal(connection_struct *conn)
149 {
...
174
175 ZERO_STRUCTP(conn);
176 talloc_destroy(conn);
177 }
Therefore, it doesn't seem to be possible to reallocate the vulnerable connection_struct object, since the program crashes on a NULL pointer dereference before having the chance to execute a handler that would allow doing so.
The only solution we can think of to address this problem is this: Groom the heap in such a way that some allocation (controlled or not), happening after the vulnerable object has been freed, ends up overlapping the vulnerable object with pointers that can be dereferenced safely, thus allowing the program to reach the next handler. However, this approach seems rather unreliable.
Conclusion
A common takeaway when failing to exploit a bug like this is the usual “not all bugs are created equal" and that stars don't always align in the right way. It was still an interesting exercise to assess an adversary's required technical level to turn these public bugs into an exploit. Moreover, triggering this vulnerability requires attackers to either target Samba servers allowing guest access, or have valid credentials. As far as exploiting an "old" CVE on an unpatched Samba server goes, the adversary will probably fall back to using CVE-2017-7494 which allows them to execute code contained in a shared library uploaded to a Samba server. The required technical knowledge is minimal in this case, as public exploits exist.
Finally,
to anybody who successfully exploited this bug, feel free to reach out to us, because we would be very interested in hearing how you pulled off the exploit.
Learn how CrowdStrike Services can help you stop intrusions now and in the future: /content/crowdstrike-www/locale-sites/us/en-us/services/