"TPM DRM" In Mac OS X: A Myth That Won't Die

© Amit Singh. All Rights Reserved. Written in December 2007

Beating a Dead Horse

Apple began shipping x86-based Macintosh computers in early 2006. Even before that, people had noted the presence of a Trusted Platform Module (TPM) in the prerelease Developer Transition Kit machines. There had been much talk and much furor about the system-wide "DRM" the TPM would enforce. However, the x86-based Macintoshes were released with the TPM being not used—for anything.

In October 2006, I wrote about the TPM and its "use" in Mac OS X. Since Apple provided no software or firmware drivers for the TPM, I also wrote and released an open-source TPM driver for Mac OS X. In particular, I said the following.

"Apple's TPM Keys"

The media has been discussing "Apple's use of TPM" for a long time now. There have been numerous reports of system attackers bypassing "Apple's TPM protection" and finding "Apple's TPM keys." Nevertheless, it is important to note that Apple does not use the TPM. If you have a TPM-equipped Macintosh computer, you can use the TPM for its intended purpose, with no side effect on the normal working of Mac OS X.

This turned out to be rather hard for many to believe. After all, Apple was somehow tying Mac OS X to their own hardware; and the early x86 Macs did have onboard TPMs. Some people concluded—incorrectly so—that the TPM must be involved. As is often the norm on the Internet, such conclusions transmogrified into infallible myths. I found the nonsense rather hard to believe—I had expected the open-source TPM driver to dispel such myths. The driver and associated software allowed the user to do "whatever" with the TPM: enable it, use it for their own purposes, even disable it—without affecting the normal functioning of Mac OS X. Besides, the TPM was a common piece of hardware in modern-day computers, and indeed, it often sat unused in computers from several vendors. In the summer of 2006, when the Mac Pro was introduced, x86-based Macs stopped having onboard TPMs altogether. Even then, conspiracy theorists still came up with explanations, as they now concluded that the TPM somehow must have been incorporated with the CPU.

Around the same time in 2006, I also wrote about kernel-level binary protection in Mac OS X. As that article explains, it is actually binary protection that ties Mac OS X to a specific class of hardware. The mechanism is partly implemented as a special-purpose virtual memory (VM) pager that is interposed between the kernel's higher layer and the vnode pager.

Nevertheless, to this day, the myth continues. The TPM, although uninvolved in any Mac-related DRM (and non-existent in Macs for a long while now), often ends up being cannon fodder in Mac-related religious warfare.

The Key to Understanding: Simple Minded Calculations

So what's missing from the popular understanding of this issue? The keys to understanding how it works are all there in the aforementioned articles on TPM and binary protection. Well, almost all there. There's the small matter of the "key". Since we are talking about binary encryption, what about the key to decrypt? I've been often asked about the nature of this key—in particular, where does it come from?

Well, the key itself wasn't so interesting to me since I'm not allergic to Apple hardware. However, not knowing this bit of information seems to have led some to the "logical" conclusion that the key must be held within the... TPM! Perhaps the TPM that's "inside the CPU"? Except that there's no such thing. Ironically, it appears that even those who initially subverted Mac OS X to intercept the keys believed them to be "TPM keys".

My interest in this is no different from my academic interest in the working of Mac OS X. The same interest had led to Mac OS X Internals, whose goal was to answer many, many questions. Here, we have a case where the wrong answer has taken a life of its own. I can't help but find such semi-wilful ignorance annoying.

What is the correct answer?

The key (actually, a pair of 32-byte values) comes from the System Management Controller (SMC). Unlike in the case of a TPM, accessing this key involves no cryptography, no random numbers, no hardware security—it's merely obfuscation. Just as you can use I/O Kit interfaces to retrieve motion sensor data and numerous other readings from the SMC, you can retrieve the key—no number crunching involved. You don't even need superuser privileges. In fact, assuming you know how to access hardware from user-space, a program to do this would be quite straightforward to write on Mac OS X—perhaps around 50 lines of C.

Figure 1 shows such a program.

Why Obfuscate?

I view the obfuscation approach as engineering pragmaticism in solving difficult problems. The problem here was that of making Mac OS X reasonably difficult to deploy (from a legal standpoint—that is, would involve reverse engineering, breaking the EULA, etc.) on non-Apple hardware. A solution based on the TPM would have been fraught with numerous problems for the developers and maintainers of the solution. As is not uncommon in such cases, obfuscation can be an effective enough solution.

