Extracting entitlements from process memory using LLDB Mar 30 2020

One of the first steps we take when analysing a macOS (or *OS) app is to go through the entitlements to extract useful information. Usually, we search for the entitlements embedded in the application binary using codesign(1) in our Static Analysis phase. But we could also obtain the entitlements in our Dynamic Analysis phase. As you might have noticed, I like using LLDB as my dynamic analysis tool. In this post, I’m going to share how to extract the entitlements from a binary during our dynamic analysis using LLDB.

I didn’t want only to show you how to extract the entitlements. I want to show you the process of exploration to obtain the entitlements. I do this, hoping that it gives you ideas on how to search for more useful information in your binaries.

Entitlements

Entitlements are stored in XML format. You might have noticed a file with extension .entitlements in your Xcode projects. As you might have guessed, the entitlements are stored in that file when you are developing your project. Once the project is built, and the final application is generated, you’ll find the entitlements embedded in the application binary as part of the embedded code signature.

We can extract the entitlements of a binary using the codesign(1) command. For example, if we run the following command:

1
$ codesign -d --entitlements :- /System/Applications/FindMy.app/Contents/MacOS/FindMy

You should get the entitlements for that binary displayed on your screen. As I’ve mentioned before, that is what we would do during our static-analysis. What we are going to do is extract the embedded entitlements from the memory of the process that we are attached to, using LLDB.

But where, in the process memory, are the entitlements located?

The entitlements are part of the embedded code signature generated when the binary is signed. The code signature is located in the __LINKEDIT segment when the binary is mapped into memory by the dynamic linker (DYLD). If you want to learn more on how it is located there, this article on DYLD by Jonathan Levin will shed some light.

To solve our problem of obtaining the entitlements, what we need to do is: first, search for the embedded signature. And from the embedded signature extract the entitlements.

Before we start with our exploration, let’s create an Xcode project, so we have a binary we control.

Create our Xcode project for analysis

Let’s create a new macOS App project. I’ll name mine “TestSubject”. You can call it however you like. It can be Swift or Objective-C, it won’t affect us, we only need a project we can work with.

Let’s build, run and start searching for the entitlements.

Searching for entitlements

Alright, as you can see there is a file called TestSubject.entitlements if you open it in your terminal you’ll see something similar to this:

1
2
3
4
5
6
7
8
9
10
<?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>com.apple.security.app-sandbox</key>
  <true/>
  <key>com.apple.security.files.user-selected.read-only</key>
  <true/>
</dict>
</plist>

Nothing weird there, let’s add some capabilities so it has more info. In the file navigator click on the project, now select the target TestSubject under Signing & Capabilities add App Groups and add a new group, name it how ever you like, or just use the default. Save and let’s check our entitlements file again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <array>
        <string>$(TeamIdentifierPrefix)</string>
    </array>
    <key>com.apple.security.files.user-selected.read-only</key>
    <true/>
</dict>
</plist>

Build the project again. Now, we can extract the entitlements from the binary. From Xcode File navigator, go to Products and drag TestSubject to your terminal to get the path to your application bundle. From there we can get the entitlements from the binary like this (Your path will be different):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ codesign -d --entitlements :- /Users/derik/Library/Developer/Xcode/DerivedData/TestSubject-djsqcdeqcholkegqpnwpuzlbntgp/Build/Products/Debug/TestSubject.app/Contents/MacOS/TestSubject
Executable=/Users/derik/Library/Developer/Xcode/DerivedData/TestSubject-djsqcdeqcholkegqpnwpuzlbntgp/Build/Products/Debug/TestSubject.app/Contents/MacOS/TestSubject
<?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>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <array>
        <string>D9GYA5MXCN.</string>
    </array>
    <key>com.apple.security.files.user-selected.read-only</key>
    <true/>
    <key>com.apple.security.get-task-allow</key>
    <true/>
</dict>
</plist>

Alright, nothing new just now. Let’s jump into LLDB.

LLDB session

Run our project and pause it to get to LLDB. We gathered from Jonathan Levin’s post that the signature is embedded in the __LINKEDIT segment. Let’s see what we can see there.

1
(lldb) image dump sections TestSubject

From that we get the offset and size of the __LINKEDIT section:

