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.
Join the DZone community and get the full member experience.
Join For FreeAt 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;
exit(0);
}
void call2(void) {
int k = 0xcafebabe;
}
void call(void) {
int j = 0xcafed00d;
call2();
}
int main(int argc, char* argv[]) {
int i = 0xdeadc0de;
call();
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
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 }
16
17 int main(int argc, char* argv[]) {
-> 18 int i = 0xdeadc0de;
19 call();
20 return 0;
21 }
(lldb)
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 }
11
12 void call(void) {
-> 13 int j = 0xcafed00d;
14 call2();
15 }
16
(lldb) register read lr
lr = 0x0000bfbf function3`main + 29 at function3.c:19
(lldb)
Note the address - 0xbfbf. Now, let's disassemble main to see the address right after the call to call(.):
(lldb) disassemble --name main
function3`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}
(lldb)
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
(lldb)
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>
2
3 void uncalled(void) {
-> 4 int l = 0xdeadd00d;
5 exit(0);
6 }
7
(lldb)
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.
Opinions expressed by DZone contributors are their own.
Comments