/* * smc_read.c: Written for Mac OS X 10.5. Compile as follows: * * gcc -Wall -o smc_read smc_read.c -framework IOKit */
#include <stdio.h> #include <IOKit/IOKitLib.h> typedef struct { uint32_t key; uint8_t __d0[22]; uint32_t datasize; uint8_t __d1[10]; uint8_t cmd; uint32_t __d2; uint8_t data[32]; } AppleSMCBuffer_t; int main(void) { io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSMC")); if (!service) return -1; io_connect_t port = (io_connect_t)0; kern_return_t kr = IOServiceOpen(service, mach_task_self(), 0, &port); IOObjectRelease(service); if (kr != kIOReturnSuccess) return kr; AppleSMCBuffer_t inputStruct = { 'OSK0', {0}, 32, {0}, 5, }, outputStruct; size_t outputStructCnt = sizeof(outputStruct); kr = IOConnectCallStructMethod((mach_port_t)port, (uint32_t)2, (const void*)&inputStruct, sizeof(inputStruct), (void*)&outputStruct, &outputStructCnt); if (kr != kIOReturnSuccess) return kr; int i = 0; for (i = 0; i < 32; i++) printf("%c", outputStruct.data[i]); inputStruct.key = 'OSK1'; kr = IOConnectCallStructMethod((mach_port_t)port, (uint32_t)2, (const void*)&inputStruct, sizeof(inputStruct), (void*)&outputStruct, &outputStructCnt); if (kr == kIOReturnSuccess) for (i = 0; i < 32; i++) printf("%c", outputStruct.data[i]); printf("\n");
return IOServiceClose(port); }

Figure 1. Reading information from the SMC


Anybody's Protected Binaries

With this knowledge, there's a particularly interesting experiment you can do. You know that the operating system knows how to execute binaries protected (encrypted) with predefined keys. You understand how this mechanism works. You also know where the keys come from. Therefore, you should be able to protect your own binaries and have the system execute them—that'd be kind of neat.

Figure 2 shows a program that will create an encrypted copy of the given Mach-O executable. (This program is a bit longer than the one in Figure 1 because it tries to do sanity checking on the input and handles some error cases.) Specifically, it will encrypt the binary's text segment. Recall from our discussion of protected binaries that the first 3 pages of a protected segment are not encrypted. Therefore, the binary that you wish to encrypt must have a text segment larger than 3 pages in size. (That may not be the case with a typical "Hello, World!" C program.)