1
2
3
4
 SectID   Type       Load Address               Perm File Off. File Size Flags   Section Name
 ---------- ---------------- --------------------------------------- ---- ---------- ---------- ---------- ----------------------------
 ...
 0x00000500 container    [0x0000000100005000-0x000000010000d000) r-- 0x00005000 0x00007e80 0x00000000 TestSubject.__LINKEDIT

We see that it starts at 0x0000000100005000(Your address might be different) and has a size of 0x00007e80. That means that inside that range of memory, we should fid our entitlements.

Just to make sure it is there, let’s read all that memory and display it on the screen. The amount of information could be overwhelming for big binaries, so see if it makes sense for what you are analysing, for our small binary it should be fine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(lldb) memory read 0x0000000100005000 -c 0x00007e80 --force
...
0x100008640: 64 06 02 01 00 00 00 00 00 00 fa de 71 71 00 00 d...........qq..
0x100008650: 01 d9 3c 3f 78 6d 6c 20 76 65 72 73 69 6f 6e 3d ..<?xml version=
0x100008660: 22 31 2e 30 22 20 65 6e 63 6f 64 69 6e 67 3d 22 "1.0" encoding="
0x100008670: 55 54 46 2d 38 22 3f 3e 0a 3c 21 44 4f 43 54 59 UTF-8"?>.<!DOCTY
0x100008680: 50 45 20 70 6c 69 73 74 20 50 55 42 4c 49 43 20 PE plist PUBLIC 
0x100008690: 22 2d 2f 2f 41 70 70 6c 65 2f 2f 44 54 44 20 50 "-//Apple//DTD P
0x1000086a0: 4c 49 53 54 20 31 2e 30 2f 2f 45 4e 22 20 22 68 LIST 1.0//EN" "h
0x1000086b0: 74 74 70 3a 2f 2f 77 77 77 2e 61 70 70 6c 65 2e ttp://www.apple.
0x1000086c0: 63 6f 6d 2f 44 54 44 73 2f 50 72 6f 70 65 72 74 com/DTDs/Propert
0x1000086d0: 79 4c 69 73 74 2d 31 2e 30 2e 64 74 64 22 3e 0a yList-1.0.dtd">.
0x1000086e0: 3c 70 6c 69 73 74 20 76 65 72 73 69 6f 6e 3d 22 <plist version="
0x1000086f0: 31 2e 30 22 3e 0a 3c 64 69 63 74 3e 0a 09 3c 6b 1.0">.<dict>..<k
0x100008700: 65 79 3e 63 6f 6d 2e 61 70 70 6c 65 2e 73 65 63 ey>com.apple.sec
0x100008710: 75 72 69 74 79 2e 61 70 70 2d 73 61 6e 64 62 6f urity.app-sandbo
0x100008720: 78 3c 2f 6b 65 79 3e 0a 09 3c 74 72 75 65 2f 3e x</key>..<true/>
0x100008730: 0a 09 3c 6b 65 79 3e 63 6f 6d 2e 61 70 70 6c 65 ..<key>com.apple
0x100008740: 2e 73 65 63 75 72 69 74 79 2e 61 70 70 6c 69 63 .security.applic
0x100008750: 61 74 69 6f 6e 2d 67 72 6f 75 70 73 3c 2f 6b 65 ation-groups</ke
0x100008760: 79 3e 0a 09 3c 61 72 72 61 79 3e 0a 09 09 3c 73 y>..<array>...<s
0x100008770: 74 72 69 6e 67 3e 44 39 47 59 41 35 4d 58 43 4e tring>D9GYA5MXCN
0x100008780: 2e 3c 2f 73 74 72 69 6e 67 3e 0a 09 3c 2f 61 72 .</string>..</ar
0x100008790: 72 61 79 3e 0a 09 3c 6b 65 79 3e 63 6f 6d 2e 61 ray>..<key>com.a
0x1000087a0: 70 70 6c 65 2e 73 65 63 75 72 69 74 79 2e 66 69 pple.security.fi
0x1000087b0: 6c 65 73 2e 75 73 65 72 2d 73 65 6c 65 63 74 65 les.user-selecte
0x1000087c0: 64 2e 72 65 61 64 2d 6f 6e 6c 79 3c 2f 6b 65 79 d.read-only</key
0x1000087d0: 3e 0a 09 3c 74 72 75 65 2f 3e 0a 09 3c 6b 65 79 >..<true/>..<key
0x1000087e0: 3e 63 6f 6d 2e 61 70 70 6c 65 2e 73 65 63 75 72 >com.apple.secur
0x1000087f0: 69 74 79 2e 67 65 74 2d 74 61 73 6b 2d 61 6c 6c ity.get-task-all
0x100008800: 6f 77 3c 2f 6b 65 79 3e 0a 09 3c 74 72 75 65 2f ow</key>..<true/
0x100008810: 3e 0a 3c 2f 64 69 63 74 3e 0a 3c 2f 70 6c 69 73 >.</dict>.</plis
0x100008820: 74 3e 0a fa de 0b 01 00 00 12 8a 30 80 06 09 2a t>.........0...*
...
(lldb)

