By Aleksandar Nikolich.

Executive summary

Cisco Talos recently discovered multiple vulnerabilities in macOS’s implementation of SMB server. An adversary could exploit these vulnerabilities to carry out a variety of malicious actions, including revealing sensitive information on the server, bypassing certain cryptographic checks, causing a denial of service or execute remote code on the targeted server. Cisco Talos worked with Apple to ensure that these issues are resolved and an update is available for affected customers, all in adherence to Cisco’s vulnerability disclosure policy. Users are encouraged to update to the latest macOS version as soon as possible to patch these vulnerabilities.

Background

SMB is among the most ubiquitous network protocols encountered in enterprise environments. It facilitates, among other things, basic file-sharing among workstations. Historically, it’s been a major security pain point, as some implementations contained high-severity vulnerabilities, such as EternalBlue.

The open-source Samba project provides support for SMB on non-Windows operating systems. This included old versions of Apple’s OS X. However, in 2011, the Samba project changed its license to GPL which made it incompatible with OS X. This prompted Apple to develop its own implementation dubbed “SmbX,” which has been included in every version of OS X and macOS ever since.

SmbX is a proprietary implementation of a protocol that has led to some well-known vulnerabilities. This makes it a very interesting target, especially with the growing presence of macOS machines in today’s network environments. We recently discovered several new vulnerabilities in macOS’ implementation of SMB. They are all on the post-authentication attack surface, which lowers their severity, but they are readily exploitable in environments with advanced authentication mechanisms.

An attacker could use these vulnerabilities to carry out a variety of malicious actions. Some of the more serious vulnerabilities could allow the attacker to execute arbitrary code on the victim server. We conducted this research on macOS Catalina, but things are mostly unchanged on the latest macOS version — Big Sur. Apple discovered TALOS-2021-1246 simultaneously and fixed it in Big Sur, but not Catalina.

Investigation

SMB support on macOS consists of separate client and server components, and while the client-side component is based on open-source implementation, the server side is not. This will be the focus of our research. The main server binary lives in `/usr/sbin/smbd` and can be started either through `launchd` or manually. The binary retains partial symbols, which made it easier to reverse-engineer these vulnerabilities:

SMB is technically a suite of protocols consisting of older SMB1 and newer SMB2 and SMB3 specifications, which focus on improving security. SmbX on macOS has full support for SMB1 and SMB2 — SMB3 support is limited to message-signing schemes and doesn’t  support newer features, such as compression. For the most part, SMB2 and SMB3 messages are processed by the same code. We focused on SMB2 for this research, as opposed to SMB1.

SMB is a session-based, stateful, protocol and specs define a specific sequence of message exchanges to perform authentication and other actions. As such, we need to differentiate between pre- and post-authentication attack surfaces. A pre-authentication attack surface is rather limited, but potential vulnerabilities would be more serious and, therefore, that code would warrant more scrutiny.

We want to investigate the code responsible for all SMB2 message types, so we should take a high-level overview of data flow and how each message is being processed in `smbd`. A little bit of reverse-engineering reveals the following code flow:

A new connection is accepted in `smb_transport_dispatch` which starts the handling of the outermost part of the received packet and proceeds to `smb_dispatch_request` which determines protocol versions and hands packet data off to, in the case of SMB2, `smb2_dispatch_compound`. Function `smb2_dispatch_compound` parses the SMB2 shell of the packet, which can be compound and contains more than one SMB2 message. It determines the type of message and dispatches it to corresponding `smb2_dispatch_*` function, such as `smb2_dispatch_tree_connect`, which then proceeds to fully parse the rest of the packet and act on it. Once the packet is fully processed and all possible replies are sent, this particular code path is done, and handling of the next packet begins in `smb_dispatch_request` again.

Testing and analysis

Naturally, we wished to perform fuzz testing against SmbX. Using SMB protocol specifications and the identified dispatch functions, we can easily delineate the pre-auth attack surface, which should be easier to fuzz with very little preparation and post-auth attack surface. The latter would require understanding of session state transitions as some states, and therefore some SMB commands, can only be reached if they are preceded by others.

The pre-authentication attack surface largely consists of the following dispatch functions and their callees:

  • smb2_dispatch_negotiate
  • smb2_dispatch_session_setup

While the post-authentication attack surface is much broader, we want to cover all of the following dispatch functions which have some complexity to them:

  • smb2_dispatch_write_file
  • smb2_dispatch_tree_connect
  • smb2_dispatch_read_file
  • smb2_dispatch_flush
  • smb2_dispatch_ioctl
  • smb2_dispatch_query_info
  • smb2_dispatch_query_directory
  • smb2_dispatch_set_file_information
  • smb2_dispatch_create_file
  • smb2_dispatch_lock

