Over a million developers have joined DZone.

Capturing Program Control with LLDB

DZone's Guide to

Capturing Program Control with LLDB

Today we're going to go over how you can capture control of a program. Once we've described how this works, we'll go through and example with shellcode.

· Mobile Zone ·
Free Resource

At this point, we have set up our development environment with a jailbroken iOS device attached via multiplexed USB to our macs. Now we're going to go over how you can capture control of a program. Keep in mind, we aren't compromising a program yet, we're just going over how it works in a debugger. Once we've described how this works, we'll go through and example with shellcode.

We're going to use this C program to show you how this works:

#include <stdlib.h>

void uncalled(void) {
  int l = 0xdeadd00d;

void call2(void) {
  int k = 0xcafebabe;

void call(void) {
  int j = 0xcafed00d;

int main(int argc, char* argv[]) {
  int i = 0xdeadc0de;
  return 0;

 So a couple of things. First, this program has two levels of function calls - x86 and x64 systems don't need that. ARM systems do, as ARM has a dedicated register that holds the return address after a function call. in x86/64 systems, this address is pushed onto the stack. On ARM, if we have a single level of function calls, the return address is just contained in the LR register. If you have multiple levels of function calls, it's pushed onto the stack. Second, we have an uncalled function in this program. Also note this function is not static - if it is, the compiler will optimize it out, and the function won't exist in your compiled program. If it's not static, it'll still be included.

We're going to simulate a buffer overflow in this example. So let's get moving. Start your debugserver on your iDevice, and bring up lldb on your host. You should see something like this:

$ lldb
Process 4452 stopped
* thread #1: tid = 0xeb308, 0x1feb5000 dyld`_dyld_start, stop reason = signal SIGSTOP
    frame #0: 0x1feb5000 dyld`_dyld_start
->  0x1feb5000 <+0>:  mov    r8, sp
    0x1feb5004 <+4>:  sub    sp, sp, #16
    0x1feb5008 <+8>:  bic    sp, sp, #15
    0x1feb500c <+12>: ldr    r3, [pc, #0x70]           ; <+132>
(lldb) breakpoint set --name main
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb) continue
Process 4452 resuming
Process 4452 stopped
* thread #1: tid = 0xeb308, 0x0000bfb8 function3`main(argc=1, argv=0x00113f44) + 22 at function3.c:18, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000bfb8 function3`main(argc=1, argv=0x00113f44) + 22 at function3.c:18
   15   }
   17   int main(int argc, char* argv[]) {
-> 18     int i = 0xdeadc0de;
   19     call();
   20     return 0;
   21   }

You've set a breakpoint on main, and then continued execution to hit it. Now let's take a look at the functions in the program:

(lldb) image dump symtab function3
Symtab, num_symbols = 10:
               Debug symbol
               |Synthetic symbol
               ||Externally Visible
Index   UserID DSX Type            File Address/Value Load Address       Size               Flags      Name
------- ------ --- --------------- ------------------ ------------------ ------------------ ---------- ----------------------------------
[    0]      0 D   SourceFile      0x0000000000000000                    Sibling -> [    6] 0x00640000 /Users/cclamb/Work/abi-playground/function3.c
[    1]      2 D   ObjectFile      0x000000005803f2ee                    0x0000000000000000 0x00660001 /Users/cclamb/Work/abi-playground/function3.o
[    2]      4 D X Code            0x000000000000bf64 0x0000bf64 0x0000000000000016 0x000f0008 uncalled
[    3]      8 D X Code            0x000000000000bf7a 0x0000bf7a 0x0000000000000010 0x000f0008 call2
[    4]     12 D X Code            0x000000000000bf8a 0x0000bf8a 0x0000000000000018 0x000f0008 call
[    5]     16 D X Code            0x000000000000bfa2 0x0000bfa2 0x0000000000000022 0x000f0008 main
[    6]     20     Code            0x000000000000bfd0 0x0000bfd0 0x0000000000000030 0x001e0000  stub helpers
[    7]     21   X Absolute        0x0000000000004000                    0x0000000000000000 0x00030010 _mh_execute_header
[    8]     26     Trampoline      0x000000000000bfc4 0x0000bfc4 0x000000000000000c 0x00010100 exit
[    9]     27   X Undefined       0x0000000000000000                    0x0000000000000000 0x00010100 dyld_stub_binder

Take a look - we have our functions, including uncalled(.). We also have an entry point for that function: 0xbf64. This is important - write this down for later. There's something I found when compiling 32-bit code (which is what this example is - it's compiled with the target armv7s-apple-ios) - the reported addresses seem to be offset by a byte. Let me show you what I mean.

Set a breakpoint in the call(.) function, continue, and read the LR register:

(lldb) breakpoint set --name call
Breakpoint 2: where = function3`call + 14 at function3.c:13, address = 0x0000bf98
(lldb) continue
Process 4452 resuming
Process 4452 stopped
* thread #1: tid = 0xeb308, 0x0000bf98 function3`call + 14 at function3.c:13, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x0000bf98 function3`call + 14 at function3.c:13
   10   }
   12   void call(void) {
-> 13     int j = 0xcafed00d;
   14     call2();
   15   }
(lldb) register read lr
      lr = 0x0000bfbf  function3`main + 29 at function3.c:19

Note the address - 0xbfbf. Now, let's disassemble main to see the address right after the call to call(.):

(lldb) disassemble --name main
    0xbfa2 <+0>:  push   {r7, lr}
    0xbfa4 <+2>:  mov    r7, sp
    0xbfa6 <+4>:  sub    sp, #0x10
    0xbfa8 <+6>:  movw   r2, #0xc0de
    0xbfac <+10>: movt   r2, #0xdead
    0xbfb0 <+14>: movs   r3, #0x0
    0xbfb2 <+16>: str    r3, [sp, #0xc]
    0xbfb4 <+18>: str    r0, [sp, #0x8]
    0xbfb6 <+20>: str    r1, [sp, #0x4]
    0xbfb8 <+22>: str    r2, [sp]
    0xbfba <+24>: bl     0xbf8a                    ; call at function3.c:12
    0xbfbe <+28>: movs   r0, #0x0
    0xbfc0 <+30>: add    sp, #0x10
    0xbfc2 <+32>: pop    {r7, pc}


This shows the address following the call as 0xbfbe, not 0xbfbf. You can trace through the execution of you'd like - I have, and the address you want to return to is 0xbfbf, so we'll want to use the address 0xbf65, not 0xbf64.

Okay, so now we have an address we want to redirect execution to after we return from call(.). To do that, we're going to overwrite the address on the stack, like this:

(lldb) breakpoint set --name uncalled
(lldb) memory read --format x --size 4 --count 20 $sp
0x00113efc: 0x00000000 0x00113f18 0x0000bfbf 0xdeadc0de
0x00113f0c: 0x00113f44 0x00000001 0x00000000 0x00000000
0x00113f1c: 0x21138873 0x1fef8000 0x00113f2c 0x00000000
0x00113f2c: 0x21138873 0x00000000 0x00000000 0x00000000
0x00113f3c: 0x00004000 0x00000001 0x00113f90 0x00000000

Look! We have the return address at $sp+8. Let's overwrite it with the address of uncalled(.). If this works, we'll hit the breakpoint in uncalled. Let's give it a shot:

(lldb) memory write --size 4 $sp+8 0xbf65
(lldb) continue
Process 4456 resuming
Process 4456 stopped
* thread #1: tid = 0xeb99c, 0x0000bf74 function3`uncalled + 16 at function3.c:4, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x0000bf74 function3`uncalled + 16 at function3.c:4
   1    #include <stdlib.h>
   3    void uncalled(void) {
-> 4      int l = 0xdeadd00d;
   5      exit(0);
   6    }

And we hit the breakpoint! We have successfully taken control of the program and redirected execution to a previously uncalled function. This is essentially what a buffer overflow does - we'll look into that next.

lldb ,cybersecurity ,mobile

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}