Usually, LLDB only reads a max of 1024 bytes, so we have to use --force flag to read all that data. Scroll up, and you’ll see that the XML entitlements are there. Perfect, now we only need to figure out how to locate it in all that chunk of memory.

Use the source

When reversing, if you have access to the source code, use it, there is no point in making your life harder. I thought I would find useful information by checking the loader.h, which is is the one used when a Mach-O binary is loaded into memory. I’ll save you some time it didn’t have useful information this time, but it was worth a check.

We can find the location of the embedded signature by checking the binary (Mach-O) header. We’ll leave that for another post on Mach-O (Maybe I’ll call it extracting useful information from a binary header).

Anyways, From Jonathan Levin’s post, we know that the header file for code signing is located in the XNU codebase. You should find it in (should is the keyword, the structs have been stripped from the code) the GitHub repository:

https://github.com/apple/darwin-xnu/blob/master/bsd/sys/codesign.h

The structs were defined in the XNU source code, in bsd/sys/codesign.h, but because they have recently been removed, you’ll have to go to an older version that still had the structs. We’ll go to the tag xnu-3789.70.16 to find them. You might wonder how do I knew to go to that specific version? Well, I didn’t, I just used the GitHub interface and checked the file manually from tag to tag, until I find one with the structs, oh the fun parts of reverse engineering.

Here is the link so you can check the code:

https://github.com/apple/darwin-xnu/blob/xnu-3789.70.16/bsd/sys/codesign.h

Alright, that was more promising. As you can see, there is a magic that will identify the embedded signature!

From the source code, this is the pattern we are going to look for:

1
CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0, /* embedded form of signature data */

What we are going to do is use the CSMAGIC_EMBEDDED_SIGNATURE as our guide. We are going to scan the memory for the specific pattern and get the offset that way. If you need a refresher on scanning a process’ memory, you can check my previous post Scanning a process’ memory using LLDB.

A side note on my investigation sessions

We’ll use a Python script to make things easier. I usually use a combination of quick scripts and LLDB commands to verify my ideas. It’s not a precise process. What I’m trying to illustrate to you is how chaotic things typically get on reversing/investigation sessions. What I do is mostly jumping from one idea to another until I find something that might help. Lately, I’ve been taking notes that look more like dialogues with myself, and I can assure you they have been helpful. You should find what works for you, and I encourage you to keep a work journal (it doesn’t need to be physical).

Let’s get back to our task.

Using LLDB’s Python scripting capabilities

Let’s create a python script that will serve as our base for the command we’ll generate in the end. I’ll create my file in ~/.config/lldb/scripts/ and name it entitlements.py. I won’t go into much detail for Python and LLDB scripting capabilities because I already did in the previous post. If you need a refresher, check the previous post.

I’m going to assume that you have the python script memscan.py that we created on the previous post. If you didn’t read it, but you want to follow along here is the code for memscan.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import lldb
import shlex
import optparse

@lldb.command ("memscan")
def scan_memory(debugger, arguments, result, internal_dict):
'
  Displays the offset of the match in memory of the pattern.
'

  process = debugger.GetSelectedTarget().GetProcess()
  target = debugger.GetSelectedTarget()
  module = target.GetModuleAtIndex(0)


  raw_args = shlex.split(arguments)
  if len(raw_args) == 0 or raw_args == ['-h']:
    parser.print_help()
    return


  # `get_values` returns a tuple (pattern_bytes, section) from the command arguments
  # but also makes sure everything is valid
  values = get_values(generate_option_parser(), raw_args, module, result)
  (pattern_bytes, section) = (None, None)
  if result.Succeeded():
    (pattern_bytes, section) = values
  else:
    return

  start_addr = section.GetLoadAddress(target)
  bytes_to_read = section.GetByteSize()
  err = lldb.SBError()
  section_blob = process.ReadMemory(start_addr,bytes_to_read, err)


  offset_found = find_in_blob(section_blob, pattern_bytes)
  if not offset_found:
    print("Pattern not found")
    return
  print("Offset from section {}({}): {}".format(section.GetName(), hex(start_addr), hex(offset_found)))
  print("Full offset: {}".format(hex(start_addr + offset_found)))