Additionally, smb2_dispatch_compound can be included in both categories as it’s what actually dispatches calls to the above functions.

Of course, these aren’t all the functions that comprise the attack surface, but most of the interesting code should be reachable through them. With this overview, we can proceed with closer code analysis and perform systematic fuzz testing.

Fuzzing

The simplest and quickest way to target and fuzz this service would be with a network fuzzer such as Mutiny. While Mutiny doesn’t have built-in feedback mechanisms and fuzzing directly over the network is relatively slow, it’s easy to get it up and running which, if nothing else, can give us a feel for what to expect and what kind of issues we might need to deal with. Since reaching pre-auth code doesn’t require any complex session setup, throwing packets that vaguely resemble SMB2 at the server, one at a time is somewhat effective for the effort it takes. Although, digging into functions, we want to target (smb2_dispatch_negotiate and smb2_dispatch_session_setup).

Targeting the post-authentication attack surface requires a bit more setup. In general, when it comes to testing server-side code, we would start with a simple prototype client that reaches the desired session state. For example, usually, the first SMB command following successful authentication is the “Tree Connect” request. A prototype client code that can successfully negotiate a session and send raw bytes that represent a “Tree Connect” request is useful in testing the fuzzer and can serve as a basis for a PoC if any issues are uncovered. SMB authentication and session tracking require a bit of work. Luckily, there are several complete implementations of sufficiently low level that can be reused. Python implementation of SMB2 called “smbprotocol” is perfectly suited for this task, as it provides direct access to connection socket and packet-signing functions. It can easily be used to extend Mutiny to first negotiate the session correctly before starting to send mutated packets to the server.

The above code properly authenticates to the server to get the session to a state where the fuzzing of the post-authentication code can begin. With respect to the SMB2 protocol state transition graph, after authentication is established, a mutated “Tree connect” request can be generated and sent to execute and test the “smb2_dispatch_tree_connect” function.

In a similar fashion, if the target function is “smb2_dispatch_create_file”, protocol specs demand that a tree connection be performed earlier in the session. In other words, “Create File” request requires a valid TreeId handle (these protocol requirements can and should be confirmed for the target, for which a prototype client is again useful).

So, for our Mutiny-based fuzzer to be effective, we would want to perform a valid “Tree connect” request in the pre-fuzzing stage and then proceed to send mutated “Create File” requests in order to test “smb2_dispatch_create_file” and its callees properly.

While fuzzing each of the dispatch functions in a systematic manner with mutiny is possible, a feedback-driven fuzzer would be more effective. Frida is a tracing tool of choice for macOS applications and can easily be used to gather code coverage. This can be used to quickly whip up a coverage-guided fuzzer that would go one step further than Mutiny’s contextless fuzzing and would actually be able to discover new code paths. Frizzer-fuzzer is a good example of such a tool. As a network-based fuzzer, it can be extended in the same manner as Mutiny to get the target in the desired state before fuzzing.

Since both of these fuzzing approaches require negotiating network connections, they are relatively slow. Simple in-memory-based fuzzing is tempting, as it would be very fast, but most of the SmbX functions that would serve as interesting targets deal with quite a bit of global state and are subject to timeouts. These are fixable problems, but usually lead to more false positives — or vulnerabilities that cannot be reproduced in normal execution environments. A more complete form of in-memory fuzzing, such as snapshot fuzzing, would alleviate these problems and we have performed extensive fuzz testing using Barbervisor. However, all the issues uncovered by fuzzing were reachable even with slower, network-based, guided fuzzing.

Coverage analysis

Besides having a prototyping client implementation that’s useful for testing, it is important to have a way to confirm that fuzzing is actually working and reaching the code we intend to test. Code coverage, used in coverage-guided fuzzing, can also provide invaluable insight with manual analysis. Confirming that the fuzzer actually works can be as easy as setting up a few breakpoints in a debugger, but actually sampling a subset of fuzz test cases and viewing cumulative coverage is more informative. Various forms of Dynamic Binary Instrumentation frameworks can be used for this purpose, but Frida was again our tool of choice. To graphically view and analyze coverage, a popular IDA or Binary Ninja plugin called “Lighthouse” is perfect. Lighthouse can ingest binary code coverage information in de facto standard “drcov” format and actually comes with a Frida script to collect it.

