Talos Vulnerability Report

TALOS-2021-1260

Apple macOS SMB server directory query request integer overflow vulnerability

June 2, 2021
CVE Number

CVE-2021-30717

Summary

A memory corruption vulnerability exists in the SMB Server on Apple macOS 11.2. A specially crafted SMB packet can trigger an integer overflow when handling directory query requests which can result in memory corruption, potentially leading to remote code execution and denial of service. This vulnerability can be triggered by sending a malicious packet to the vulnerable server.

Tested Versions

Apple macOS 11.2

Product URLs

https://apple.com

CVSSv3 Score

7.5 - CVSS:3.0/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-190 - Integer Overflow or Wraparound

Details

macOS is a series of proprietary operating systems developed by Apple with macOS 11.2, with Big Sur being the latest.

Server Message Block (SMB) is a network file sharing protocol widely used in Windows network environments and macOS contains a proprietary implementation of both server and client components. SMB is often used in office and enterprise environments for file and printer sharing.

Three distinct versions and multiple dialects of SMB protocol are supported by macOS’ SMB server. This vulnerability is present in SMB2 and newer versions of the protocol, more specifically in the QUERY_DIRECTORY request processing.

Protocol specification shows that QUERY_DIRECTORY request is to be used to get information about 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 a form of a unicode string at the end. When processing QUERY_DIRECTORY request in smb2_dispatch_query_directory function, following code is executed:

1000291d0      int32_t rax_14 = platform::utf::ConvertUTF16toUTF8(&var_4e0, rax_13 + (rdx_2 << 1), &var_490, rcx_2, 4) [1]
1000291e2      int32_t rcx_4 = var_490.d - &var_448                             
1000291e6      int32_t rdx_5 = neg.d(rax_14)                                [2]
1000291e8      if (rax_14 == 0)                                                         [3]
1000291ea          rdx_5 = rcx_4                                                        [4]
1000291ed      int64_t rax_15 = sx.q(rdx_5)
1000291f0      *(&var_448 + rax_15) = 0                                        [5]
1000291f8      r15 = 0
1000291fb      int32_t rax_18
1000291fb      int32_t rax_28
1000291fb      if (rax_15.d s< 0)
100029257          rax_18 = smb2_schedule_error(arg1, 0xc00000e5)
10002920d      else
10002920d          std::__1::basic_string<c..., std::__1::allocator<char> >::assign(&var_488)

In the above code, a call to platform::utf::ConvertUTF16toUTF8 is made to covert a UTF16 search pattern string from the packet into a UTF8 one used by the rest of the code. Function ConvertUTF16toUTF8 has four distinct return values specifying different success conditions, zero being success and 1, 2 and 3 being different error codes. At [2] in the above code, return value from ConvertUTF16toUTF8 call is negated and then the original value is compared to zero at [3]. If return code was zero (success), calculated length value is copied into rdx_5, otherwise negated and sign extended value is copied into rax_15 which is then used to dereference a var_448 pointer and write a single NULL byte at the specified address at [5]. In regular execution, this code would write a single zero byte at the end of the converted string. However, if check at [3] fails, a negated error code is used instead. Possible error codes are 1, 2 or 3 , which when negated and sign extended result in a very large value due to integer wraparound. At [5], this bogus value is used as an index to a pointer which results in an out of bounds memory write. Since the wraparound value is very large, this wraparound at [5] actually ends up accessing a stack pointer. With error value 3 , 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. We can observe this behavior in the debugger:

* thread #5, queue = 'com.apple.root.default-qos', stop reason = breakpoint 2.1
    frame #0: 0x00000001070688af smbd`smb2_dispatch_query_directory(smb_request&, unsigned char*, unsigned char*) + 564
smbd`smb2_dispatch_query_directory:
->  0x1070688af <+564>: call   0x107073bd2               ; platform::utf::ConvertUTF16toUTF8(unsigned short const**, unsigned short const*, unsigned char**, unsigned char*, unsigned int)
    0x1070688b4 <+569>: mov    ecx, dword ptr [rbp - 0x480]
    0x1070688ba <+575>: lea    rdx, [rbp - 0x438]
    0x1070688c1 <+582>: sub    ecx, edx
Target 0: (smbd) stopped.
(lldb) memory read $rdi -s 8 -fx
0x7000039b2840: 0x00007fc9adc13270 0x00007000039b28b8
0x7000039b2850: 0x00007000039b2870 0x00007fff6a2a9903
0x7000039b2860: 0x0000000001010021 0x0000000000000003
0x7000039b2870: 0x0000000000000001 0x0000000000060060
(lldb) memory read 0x00007fc9adc13270
0x7fc9adc13270: 06 d8 d8 06 00 00 00 20 35 2b dc 9a fc 07 07 00  .��.... 5+�.�...
0x7fc9adc13280: 00 0e 43 74 ff 7f 00 00 ab b1 58 ef 00 00 00 00  ..Ct�...��X�....

Above output shows a breakpoint just before a call to ConvertUTF16toUTF8 showing the unicode data to be converted. Stepping over the function call :

(lldb) ni
Process 85684 stopped
* thread #5, queue = 'com.apple.root.default-qos', stop reason = instruction step over
    frame #0: 0x00000001070688b4 smbd`smb2_dispatch_query_directory(smb_request&, unsigned char*, unsigned char*) + 569