def find_in_blob(blob,sequence):
  if(len(blob) < len(sequence)):
    return
  for i, x in enumerate(blob):
    if(bytes(blob[i:i+len(sequence)] == sequence)):
      return i
  return

def get_values(parser, raw_args, module, result):
  try:
    (options, args) = parser.parse_args(raw_args)
  except:
    result.SetError(parser.usage)
    return

  if not args:
    result.SetError("missing pattern to search.\n{}".format(parser.usage))
    return

  pattern = 0 
  try:
    pattern = int(args[0], base=16)
  except:
    result.SetError("parsing argument. Argument should be in hex")
    return

  if not options.endianness in ["big", "little"]:
    result.SetError("endianness can only be 'little' or 'big'")
    return

  if not options.section:
    result.SetError("missing section, please specify section.\n{}".format(parser.usage))
    return

  section = module.FindSection(options.section)
  if not section:
    result.SetError("Couldn't find section: {}".format(options.section))
    return

  pattern_bytes = pattern.to_bytes(pattern.bit_length() // 8,byteorder=options.endianness)
  return (pattern_bytes, section)

def generate_option_parser():
  usage = "usage: memscan <PATTERN> <-s|--section> <SECTION_NAME> [options]"
  parser = optparse.OptionParser(usage=usage)
  parser.add_option("-s", "--section",
           action="store",
           default=None,
           dest="section",
           help="Define the section to search for pattern")
  parser.add_option("-e", "--endianness",
           action="store",
           default='big',
           dest="endianness",
           help="Define pattern endianness")
  return parser

If you didn’t have that script already in your lldbinit you can import it to the LLDB session like this:

1
(lldb) command script import ~/.config/lldb/scripts/memscan.py

And our entitlements.py will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import lldb
import os
import memscan

@lldb.command ("entitlements")
def get_entitlements(debugger, command, result, internal_dict):
'
  Displays the current module entitlements
'

  process = debugger.GetSelectedTarget().GetProcess()
  target = debugger.GetSelectedTarget()
  module = target.GetModuleAtIndex(0)

  section = module.FindSection("__LINKEDIT")
  start_addr = section.GetLoadAddress(target)
  bytes_to_read = section.GetByteSize()
  err = lldb.SBError()
  print("start_addr: {}".format(start_addr))
  print("bytes to read: {}".format(bytes_to_read))
  linkedit_blob = process.ReadMemory(start_addr,bytes_to_read, err)

  #CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0, /* embedded form of signature data */
  match = memscan.find_in_blob(bytes(linkedit_blob),bytes([0xfa,0xde,0x0c,0xc0]))
  print("Offset of match:{}".format(match))

We can load our script:

1
(lldb) command script import ~/.config/lldb/scripts/entitlements.py

And run it:

1
2
3
4
(lldb) entitlements
start_addr: 4294987776
bytes to read: 32768
Offset of match:13088

The offset for you might be different. Remember that is the offset of the section, so we have to add it to the offset of __LINKEDIT (Which we calculated before, in mi case 0x0000000100005000):

1
2
3
(lldb) p/x (0x0000000100005000 + 13088)
(long) $2 = 0x0000000100008320
(lldb)

Let’s see what is in that section of memory (I’ll just read and show 32 words in hex) :

1
2
3
4
5
6
7
8
9
10
11
12
13
# We could have used the LLDB variable $2 but it might be different
# in yoru session and might get you confused, but it's handy.
# The command could have been:
# x/32wx $2 
(lldb) x/32wx 0x0000000100008320
0x100008320: 0xc00cdefa 0x8d170000 0x04000000 0x00000000
0x100008330: 0x2c000000 0x02000000 0x6e020000 0x05000000
0x100008340: 0x2a030000 0x00000100 0x03050000 0x020cdefa
0x100008350: 0x42020000 0x00050200 0x00000100 0x22010000
0x100008360: 0x60000000 0x05000000 0x09000000 0x20830000
0x100008370: 0x0c000220 0x00000000 0x00000000 0x77000000
0x100008380: 0x00000000 0x00000000 0x00000000 0x00000000
0x100008390: 0x00000000 0x00000000 0x00000000 0x00000000

Nice, at least we know we are in the correct place. But we still need to figure out how to find the entitlements inside that blob. If we look at the code in codesign.h we see this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
 * Structure of an embedded-signature SuperBlob
 */
typedef struct __BlobIndex {
    uint32_t type;                  /* type of entry */
    uint32_t offset;                /* offset of entry */
} CS_BlobIndex;

typedef struct __SC_SuperBlob {
    uint32_t magic;                 /* magic number */
    uint32_t length;                /* total length of SuperBlob */
    uint32_t count;                 /* number of index entries following */
    CS_BlobIndex index[];           /* (count) entries */
    /* followed by Blobs in no particular order as indicated by offsets in index */
} CS_SuperBlob;

Maybe, it kind of makes sense. The Super Blob contains an index which is a variable size array of CS_BLobIndex (see struct hack). Let’s try to match it to the data in memory.

If we found a match in the script, we can from that offset extract the length and how many blobs it has.

1
2
3
4
  embedded_sig_length = int.from_bytes(linkedit_blob[match+4:match+8], byteorder='big')
  num_blobs = int.from_bytes(linkedit_blob[match+8:match+0xc], byteorder='big')
  print("Embedded signature length: {}".format(embedded_sig_length))
  print("Number of blobs in signature: {}".format(num_blobs))

We can reload our script and run it:

1
2
3
4
5
6
7
(lldb) command script import ~/.config/lldb/scripts/entitlements.py
(lldb) entitlements
start_addr: 4294987776
bytes to read: 32768
Offset of match:13088
Embedded signature length: 6029
Number of blobs in signature: 4

Remember what we had in memory:

1
2
3
# Just showing the first part
#Magic   Length   Count 
0xc00cdefa 0x8d170000 0x04000000

If our assumption is correct, then what follows is the CS_BlobIndex index[];. We assume it’ll be of size four (our count is 4). Each element of the index is a CS_BlobIndex that has a type and offset (Again, this from the codesign.h):

1
2
3
4
typedef struct __BlobIndex {
    uint32_t type;                  /* type of entry */
    uint32_t offset;                /* offset of entry */
} CS_BlobIndex;

So we can cycle through it. I’ll use a variable current_offset to keep track of the offset of the current blob inside of the array of CS_BlobIndexs. Remember the first 4 bytes are the type and the next 4 are the offset.

1
2
3
4
5
6
7
8
9
10
  current_offset = match+0x8
  for i in range(0,num_blobs):
    print("i:{}".format(i))
    blob_type_offset = current_offset + 4
    blob_type_offset_offset = current_offset + 8
    current_offset = blob_type_offset_offset
    print("-- Offsets: type: {} offset: {}".format(blob_type_offset, blob_type_offset_offset))
    blob_type = int.from_bytes(linkedit_blob[blob_type_offset:blob_type_offset+4], byteorder="big")
    blob_offset = int.from_bytes(linkedit_blob[blob_type_offset_offset:blob_type_offset_offset+4], byteorder="big")
    print("Blob type: {} offset: {}".format(blob_type,blob_offset))

Let’s reload our script and run it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(lldb) command script import ~/.config/lldb/scripts/entitlements.py
(lldb) entitlements
start_addr: 4294987776
bytes to read: 32768
Offset of match:13088
Embedded signature length: 6029
Number of blobs in signature: 4
i:0
-- Offsets: type: 13100 offset: 13104
Blob type: 0 offset: 44
i:1
-- Offsets: type: 13108 offset: 13112
Blob type: 2 offset: 622
i:2
-- Offsets: type: 13116 offset: 13120
Blob type: 5 offset: 810
i:3
-- Offsets: type: 13124 offset: 13128
Blob type: 65536 offset: 1283
(lldb)

Ok, that seems promising. In reality, we haven’t proven anything. We have been assuming and displaying the data in a format that we think is correct so obviously, it’ll have a pattern (The pattern we are presenting). Don’t trick yourself, when reversing make assumptions but know they are still assumptions everything might change if you find some data that invalidate your claim, and you’ll have to adapt.

Ok, if we go back and check the types in codesign.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
 * Magic numbers used by Code Signing
 */
enum {
...
    CSSLOT_CODEDIRECTORY = 0,               /* slot index for CodeDirectory */
    CSSLOT_INFOSLOT = 1,
    CSSLOT_REQUIREMENTS = 2,
    CSSLOT_RESOURCEDIR = 3,
    CSSLOT_APPLICATION = 4,
    CSSLOT_ENTITLEMENTS = 5,

...
    CSSLOT_SIGNATURESLOT = 0x10000,         /* CMS Signature */
...
};

The types 0, 2, and 5 make sense, but the 65536?

1
2
(lldb) p/x 65536
(int) $3 = 0x00010000

We can leave it there or figure out if it makes sense or not. If you read *OS Internals Volume III Chapter 5, you’ll find the confirmation we need. Or we can just add it to our list of assumptions.

So type five is what we are looking for.

Let’s validate it. We got this from our output:

1
Blob type: 5 offset: 810

Alright, so offset from where?

We have this from our output (I’ll add some comments): c start_addr: 4294987776 //offset of __LINKEDIT section bytes to read: 32768 Offset of match:13088 //offset of the magic 0xfade0cc0 Embedded signature length: 6029 Number of blobs in signature: 4

So the offset can’t be from the __LINKEDIT because it’ll fall shorter than the offset of the CSMAGIC_EMBEDDED_SIGNATURE. Alright, let’s try from the offset of the CSMAGIC_EMBEDDED_SIGNATURE:

1
2
3
4
(lldb) p/x 4294987776 + 13088 + 810
(long) $4 = 0x000000010000864a
(lldb) x/s 0x000000010000864a
0x10000864a: "\xfffffffa\xffffffdeqq"

Doesn’t seem to be there. This is the point where you grab a cup of tea and think. Some of you might already know the answer, but for me, it took a while. Let’s fast forward after a walk and some tea (or maybe a couple of days), what makes sense is that that offset is from the start of the CS_BlobIndex.

We know that offset, remember:

1
2
3
4
5
6
7
typedef struct __SC_SuperBlob {
    uint32_t magic;                 /* magic number */
    uint32_t length;                /* total length of SuperBlob */
    uint32_t count;                 /* number of index entries following */
    CS_BlobIndex index[];           /* (count) entries */
    /* followed by Blobs in no particular order as indicated by offsets in index */
} CS_SuperBlob;

So it should be the offset of CSMAGIC_EMBEDDED_SIGNATURE + 8 + the offset we got from the blob index for blob type 5. Let’s see if that works:

1
2
3
4
5
6
7
8
9
10
# Blob type: 5 offset: 810
# 4294987776 - __LINKEDIT
# 13088   - CSMAGIC_EMBEDDED_SIGNATURE
# 8     - CS_BlobIndex index[];
# 810    - offset of CSSLOT_ENTITLEMENTS
(lldb) p/x 4294987776 + 13088 + 8 + 810
(long) $5 = 0x0000000100008652
(lldb) x/s 0x0000000100008652
0x100008652: "<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.application-groups</key>\n\t<array>\n\t\t<string>D9GYA5MXCN.</string>\n\t</array>\n\t<key>com.apple.security.files.user-selected.read-only</key>\n\t<true/>\n\t<key>com.apple.security.get-task-allow</key>\n\t<true/>\n</dict>\n</plist>\n\xfffffffa\xffffffde\v\x01"
(lldb) 

Perfecto!

We got it! Alright, now we can fix our script to only read from memory the size we need. What I’ll do is calculate the offset of the next blob in the CS_BlobIndex index[]. Once I have the offset of the following blob subtract and calculate the size. Our loop will look like this now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  current_offset = match+0x8
  blob_segments_offset = current_offset
  for i in range(0,num_blobs):
    print("i:{}".format(i))
    blob_type_offset = current_offset + 4
    blob_type_offset_offset = current_offset + 8
    current_offset = blob_type_offset_offset
    print("-- Offsets: type: {} offset: {}".format(blob_type_offset, blob_type_offset_offset))
    blob_type = int.from_bytes(linkedit_blob[blob_type_offset:blob_type_offset+4], byteorder="big")
    blob_offset = int.from_bytes(linkedit_blob[blob_type_offset_offset:blob_type_offset_offset+4], byteorder="big")
    print("Blob type: {} offset: {}".format(blob_type,blob_offset))
    if blob_type == 5:
      begin_of_entitlements = blob_segments_offset + blob_offset 
      beginig_of_next_blob = int.from_bytes(linkedit_blob[blob_type_offset_offset+8:blob_type_offset_offset+0xc], byteorder="big")
      end_of_entitlements = blob_segments_offset + beginig_of_next_blob - 8
      content_in_bytes = linkedit_blob[begin_of_entitlements:end_of_entitlements]
      import plistlib
      entitlements = plistlib.loads(content_in_bytes)
      import json
      print(json.dumps(entitlements, indent=2))

We can reload our script and run it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(lldb) command script import ~/.config/lldb/scripts/entitlements.py
(lldb) entitlements
start_addr: 4294987776
bytes to read: 32768
Offset of match:13088
Embedded signature length: 6029
Number of blobs in signature: 4
i:0
-- Offsets: type: 13100 offset: 13104
Blob type: 0 offset: 44
i:1
-- Offsets: type: 13108 offset: 13112
Blob type: 2 offset: 622
i:2
-- Offsets: type: 13116 offset: 13120
Blob type: 5 offset: 810
{
 "com.apple.security.app-sandbox": true,
 "com.apple.security.application-groups": [
  "D9GYA5MXCN."
 ],
 "com.apple.security.files.user-selected.read-only": true,
 "com.apple.security.get-task-allow": true
}
i:3
-- Offsets: type: 13124 offset: 13128
Blob type: 65536 offset: 1283
(lldb) 

Perfect :)

