|
| 1 | +--- |
| 2 | +title: "MacOS Binary Debugging" |
| 3 | +date: 2025-01-12 |
| 4 | +tags: |
| 5 | + - "lldb" |
| 6 | + - "ghidra" |
| 7 | + - "debugging" |
| 8 | + - "macos" |
| 9 | +advisory: false |
| 10 | +cves: |
| 11 | +og: |
| 12 | + image: "/img/blogs/macos-debugging/HeroImage.png" |
| 13 | + title: "MacOS Binary Debugging" |
| 14 | + description: "This blog post covers the basics of setting up an environment to debug binaries on MacOS" |
| 15 | +--- |
| 16 | + |
| 17 | +<img src="/img/blogs/macos-debugging/image1.png" style="display: block; margin-left: auto; margin-right: auto; width: 25%;"/> |
| 18 | + |
| 19 | +# Introduction |
| 20 | + |
| 21 | +Dynamic reverse engineering and binary debugging involve analyzing the behavior of a running program to understand its functionality and/or identify potential vulnerabilities. This technique is often used for malware analysis, security research, and software testing. If you ever had the chance to dynamically reverse engineer, you most likely used [GDB](https://www.sourceware.org/gdb/) as the debugger. But ever since 2007, Apple shifted gradually from GCC to Clang, with it becoming the default compiler in Xcode 4.0 released in 2011.\ |
| 22 | +This means we will need to use [lldb](https://lldb.llvm.org/) for debugging. Fortunately, the differences in user experience aren't big. |
| 23 | + |
| 24 | +## lldb Setup |
| 25 | + |
| 26 | +Installing `lldb` could have not been easier, simply open the terminal and write `lldb`, if it's not installed you will be prompted with the installation window. |
| 27 | + |
| 28 | +<img src="/img/blogs/macos-debugging/image2.jpg" style="width: 100%;"/> |
| 29 | + |
| 30 | +After installation, if we try to run `lldb` and attach it to a running process (or execute a binary via `process launch`) we will probably face with the following error: |
| 31 | + |
| 32 | +<img src="/img/blogs/macos-debugging/image3.png" style="width: 100%;"/> |
| 33 | + |
| 34 | +> *error: attach failed: attach failed (Not allowed to attach to process. Look in the console messages (Console.app), near the debugserver entries, when the attach failed. The subsystem that denied the attach permission will likely have logged an informative message about why it was denied.)* |
| 35 | +
|
| 36 | +# OSX Protections |
| 37 | + |
| 38 | +Debugging applications on macOS can be more complex than on other operating systems due to the stringent security measures [implemented](https://developer.apple.com/documentation/security?language=objc) by Apple. Mechanisms such as [Harden Runtime](https://developer.apple.com/documentation/security/hardened-runtime?language=objc), [System Integrity Protection](https://support.apple.com/en-us/102149) (SIP), and [Gatekeeper](https://support.apple.com/en-gb/guide/security/sec5599b66df/web), are designed to safeguard user privacy and system integrity but often restrict necessary access for researchers. |
| 39 | + |
| 40 | +One important tip that can help us troubleshoot issues throughout our setup process is the use of the `console` app. It provides a centralized interface to view system logs, application logs, and other diagnostic information. By analyzing these logs, we can pinpoint which mechanism blocks us and seek a relevant solution. |
| 41 | + |
| 42 | +<img src="/img/blogs/macos-debugging/image4.png" style="display: block; margin-left: auto; margin-right: auto; width: 15%;"/> |
| 43 | + |
| 44 | +So let’s try to attach via `lldb` again and take a look at the console. |
| 45 | + |
| 46 | +<img src="/img/blogs/macos-debugging/image5.png" style="width: 100%;"/> |
| 47 | + |
| 48 | +Due to Harden Runtime, an application in MacOS has to grant permissions in order for debuggers to attach to their processes. These permissions are done via [entitlement](https://developer.apple.com/documentation/bundleresources/entitlements?language=objc) in the code signature. In our specific case, `get-task-allow` entitlement is missing. |
| 49 | + |
| 50 | +But as researchers, we usually debug built applications that are already signed with certain permissions. Can we change them? |
| 51 | + |
| 52 | +# Self-Signing Binaries to Grant Permissions |
| 53 | +We can [resign](https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html), with extra permissions, the specific binary we would like to debug. However, as I will explain later in the blog, it might cause some other problems in the future. But, for now, let's see how we can resign executables. |
| 54 | + |
| 55 | +First, here is how we can see a file's current signature: Form the Authority, TeamIdentifier, to the actual (XML formatted) entitlements. |
| 56 | +```bash |
| 57 | +codesign -d -vvv --entitlements :- <path_to_file> |
| 58 | +``` |
| 59 | + |
| 60 | +We can take the XML text of the entitlements and write it to a file adding our `get-task-allow` permission, e.g: |
| 61 | + |
| 62 | +```xml |
| 63 | +<?xml version="1.0" encoding="UTF-8"?> |
| 64 | +<!DOCTYPE plist |
| 65 | + PUBLIC '-//Apple//DTD PLIST 1.0//EN' |
| 66 | + 'https://www.apple.com/DTDs/PropertyList-1.0.dtd'> |
| 67 | +<plist version="1.0"> |
| 68 | + <dict> |
| 69 | + <key>com.apple.security.cs.allow-jit</key> |
| 70 | + <true/> |
| 71 | + <key>com.apple.security.cs.allow-unsigned-executable-memory</key> |
| 72 | + <true/> |
| 73 | + <key>com.apple.security.get-task-allow</key> |
| 74 | + <true/> |
| 75 | + </dict> |
| 76 | +</plist> |
| 77 | + |
| 78 | +``` |
| 79 | + |
| 80 | +Now we need to create a certificate, which we can use to sign the file. |
| 81 | + |
| 82 | +1. Open Keychain Access application |
| 83 | +<img src="/img/blogs/macos-debugging/image6.png" style="display: block; margin-left: auto; margin-right: auto; width: 15%;"/> |
| 84 | +2. And via the toolbar create a certificate with the type “Code Signing”: |
| 85 | +<img src="/img/blogs/macos-debugging/image7.png" style="width: 100%;"/> |
| 86 | +<img src="/img/blogs/macos-debugging/image8.png" style="width: 100%;"/> |
| 87 | + |
| 88 | +using the custom certificate we can now re-sign the executable using: |
| 89 | +```bash |
| 90 | +sudo codesign --entitlements <path_to_entitlements.xml> -fs lldb <path_to_binary> |
| 91 | +``` |
| 92 | +*lldb* is the name given to the certificate in this example, of course, this can be whatever you’d like. And the `-fs` flag, forcefully signs the binary which ignores previous signatures. |
| 93 | +To test if the signature was updated, simply run the first command discussed in this section. You should see the new entitlements (and the custom certificate as the authority). |
| 94 | + |
| 95 | +# Bypassing Signature Check |
| 96 | +Sometimes, changing the signature can affect the binary's execution flow if the implemented logic checks certain fields of the signature that have been tampered with. A common check is the [Team Identifier](https://developer.apple.com/documentation/automaticassessmentconfiguration/aeassessmentapplication/teamidentifier?language=objc), which is meant to identify the team that developed the application (This can be used for example, when an application needs to verify that a received [XPC](https://developer.apple.com/documentation/xpc?language=objc) communication is made from a trustable source). |
| 97 | + |
| 98 | +## Insufficient Signature Check |
| 99 | +If not performed correctly, such as in this [case](https://wojciechregula.blog/post/learn-xpc-exploitation-part-1-broken-cryptography/) covered by [Wojciech Reguła](https://x.com/_r3ggi), you can simply change the "Organizational Unit" in the certificate by overriding the default parameters of the certificate |
| 100 | +<img src="/img/blogs/macos-debugging/image9.png" style="width: 100%;"/> |
| 101 | +<img src="/img/blogs/macos-debugging/image10.png" style="width: 100%;"/> |
| 102 | + |
| 103 | +However, in one of my research, the application used the key [kSecCodeInfoTeamIdentifier](https://developer.apple.com/documentation/security/kseccodeinfoteamidentifier?language=objc), which checked the team identifier in a safe manner, and I couldn't find a way to change it. In this case I had to seek different solutions, with the simplest ones being patching the binary to bypass the verifications. |
| 104 | + |
| 105 | +We can automate a dynamic patch to the memory or change the file on the disk itself, which will avoid the check of the signature. Each patching approach, dynamic vs. static, has pros and cons. In the dynamic approach, we change the code in memory after the binary is already loaded, and depending on the situation, it could be simpler than a static patch as this method does not require us to resign the binary again. |
| 106 | + |
| 107 | +## Dynamic Patching |
| 108 | +For demonstration purposes, let's say we would like to patch a function named `is_dev`, that hard-codedly returns 0. But in cases when it returns 1, the signature verification is skipped (finding what exactly is needed to be changed is binary-dependent, and can be discovered by static reverse engineering). |
| 109 | +The function disassembled code looks as such: |
| 110 | +``` |
| 111 | +00 00 80 52 mov w0,#0x0 |
| 112 | +c0 03 5f d6 ret |
| 113 | +``` |
| 114 | + |
| 115 | +We can use an [online disassembler](https://shell-storm.org/online/Online-Assembler-and-Disassembler/?inst=movz+w0%2C+%231%0D%0Aret&arch=arm64&as_format=inline#assembly) or change the value directly in ghidra to see the new function bytes, in this case, returning 1 will change the first null byte to `\x20`. Let's look at two approaches to patching the function: |
| 116 | + |
| 117 | +Using `lldb` CLI: |
| 118 | +- Attach to the process |
| 119 | +- Find the function location in memory using `image lookup -n is_dev -v` |
| 120 | +- Write to change the code using `mem write <address> <value>` |
| 121 | +- Read to confirm the change - `mem read <address> (--count optional)` |
| 122 | + |
| 123 | +Automate it using Python ([official docs](https://lldb.llvm.org/use/python-reference.html#using-the-lldb-py-module-in-python)): |
| 124 | +- Access scripting tool via the `script` command in `lldb`'s CLI |
| 125 | +- Or if you'd like to import `lldb` directly into your Python environment |
| 126 | + - Locate `lldb` python module using `lldb -P`, then according to the module use the corresponding python version (can see it in the file name `_lldb.cpython-{version}-darwin.so`, for example `_lldb.cpython-312-darwin.so` use python3.12) |
| 127 | + - In case you get `ImportError: cannot import name '_lldb' from partially initialized module 'lldb' (most likely due to a circular import` or would like another version you can `brew install llvm` and get the new python module at `/opt/homebrew/opt/llvm/bin/lldb -P` |
| 128 | +- Here is a sample code: |
| 129 | +```py |
| 130 | +import sys sys.path.append('/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python3') |
| 131 | +import lldb |
| 132 | + |
| 133 | +process_name = "<NAME>" |
| 134 | + |
| 135 | +listener = lldb.SBListener() |
| 136 | +error = lldb.SBError() |
| 137 | +dbg = lldb.SBDebugger.Create() |
| 138 | +target = dbg.CreateTarget(None) |
| 139 | +process = target.AttachToProcessWithName(listener, process_name, False, error) |
| 140 | +function_name = "is_dev" |
| 141 | +function = target.FindFunctions(function_name)[0] |
| 142 | +function_address = function.GetSymbol().GetStartAddress() |
| 143 | +process.WriteMemory(function_address.GetLoadAddress(target), '\x20', error) |
| 144 | +# check that it worked - process.ReadMemory(function_address.GetLoadAddress(target)-1, 1, error) |
| 145 | +process.Continue() |
| 146 | +``` |
| 147 | + |
| 148 | +## Static Patching |
| 149 | +Static patching can be the solution in some scenarios where the binary that you need to patch runs on demand and does not stay running. This is a simpler solution, but can run into problems since we do need to re-sign the executable. |
| 150 | + |
| 151 | +You can directly patch the binary via ghidra, just change the desired code and `file -> export program`. After that, you’ll need to sign the file again as explained in previous sections. Again, if something doesn’t work, the console app is your friend. |
| 152 | + |
| 153 | +# Summary |
| 154 | +In this blog, I covered a beginner-friendly explanation on setting up the environment for dynamically debugging a complied application on MacOS. I discussed how to overcome some of Apple’s protections and provided simple tools to self-diagnose issues that will likely arise. Hopefully this will help you start smoothly when debugging binaries on MacOS. |
0 commit comments