An overview of function level coverage, as shown above, is informative, but a more detailed basic block graph view can be even more useful in identifying fuzzer choke points, such as very strict checks or large constants that are hard for a fuzzer to generate at random. The following example from smb2_dispatch_ioctl illustrates this point, as the function implements a switch statement that enumerates all supported IOCTL constants.

Each IOCTL code is a four-byte magic value that would be hard for a fuzzer to hit. These can be added to a dictionary to be used during test case mutation to improve coverage. Coverage analysis can also lead you to code paths that are unreachable due to some preconditions, like configuration details, existence or non-existence of files or any options negotiated in previous steps.

Reversing insights

Fuzzing can only catch bugs that have externally observable effects. In other words, for the most part, they can catch bugs that cause crashes. Some of the identified vulnerabilities highlighted later, like directory query arbitrary file access, fall into the logic bugs category, rather than memory corruption. We will often supplement fuzzing with manual analysis which not only has a chance of finding additional vulnerabilities but often leads to better fuzzing.

For example, cursory reverse-engineering of the dispatch functions would reveal that they all have a similar form:

A call to one of the `smb2::extract()` functions followed by a validity check of some form. No fuzzer would be considered effective if 99% of generated test case fail this first check right there. Every type of SMB2 message actually has a corresponding “smb2::extract” function in SmbX.

This “smb2::extract” family of functions usually boils down to mapping contents of a packet to in-memory structure:

But, most importantly, they perform certain sanity checks. As in the above example, the specified size of the message header is explicitly checked to be a constant of 0x39, otherwise, the function fails and the packet is discarded. Knowing this, we can implement a fix in our fuzzer to make sure all generated test cases pass this simple check.

Another nuisance of SMB2 fuzzing is that SmbX requires message signatures when negotiating the connection. This means that either our fuzzer will have to calculate the correct signature for every mutated packet, or we can simply do away with it by patching the binary that we are testing:

Effective fuzzers hopefully uncover crashes, some of which can be pretty shallow and easy to trigger. This was the case with the compound message parsing vulnerability disclosed in TALOS-2021-1237. An easy-to-trigger vulnerability like that one is often an impediment to effective fuzzing. If a single vulnerability gets triggered every 5 fuzz test cases, frequent restarts slow down fuzzing and pollute results. It is therefore imperative to find the root cause of the vulnerability early in the fuzzing process so it can be worked around, patched or ignored. In some cases, this is necessary even for unexploitable bugs. The infinite loop vulnerability disclosed in TALOS-2021-1263 is another example of a low severity vulnerability that can severely impact fuzzing performance.

Results of our research

This research we’ve outlined resulted in the discovery of seven vulnerabilities. In accordance with Cisco’s vulnerability disclosure policy, Talos promptly disclosed these vulnerabilities to Apple. Apple released fixes in a recent macOS update. All but one affected all supported versions of macOS, with TALOS-2021-1246 only affecting versions up to and including Catalina.  TALOS-2021-1246 appears to have been found by Apple internally and fixed in Big Sur upon release, but other supported versions of macOS remained unpatched and vulnerable at the time we reported it. Since then, it was silently fixed in one of the monthly security updates.

TALOS-2021-1237 (CVE-2021-1878) Apple macOS SMB server signature verification information disclosure vulnerability

Since the SMB2 revision of the SMB protocol, a supported compound SMB messages. This means that a single packet can contain multiple, chained, messages that need to be parsed, verified and processed in sequence. NextCommand facilitates this in the SMB2 header and specifies the offset from the beginning of the current packet to the next one in the compounded request or response. If the request/response packet isn't compound, or is last in sequence, this field must be 0. Both SMB2 and SMB3 have mandatory message signing, facilitated by different HMAC algorithms. SMB2 dialects use SHA256 based HMAC, while SMB3 dialects use AES-128-CMAC.

This vulnerability exists in the way macOS SMB server processes SMB3 compounded packets. By carefully controlling the NextCommand value, an arbitrary amount of out-of-bounds bytes can be included in HMAC calculation, which can either result in HMAC verification as valid or invalid. This can potentially be used to construct an information disclosure oracle by sending a sequence of specially crafted packets. When the value of NextCommand is equal to 64, in both cases (SMB2 and SMB3), the calculated length for the second HMAC Update function call will be 0, meaning that no bytes from the rest of the packet would be used in signature verification. This compromises message integrity and could potentially be abused in man-in-the-middle scenarios to inject arbitrary content into requests and replies between client and server.

TALOS-2021-1246 (CVE-2020-10005): Silently fixed Apple macOS SMB server TREE_CONNECT stack buffer overflow vulnerability