smbd`smb2_dispatch_query_directory:
->  0x1070688b4 <+569>: mov    ecx, dword ptr [rbp - 0x480]
    0x1070688ba <+575>: lea    rdx, [rbp - 0x438]
    0x1070688c1 <+582>: sub    ecx, edx
    0x1070688c3 <+584>: mov    edx, eax
Target 0: (smbd) stopped.
(lldb) register read rax
     rax = 0x0000000000000003

Above shows the return value from ConvertUTF16toUTF8 being 3 which signifies illegal unicode sequence was found and conversion was stopped. Continuing forward to the point where out of bounds write occurs:

* thread #5, queue = 'com.apple.root.default-qos', stop reason = instruction step over
    frame #0: 0x00000001070688cf smbd`smb2_dispatch_query_directory(smb_request&, unsigned char*, unsigned char*) + 596
smbd`smb2_dispatch_query_directory:
->  0x1070688cf <+596>: mov    byte ptr [rbp + rax - 0x438], 0x0
    0x1070688d7 <+604>: xor    r15d, r15d
    0x1070688da <+607>: test   eax, eax
    0x1070688dc <+609>: js     0x107068977               ; <+764>
Target 0: (smbd) stopped.
(lldb) reg read rax
     rax = 0xfffffffffffffffd
(lldb) memory read $rbp+$rax-0x438
0x7000039b28d5: 7f 00 00 a0 d7 c2 ad c9 7f 00 00 a0 a0 c1 ad c9  ...��­�...����
0x7000039b28e5: 7f 00 00 10 32 c1 ad c9 7f 00 00 66 00 00 00 00  ....2��...f....
(lldb) memory read $rbp+$rax-0x438 -s 8 -fx
0x7000039b28d5: 0xc9adc2d7a000007f 0xc9adc1a0a000007f
0x7000039b28e5: 0xc9adc1321000007f 0x000000006600007f
0x7000039b28f5: 0x00039b2908000000 0x0000000000000070
0x7000039b2905: 0xff00000066660000 0xc9adc1a0a000007f

Above shows the code right before the out of bounds write happens. Value in rax is used to index into a stack buffer and we can see it’s value is abnormally large. The pointer dereference points to $rbp+$rax-0x438 which is this same function’s stack. As it is currently lined up, out of bounds write will overwrite the 0x7f MSB of a pointer.

(lldb) ni
Process 85684 stopped
* thread #5, queue = 'com.apple.root.default-qos', stop reason = instruction step over
    frame #0: 0x00000001070688d7 smbd`smb2_dispatch_query_directory(smb_request&, unsigned char*, unsigned char*) + 604
smbd`smb2_dispatch_query_directory:
->  0x1070688d7 <+604>: xor    r15d, r15d
    0x1070688da <+607>: test   eax, eax
    0x1070688dc <+609>: js     0x107068977               ; <+764>
    0x1070688e2 <+615>: lea    rdi, [rbp - 0x460]
Target 0: (smbd) stopped.
(lldb) memory read $rbp+$rax-0x438 -s 8 -fx
0x7000039b28d5: 0xc9adc2d7a0000000 0xc9adc1a0a000007f
0x7000039b28e5: 0xc9adc1321000007f 0x000000006600007f
0x7000039b28f5: 0x00039b2908000000 0x0000000000000070
0x7000039b2905: 0xff00000066660000 0xc9adc1a0a000007f
(lldb)

And indeed, after the out of bounds write, the pointer on the stack is corrupted.

Continuing execution will lead to a crash when this pointer is next used:

   * thread #5, queue = 'com.apple.root.default-qos', stop reason = EXC_BAD_ACCESS (code=1, address=0xc9adc2d7b0)
    frame #0: 0x00000001070687e2 smbd`smb2_dispatch_query_directory(smb_request&, unsigned char*, unsigned char*) + 359
smbd`smb2_dispatch_query_directory:
->  0x1070687e2 <+359>: lock
    0x1070687e3 <+360>: dec    dword ptr [rdi + 0x10]
    0x1070687e6 <+363>: jne    0x1070687f2               ; <+375>
    0x1070687e8 <+365>: add    rdi, 0x8
Target 0: (smbd) stopped.
(lldb) reg read rdi
     rdi = 0x000000c9adc2d7a0
(lldb) bt
* thread #5, queue = 'com.apple.root.default-qos', stop reason = EXC_BAD_ACCESS (code=1, address=0xc9adc2d7b0)
  * frame #0: 0x00000001070687e2 smbd`smb2_dispatch_query_directory(smb_request&, unsigned char*, unsigned char*) + 359
    frame #1: 0x000000010706aabe smbd`smb2_dispatch_compound(smb_transport*, unsigned char*, unsigned char*) + 1358
    frame #2: 0x00000001070561a1 smbd`invocation function for block in smb_transport::dispatch() + 54
    frame #3: 0x00007fff6a04e6c4 libdispatch.dylib`_dispatch_call_block_and_release + 12
    frame #4: 0x00007fff6a04f658 libdispatch.dylib`_dispatch_client_callout + 8
    frame #5: 0x00007fff6a05daa8 libdispatch.dylib`_dispatch_root_queue_drain + 663
    frame #6: 0x00007fff6a05e097 libdispatch.dylib`_dispatch_worker_thread2 + 92
    frame #7: 0x00007fff6a2a99f7 libsystem_pthread.dylib`_pthread_wqthread + 220
    frame #8: 0x00007fff6a2a8b77 libsystem_pthread.dylib`start_wqthread + 15
(lldb)

Above shows a write access violation when a corrupt pointer is being dereferenced. 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]

Above code 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 corrupted pointer ends up pointing to memory under attacker’s control, this could lead to arbitrary code execution.

Timeline

2021-03-09 - Vendor Disclosure
2021-05-25 - Vendor Patched

2021-06-02 - Public Release

Credit

Discovered by Aleksandar Nikolic of Cisco Talos.