There is one small caveat. When you are running an iOS app on the simulator the entitlements are not located in __LINKEDIT they are in __TEXT.__entitlements (as pointed by our friend Derek which I’ve been mentioning a lot lately, thanks for all your help).

It’s even easier on the simulator, let’s have a look.

On Simulator

If we are working on the simulator the entitlements are located in the __TEXT segment, in the section __entitlements. So to obtain it we can just do a simple:

1
2
3
4
5
6
7
8
9
10
text_section = module.FindSection("__TEXT")
section = text_section.FindSubSection("__entitlements")
start_addr = section.GetLoadAddress(target)
bytes_to_read = section.GetByteSize()
err = lldb.SBError()
entitlements_in_bytes = process.ReadMemory(start_addr,bytes_to_read, err)
import plistlib
entitlements = plistlib.loads(entitlements_in_bytes)
import json
print(json.dumps(entitlements, indent=2))

Great, we don’t need any fancy offset calculations.

In the final script, I’ll add the option to pass a flag to indicate we are in the simulator to check the correct section ([-s|--simulator]).

So here is the whole script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import lldb
import os
import memscan
import plistlib
import json

@lldb.command ("entitlements")
def get_entitlements(debugger, command, result, internal_dict):
'
  Displays the current module entitlements
'

  process = debugger.GetSelectedTarget().GetProcess()
  target = debugger.GetSelectedTarget()
  module = target.GetModuleAtIndex(0)

  command_args = shlex.split(command, posix=True)
  parser = generateOptionParser()
  try:
    (options, args) = parser.parse_args(command_args)
  except:
    result.SetError(parser.usage)
    return

  if command_args == ['-h']:
    parser.print_help()

  section = None
  if options.simulator:
    text_section = module.FindSection("__TEXT")
    section = text_section.FindSubSection("__entitlements")
    start_addr = section.GetLoadAddress(target)
    bytes_to_read = section.GetByteSize()
    err = lldb.SBError()
    entitlements_in_bytes = process.ReadMemory(start_addr,bytes_to_read, err)
    entitlements = plistlib.loads(entitlements_in_bytes)
    print(json.dumps(entitlements, indent=2))
    return

  section = module.FindSection("__LINKEDIT")
  start_addr = section.GetLoadAddress(target)
  bytes_to_read = section.GetByteSize()
  err = lldb.SBError()
  if options.verbose: print("start_addr: {}".format(start_addr))
  if options.verbose: print("bytes to read: {}".format(bytes_to_read))
  linkedit_blob = process.ReadMemory(start_addr,bytes_to_read, err)

  #CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0, /* embedded form of signature data */
  match = memscan.find_in_blob(bytes(linkedit_blob),bytes([0xfa,0xde,0x0c,0xc0]))
  if not match:
    print("Couldn't find embedded signature in section: {}".format(section.GetName()))

  if options.verbose: print("Offset of match:{}".format(match))
  # From codesign.h
  # https://github.com/apple/darwin-xnu/blob/xnu-3789.70.16/bsd/sys/codesign.h
  # /*
  # * Structure of an embedded-signature SuperBlob
  # */
  #typedef struct __BlobIndex {
  # uint32_t type;                  /* type of entry */
  # uint32_t offset;                /* offset of entry */
  #} CS_BlobIndex;
  #
  #typedef struct __SC_SuperBlob {
  # uint32_t magic;                 /* magic number */
  # uint32_t length;                /* total length of SuperBlob */
  # uint32_t count;                 /* number of index entries following */
  # CS_BlobIndex index[];           /* (count) entries */
  # /* followed by Blobs in no particular order as indicated by offsets in index */
  #} CS_SuperBlob;

  embedded_sig_length = int.from_bytes(linkedit_blob[match+4:match+8], byteorder='big')
  num_blobs = int.from_bytes(linkedit_blob[match+8:match+0xc], byteorder='big')
  if options.verbose: print("Embedded signature length: {}".format(embedded_sig_length))
  if options.verbose: print("Number of blobs in signature: {}".format(num_blobs))
  current_offset = match+0x8
  blob_segments_offset = current_offset
  for i in range(0,num_blobs):
    blob_type_offset = current_offset + 4
    blob_type_offset_offset = current_offset + 8
    current_offset = blob_type_offset_offset
    if options.verbose: print("-- Offsets: type: {} offset: {}".format(blob_type_offset, blob_type_offset_offset))
    blob_type = int.from_bytes(linkedit_blob[blob_type_offset:blob_type_offset+4], byteorder="big")
    blob_offset = int.from_bytes(linkedit_blob[blob_type_offset_offset:blob_type_offset_offset+4], byteorder="big")
    if options.verbose: print("Blob type: {} offset: {}".format(blob_type,blob_offset))
    if blob_type == 5:
      begin_of_entitlements = blob_segments_offset + blob_offset 
      beginig_of_next_blob = int.from_bytes(linkedit_blob[blob_type_offset_offset+8:blob_type_offset_offset+0xc], byteorder="big")
      end_of_entitlements = blob_segments_offset + beginig_of_next_blob - 8
      content_in_bytes = linkedit_blob[begin_of_entitlements:end_of_entitlements]
      entitlements = plistlib.loads(content_in_bytes)
      print(json.dumps(entitlements, indent=2))