/* * apb_encrypt.c: Written for Mac OS X 10.5. Compile as follows: * * gcc -Wall -o apb_encrypt apb_encrypt.c -framework IOKit -lcrypto */ #include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <stdint.h> #include <unistd.h> #include <sys/types.h> #include <arpa/inet.h> #include <copyfile.h> #include <mach/mach.h> #include <mach/machine.h> #include <mach-o/fat.h> #include <mach-o/loader.h> #include <openssl/aes.h> #include <IOKit/IOKitLib.h> #define APB_UNPROTECTED_HEADER_SIZE (3 * PAGE_SIZE) #define APB_CRYPT_AES_KEY_SIZE (256) #define APB_FAT_MAX_ARCH (5) static char header_page[PAGE_SIZE]; static char data_page[PAGE_SIZE]; static char xcrypted_page[PAGE_SIZE]; static boolean_t apb_initialize(int, AES_KEY*, AES_KEY*); static int apb_encrypt_page(const void*, void*); static io_connect_t AppleSMC_Connect(void); static void AppleSMC_Disconnect(io_connect_t); static IOReturn AppleSMC_Read32(io_connect_t, uint32_t, uint8_t*); typedef struct { uint32_t key; uint8_t __d0[22]; uint32_t datasize; uint8_t __d1[10]; uint8_t cmd; uint32_t __d2; uint8_t data[32]; } AppleSMCBuffer_t; static io_connect_t AppleSMC_Connect(void) { io_connect_t port = (io_connect_t)0; io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSMC")); if (!service) { return port; } kern_return_t kr = IOServiceOpen(service, mach_task_self(), 0, &port); IOObjectRelease(service); if (kr != kIOReturnSuccess) { return (io_connect_t)0; } return port; } static void AppleSMC_Disconnect(io_connect_t port) { (void)IOServiceClose(port); } static IOReturn AppleSMC_Read32(io_connect_t port, uint32_t key, uint8_t* data32) { AppleSMCBuffer_t inputStruct = { 0, { 0 }, 32, { 0 }, 5, }; AppleSMCBuffer_t outputStruct; size_t outputStructCnt = sizeof(outputStruct); inputStruct.key = key; IOReturn kr = IOConnectCallStructMethod((mach_port_t)port, (uint32_t)2, (const void*)&inputStruct, sizeof(inputStruct), (void*)&outputStruct, &outputStructCnt); if (kr != kIOReturnSuccess) { return kr; } (void)memcpy(data32, outputStruct.data, 32); return kIOReturnSuccess; } static boolean_t apb_initialize(__unused int mode, AES_KEY* key1, AES_KEY* key2) { boolean_t result = FALSE; io_connect_t port = AppleSMC_Connect(); if (!port) { return result; } IOReturn ret; uint8_t data32[32] = { 0 }; ret = AppleSMC_Read32(port, 'OSK0', data32); if (ret != kIOReturnSuccess) { goto out; } AES_set_encrypt_key(data32, APB_CRYPT_AES_KEY_SIZE, key1); ret = AppleSMC_Read32(port, 'OSK1', data32); if (ret != kIOReturnSuccess) { goto out; } AES_set_encrypt_key(data32, APB_CRYPT_AES_KEY_SIZE, key2); result = TRUE; out: AppleSMC_Disconnect(port); return result; } static int apb_encrypt_page(const void* in, void* out) { static AES_KEY key1, key2; static boolean_t initialized = FALSE; if (initialized == FALSE) { initialized = apb_initialize(AES_ENCRYPT, &key1, &key2); if (initialized == FALSE) { return -1; } } const unsigned char* _in = (const unsigned char*)in; unsigned char* _out = (unsigned char*)out; unsigned char apb_null_iv1[AES_BLOCK_SIZE] = { 0x0, }; unsigned char apb_null_iv2[AES_BLOCK_SIZE] = { 0x0, }; AES_cbc_encrypt(_in, _out, PAGE_SIZE / 2, &key1, apb_null_iv1, AES_ENCRYPT); _in += (PAGE_SIZE / 2); _out += (PAGE_SIZE / 2); AES_cbc_encrypt(_in, _out, PAGE_SIZE / 2, &key2, apb_null_iv2, AES_ENCRYPT); return 0; } int main(int argc, char** argv) { int fd_in = -1; int fd_out = -1; if (argc != 3) { fprintf(stderr, "usage: %s <infile> <outfile>\n", argv[0]); exit(1); } fd_in = open(argv[1], O_RDONLY); if (fd_in < 0) { perror("open"); exit(1); } off_t base = (off_t)0; off_t ebase_begin = (off_t)0; off_t ebase_end = (off_t)0; uint32_t n = 0; int ret = 0; ssize_t nbytes = pread(fd_in, header_page, PAGE_SIZE, (off_t)0); if (nbytes != PAGE_SIZE) { ret = -1; goto out; } uint32_t magic = *(uint32_t*)header_page; struct mach_header* mh = (struct mach_header*)0; #ifdef __LITTLE_ENDIAN__ if (magic == FAT_CIGAM) { struct fat_header* fh = (struct fat_header*)header_page; uint32_t nfat_arch = ntohl(fh->nfat_arch); if (nfat_arch > APB_FAT_MAX_ARCH) { fprintf(stderr, "too many architectures in Universal binary\n"); ret = -1; goto out; } struct fat_arch* fa = (struct fat_arch*)((char*)header_page + sizeof(struct fat_header)); for (n = 0; n < nfat_arch; n++, fa++) { if (ntohl(fa->cputype) == CPU_TYPE_X86) { base = (off_t)ntohl(fa->offset); nbytes = pread(fd_in, header_page, PAGE_SIZE, base); if (nbytes != PAGE_SIZE) { fprintf(stderr, "failed to read Universal binary\n"); ret = -1; goto out; } mh = (struct mach_header*)header_page; break; } } } else if (magic == MH_MAGIC) { mh = (struct mach_header*)header_page; if (mh->cputype != CPU_TYPE_X86) { fprintf(stderr, "this program supports only x86 architecture\n"); ret = -1; goto out; } } else { fprintf(stderr, "not an appropriate Mach-O file\n"); ret = -1; goto out; } #else #error This file can only be compiled on Intel. #endif struct segment_command* text = (struct segment_command*)0; uint32_t ncmds = mh->ncmds; struct load_command* lc = (struct load_command*)((char*)mh + sizeof(struct mach_header)); for (n = 0; n < ncmds; n++) { if (lc->cmd == LC_SEGMENT) { struct segment_command* sc = (struct segment_command*)lc; if (strcmp(sc->segname, SEG_TEXT) == 0) { text = sc; break; } } lc = (struct load_command*)((char*)lc + lc->cmdsize); } if (!text) { fprintf(stderr, "failed to find text segment\n"); ret = -1; goto out; } if (text->flags & SG_PROTECTED_VERSION_1) { fprintf(stderr, "already encrypted\n"); ret = -1; goto out; } if (text->filesize < APB_UNPROTECTED_HEADER_SIZE) { fprintf(stderr, "text segment is too small to protect\n"); ret = -1; goto out; } off_t archbase_begin = (off_t)(text->fileoff + APB_UNPROTECTED_HEADER_SIZE); off_t archbase_end = archbase_begin + (off_t)(text->filesize - APB_UNPROTECTED_HEADER_SIZE); ebase_begin = base + archbase_begin; ebase_end = base + archbase_end; fd_out = open(argv[2], O_RDWR | O_CREAT | O_EXCL, 0755); if (fd_out < 0) { perror("open"); ret = -1; goto out; } ret = fcopyfile(fd_in, fd_out, (copyfile_state_t)0, COPYFILE_ALL); if (ret) { perror("copyfile"); ret = -1; goto out; } text->flags |= SG_PROTECTED_VERSION_1; nbytes = pwrite(fd_out, header_page, PAGE_SIZE, base); if (nbytes != PAGE_SIZE) { perror("pwrite"); ret = -1; goto out; } off_t count = ebase_end - ebase_begin; if (count % PAGE_SIZE) { fprintf(stderr, "text segment not a multiple of page size\n"); ret = -1; goto out; } while (count > 0) { nbytes = pread(fd_in, data_page, PAGE_SIZE, ebase_begin); if (nbytes != PAGE_SIZE) { perror("pread"); ret = -1; goto out; } ret = apb_encrypt_page(data_page, xcrypted_page); if (ret) { fprintf(stderr, "failed to encrypt page\n"); goto out; } nbytes = pwrite(fd_out, xcrypted_page, PAGE_SIZE, ebase_begin); if (nbytes != PAGE_SIZE) { perror("pwrite"); ret = -1; goto out; } ebase_begin += (off_t)PAGE_SIZE; count -= (off_t)PAGE_SIZE; } ret = 0; out: if (fd_in >= 0) { close(fd_in); } if (fd_out >= 0) { close(fd_out); if (ret) { unlink(argv[2]); } } exit(ret); }

