Trying to Dance the Samba: An Exercise in Weaponizing Vulnerabilities

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 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 [rip+0x4465ca]        # 0x7fde81281840)
RIP: 0x7fde80df9569 (<get_valid_user_struct_internal+9>:        mov    r8,QWORD PTR [rdi])
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 [rax+0x0]
   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 [rdi]
   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 [rip+0x4465ca]        # 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: https://www.crowdstrike.com/services/

Related Content