def generateOptionParser():
  usage = "usage: %prog [options]"
  parser = optparse.OptionParser(usage=usage, prog="entitlements")
  parser.add_option("-s", "--simulator",
           action="store_true",
           default=False,
           dest="simulator",
           help="Specify that the process is running in the simulator")
  parser.add_option("-v", "--verbose",
           action="store_true",
           default=False,
           dest="verbose",
           help="Display much more info")
  return parser 

You can reload the script and run it to verify if it works.

Final Thoughts

We made it. We can now extract the entitlements of the module we are analysing in our LLDB session. You might be wondering why? Why go into all this trouble when you could have those entitlements extracted via codesign?

There is information we can more easily get during runtime, for example with the entitlements we can check which groups the app belongs to. We normally just check the Document directory of an app, but the shared directories are also a good place to search for additional information. Once we have the groups that the app belongs to we can check if there is any shared documents between apps. With a script similar to:

1
2
3
4
5
6
7
8
9
10
def getSharedDirForGroup(group_name):
  command_script = r'''
  @import ObjectiveC;
  @import Foundation;
  [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"'''
  command_script += group_name + '"]'
  d_sbval = executeCommand(command_script)
  if d_sbval.error.fail:
    return str(d_sbval.error)
  return d_sbval.description

Also, this post was not only on obtaining the entitlements, but it was also to show what goes behind the scenes of a dynamic analysis session. Sometimes when we read security reports, we only see the result. We don’t get to see the “cut-scenes” with the errors and the endless days without any success.

The internet now is full of edited content, so maybe this post could also help to shed some light in what goes on when someone is doing some research in a topic they don’t know much about.

Ok, that’s it. I hope this post was helpful.

Related topics/notes of interest


** There is no comment system yet, but you can send me a message on twitter @rderik or send me an email: derik[at]rderik[dot]com.