Figure 2. Creating a protected binary


Let us look at our encryption program in action. Recall from the binary protection discussion that the LC_SEGMENT load command for an encrypted segment has a special bit (0x8) set. In that discussion, we used the otool command-line program to view the load commands in a Mach-O file. Figure 3 shows an excerpt from the output of running otool on the binaries for ls and the Finder. We see that the flags field has the 0x8 bit set in the Finder's case, which is a protected binary, but not in the case of ls.

$ otool -l /bin/ls /bin/ls: Load command 0 ... Load command 1 cmd LC_SEGMENT cmdsize 260 segname __TEXT vmaddr 0x00001000 vmsize 0x00005000 fileoff 0 filesize 20480 maxprot 0x00000007 initprot 0x00000005 nsects 3 flags 0x0 Section ... $ cd /System/Library/CoreServices/Finder.app/Contents/MacOS $ otool -l Finder Finder: Load command 0 ... Load command 1 cmd LC_SEGMENT cmdsize 668 segname __TEXT vmaddr 0x00001000 vmsize 0x00404000 fileoff 0 filesize 4210688 maxprot 0x00000007 initprot 0x00000005 nsects 9 flags 0x8 Section ...

Figure 3. Using otool to view Mach-O load commands


Let us now create a protected copy of ls. Figure 4 shows a sequence of commands to do that assuming you've downloaded apb_encrypt.c to /tmp/.

$ cd /tmp $ gcc -Wall -o apb_encrypt apb_encrypt.c -framework IOKit -lcrypto $ cp /bin/ls ./ls $ ./apb_encrypt ls ls.protected $ ls -l ls* total 288 144 -r-xr-xr-x 1 you wheel 73696 Jan 31 22:52 ls 144 -r-xr-xr-x 1 you wheel 73696 Jan 31 22:54 ls.protected $ diff ls ls.protected Binary files ls and ls.protected differ $ otool -l ls.protected ... Load command 1 cmd LC_SEGMENT cmdsize 260 segname __TEXT vmaddr 0x00001000 vmsize 0x00005000 fileoff 0 filesize 20480 maxprot 0x00000007 initprot 0x00000005 nsects 3 flags 0x8 Section ... $ strings ls ... usage: ls [-ABCFGHLPRSTWabcdefghiklmnopqrstuwx1] [file ...] humanize_number buf != NULL humanize_number.c suffix != NULL ... $ strings ls.protected ... q9D@ J[19 :Jk# .Wnu `=SI ... $ otool -t -v -V ls.protected | grep 'bad opcode' ... 0000510c .byte 0xb2 #bad opcode 000051b9 .byte 0xf1 #bad opcode 000051c8 .byte 0x8e #bad opcode 000051f1 .byte 0x77 #bad opcode ... $ ./ls.protected -l total 288 -r-xr-xr-x 1 you wheel 73696 Jan 31 22:52 ls -r-xr-xr-x 1 you wheel 73696 Jan 31 22:54 ls.protected

Our protected version of ls is automagically decrypted at runtime by the operating system. If you're so inclined, you could even encrypt your entire system (!) this way with your own keys. Of course, you'd have to re-encrypt the system's apple-protected binaries with your own keys too, and write a kernel extension that sets your keys up as the ones to be used by the operating system. You'll then have a system unlike any other. (Don't do it—I wasn't being serious.)