This vulnerability is present in SMB2 and newer versions of the protocol, more specifically in the TREE_CONNECT request processing. TREE_CONNECT request is used to connect to a specified SMB share and has a relatively simple structure consisting of structure size, flags, path offset, path length and a utf16 string specifying a path.

When processing the TREE_CONNECT request, function smb2_dispatch_tree_connect is invoked. First, parts of the TREE_CONNECT structure are extracted by calling the appropriate smb2::extract method:

__text:0000000100002C5E lea     rdi, [rbp+pTreeConnectBuffer] ; this

__text:0000000100002C65 mov     [rdi], rsi

__text:0000000100002C68 lea     rsi, [rbp+var_868]

__text:0000000100002C6F mov     [rsi], rdx

__text:0000000100002C72 lea     rcx, [r15+90h]

__text:0000000100002C79 lea     rdx, [rbp+tree_connect_request_extracted]

__text:0000000100002C7D call    smb2::extract(uchar *&,uchar * const&,smb2::tree_connect_request &,uchar * const&)

Calling smb::extract will extract the utf16 path string and its length. Shortly after that, there’s a memcpy call:

__text:0000000100002CCF cmovs   rsi, rax        ; void *

__text:0000000100002CD3 lea     edx, [rbx+rbx]  ; size_t

__text:0000000100002CD6 lea     r12, [rbp+wcSharePath]

__text:0000000100002CDD mov     rdi, r12        ; void *

__text:0000000100002CE0 call    _memcpy

From the above, we can observe that the destination for the memcpy call is a stack buffer rbp+wcSharePath and the source is a previously extracted pointer to path string. The length is specified from the TREE_CONNECT structure. Since the local stack buffer wcSharePath is located in the stack and of static size, this constitutes a stack buffer overflow that can lead to arbitrary code execution.

TALOS-2021-1258 (CVE-2021-30712): Apple macOS SMB server IOCTL request uninitialized stack variable vulnerability

While processing the IOCTL_REQUEST packets, function smb2_dispatch_ioctl_request is invoked with supplied packet bytes as its arguments. In turn, it calls smb2::extract(uchar *&,uchar * const&,smb2::ioctl_request &,uchar * const&) which extracts different fields from the packet into IOCTL_REQUEST structure.

A code path exists in "smb2::extract" that can leave a stack buffer uninitialized while still succeeding. This pointer actually resides on the calling function's stack, is uninitialized and contains random data. When it is subsequently used, it can lead to a crash or further memory corruption if contents can be controlled.

Uninitialized stack variables are rarely exploitable, so to demonstrate control over the uninitialized stack area, the attached PoC constructs a special, compound, SMB command. To control the stack contents, we need to find a function that gives us control of its own stack. A prime candidate for this is the smb2_dispatch_tree_connect function, which is perfect for this task because it has a large stack frame and directly copies packet data onto its stack. Different SMB messages in a compound SMB message are processed in the same thread, meaning the stack memory initialized by "TREE_CONNECT" will subsequently be utilized by "IOCTL_REQUEST" and we can therefore achieve control of the uninitialized variable.

TALOS-2021-1260 (CVE-2021-30717): Apple macOS SMB server directory query request integer overflow vulnerability

Protocol specification shows that QUERY_DIRECTORY request is to be used to get information about the directory's contents and search for files. As such, important parts of the QUERY_DIRECTORY structure are FileId that refers to the previously opened directory, and an arbitrary length search pattern which is sent in the form of a Unicode string at the end.

When converting the search pattern from UTF16 (as per SMB2 specs) to UTF8 used by SmbX, an error return code can be treated as an index value. With certain error values, this can lead to integer wraparound when used as an index and the wraparound actually ends up overwriting a most significant non-zero byte of a pointer on the stack which can lead to further memory corruption.

Results can differ between the versions of SmbX, but as luck would have it, the corrupted pointer is being dereferenced during function teardown to free allocated resources, and what's more important, the access violation happens right before the following code:

0x107068c4b <+1488>: lock

0x107068c4c <+1489>: dec    dword ptr [rdi + 0x10]

0x107068c4f <+1492>: jne    0x107068c5b               ; <+1504>

0x107068c51 <+1494>: add    rdi, 0x8

0x107068c55 <+1498>: mov    rax, qword ptr [rdi]

0x107068c58 <+1501>: call   qword ptr [rax + 0x8]

The code above shows a vtable dereference from “rdi” (the corrupted pointer) and a call instruction using that dereference. If sufficient control over memory layout is achieved by the attacker, and a corrupted pointer ends up pointing to memory under the attacker's control, this could lead to arbitrary code execution.

TALOS-2021-1263 (CVE-2021-30716): Apple macOS SMB server lock request infinite loop

Lock requests are meant to be used to prevent changes to file contents pending other operations. As specified by SMB protocol, the lock request structure contains a file ID that points to a file to be locked. It also has one or more LOCK_ELEMENT structures, lock size and flags. LOCK_ELEMENT structure contains a file offset at which a lock should be made.

A vulnerability exists in the way error conditions are handled. When two LOCK_ELEMENT structures in a single lock request have an overlapping range, a second lock call can fail to depend on the flags. When lock_range fails, an error code is returned and looping over LOCK_ELEMENT structures is halted. Due to a bug in this error handling, function "ntvfs::lock_range" is called in a loop without any possible exit condition. Once all available threads end up stuck in infinite loops, the SMB server becomes unavailable which constitutes a denial of service state. Moreover, the whole operating system is affected as SMB threads use up all available CPUs.

TALOS-2021-1268 (CVE-2021-30722): Apple macOS SMB server create file request uninitialized memory disclosure

SMB CREATE_REQUEST structure as sent by the client contains several fields related to file creation, including a file name and any number of SMB2_CREATE_CONTEXT buffers. Protocol specification enumerates a number of unique SMB2_CREATE_CONTEXT buffer types, one of which is SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST whose identifying value is 0x4d784163 (or "MxAc"). This is used to request file access information to be included in the server's reply.

Specification surrounding SMB2_CREATE_CONTEXT buffers states that each of these, in request and replies both, must start on an eight-byte aligned boundary. When an allocation for this output buffer is made, no initialization is performed, but the memory contents of the previous allocation are still present. Depending on the structure sizes and alignment, by the time this call to smb2::insert is done, the output buffer can contain from one up to 4 bytes of uninitialized data which is then sent to the client. This is demonstrated by the attached proof of concept exploit that crafts a CREATE_REQUEST SMB2 packet with thousands of SMB2_CREATE_CONTEXT structures, each of which gets a reply.

What's interesting about this vulnerability is that it can easily be spotted by observing network packet capture:

Every four bytes following the “MxAc” tag leak four uninitialized bytes from memory. These often contain partial pointers that can aid further exploitation.

TALOS-2021-1269 (CVE-2021-30721): Apple macOS SMB server directory query arbitrary file access

QUERY_DIRECTORY command is used to enumerate directory information and contents. Protocol specifications show that QUERY_DIRECTORY structure can contain a unicode buffer that represents a search string to be run on a queried directory. This search string is meant to contain a file name or a wildcard string to filter results of the query.

Normally, only an authenticated user is permitted to access and query directories and files that fall under the share that is being accessed. The vulnerability lies in the fact that the query string can contain a regular UNIX path that begins with the character “/”, which would allow an attacker to access any file on the file system, presuming the underlying user has access right to it. Metadata about the file or directory is returned to the user, which can include access timestamps, sizes and other information. Additionally, if the file or directory does not exist, an error is sent in reply. This can be used to enumerate existing files and directories, especially user home directories, which would reveal usernames that could be useful in further attacks.

Conclusion

When targeting stateful protocols such as SMB, it is very important to, at least vaguely, identify and understand the state graph. This graph shows which states are only reachable from certain other states. This might require extensive reverse engineering if the protocol specification is unknown or the implementation doesn’t strictly adhere to the specs. Then, it becomes very effective to fuzz certain states by first satisfying the pre-conditions.

In this case, we started fuzzing in different states and targeted the next state transition, one at a time, until we tested all the code we wanted to look at. The next step in this research would be to construct a way to effectively fuzz multiple steps in the same SMB connection. This goes beyond code coverage and leads to more complex states that would surely uncover deeper vulnerabilities.

Another takeaway would be to identify fuzzer blockers and chokepoints early on by analyzing the code that is being tested. This, plus patching or avoiding known crashes, often leads to harder-to-reach vulnerabilities. Both can be achieved by either fixing generated fuzz test cases after mutation or by employing more structure-aware generators in the first place.

Coverage

The following SNORTⓇ rules will detect exploitation attempts against this vulnerability: 57115 - 57118, 57136, 57232, 57265, 57282, 57310 and 57340. Additional rules may be released in the future and current rules are subject to change, pending additional vulnerability information. For the most current rule information, please refer to your Firepower Management Center or Snort.org.