-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathHowToFindAndExploitBugsIniBootv3.txt
More file actions
436 lines (314 loc) · 35.3 KB
/
Copy pathHowToFindAndExploitBugsIniBootv3.txt
File metadata and controls
436 lines (314 loc) · 35.3 KB
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
How to Find and Exploit Bugs in iBoot (part 2)
================================================
Ok, I didn't want to just do a repeat of last year's presentation incase we had some repeat attendees. So, this year I decided I'd picking up from where I left off on fuzzing for bugs in iBoot, specifically in the HFS code. Last time I went through the boot up process of the iPhone, and demonstrated how to fuzz a very difficult layer of iBoot using my open source iPhone bootloader payload cyanide. It has ability for dumping memory, mounting filesystems, loading files from the filesystem, setting breakpoints and watchpoints, and much more. Essentially everything we'll need to investigate a crash and discover if it's exploitable, and how to proceed exploiting it. In order to avoid finding new bugs or that my bug would be patched in 6.x or 7.x I'm going to continue using iOS 5.1.1. First we're going to fuzz for a little bit until we get a specific crash, and then we're going to analyze this crash and figure out if it's exploitable in anyway. Finally we'll demonstrate how and if this crash can be exploited. This is going to be another advanced class and not too many hands on labs, but lots of good knowledge I promise. Feel free to speak up if you have any questions at all. Last time I showed how we could invalidate the LLB on the device so it would always boot up into DFU mode. This was critical so we wouldn't need to manually place the device into DFU mode every time the device crashed. Using cyanide and it's automatically function finding abilities we will do this again before we start where we left off on fuzzing iBoot for vulnerabilities. This will also be a quick refresher for people who weren't here last year. After booting up cyanide on the device, we get a list of all functions added in cyanide we can start with a quick "go help" command to see all the available commands.
================================================
> go help
Commands:
bootx boot a kernel cache at specified address
reboot reboot the device
reset reset the device
bgcolor set the display background color
setpicture set the image on the display
go jump directly to address
ramdisk create a ramdisk from the specified address
devicetree create a device tree from the specified address
getenv get environment variable over usb
setenv set an environment variable
saveenv save current environment to flash
ticket load in an apticket for firmware verification
help display all available commands
echo write characters back to screen
hexdump dump section of memory to screen
mw write value to specified address
md display value at specified address
call calls a subroutine passing args to it
rdboot patch and boot kernel with ramdisk
fuzz set the current random seed or generate random bytes
test test finding functions offsets
jump shutdown current image and jump into another
function find and display function offsets
nvram view and modify nvram variables
uart read and write data to serial interfaces
fbecho write characters back to framebuffer
radio send AT commands to baseband radio
patch patch firmware in memory
memory display, search, and manipulate memory
string display, search, and manipulate strings
task view and change running tasks
aes encrypt/decrypt kbag aes keys using gid
bdev read or write data to block devices
image display and operate on for firmware images
fs perform operations on the filesystem
kernel operations for filesystem kernel
break create and delete debugging breakpoints
================================================
The "function" command will also give a list of functions cyanide can automatically find in iboot based on known strings contained within the functions. We can then use the "call" command to execute these functions in boot passing in whatever arguments we want.
================================================
> go function list
aes_crypto_cmd: 0x5ff1b0c5
free: 0x5ff14041
fs_mount: 0x5ff1d39d
cmd_go: 0x5ff00edd
image_load: 0x5ff14df1
kernel_load: 0x5ff1608d
task_yield: 0x5ff198fd
default_block_write: 0x5ff12045
uart_read: 0x5ff1048d
uart_write: 0x5ff103c1
task_exit: 0x5ff19b39
dma_set_aes: 0x5ff01261
dma_generate_segments: 0x5ff016d5
dma_generate_aes_segments: 0x5ff016d5
dma_cancel: 0x5ff01451
dma_continue_async: 0x5ff016d5
dma_int_handler: 0x5ff01525
cdma_init: 0x5ff01209
aes_hw_crypto_cmd: 0x5ff01ef2
displaypipe_init: 0x5ff0234e
h2fmi_wait_dma_task_pending: 0x5ff044e9
h2fmi_pio_read_sector: 0x5ff0457d
h2fmi_pio_write_sector: 0x5ff04615
h2fmi_wait_done: 0x5ff04655
_memalign: 0x5ff142bd
_malloc: 0x5ff13f91
sha1_calculate: 0x5ff1bb2d
nand_read_block_hook: 0x5ff0d9c9
uart_set_mode: 0x5ff1063d
uart_set_flow_control: 0x5ff10224
uart_set_baud_rate: 0x5ff106b5
hfs_init: 0x5ff12d75
h2fmiReadBootpage: 0x5ff03189
h2fmiWriteBootpage: 0x5ff02b65
================================================
To overwrite LLB we need to overwrite the boot page of the device. We can clearly see here 2 functions of interest, h2fmiReadBootpage, and h2fmiWriteBootpage. First let's try to read the boot page to make sure we're using the correct arguments for these functions and receiving the correct data. Like I mentioned before, the "call" function allows us to call arbitrary address with arguments. We could either enter the address returned manually, or we can use a shortcut by appending an "@" symbol to the beginning of the function name. The cyanide command processor will automatically search for any known functions and replace the name with the correct address to be called.
================================================
> go echo @h2fmiReadBootpage
0x5ff0318
================================================
You can see it replaced the function name with the correct (or almost correct) address. because iBoot is written in thumb, all functions will start with an odd address. Our "call" function will take care of that though and make sure it adds 1 to the function before calling it. let's test it out.
================================================
> go call @h2fmiReadBootpage 0 2 0x40000000
returned: 00000000
================================================
The first argument passed is the chip ID we want to access, the second argument is which page we want to access, and the last argument is the address we want to read or write from. The function returned 0 which usually mean success, but let's make sure the correct data was returned by checked the data at 0x40000000
================================================
> go hexdump 0x40000000 0x40
0x40000000: 33 67 6d 49 00 0a 02 00 c5 09 02 00 b8 01 02 00 3gmI............
0x40000010: 62 6c 6c 69 45 50 59 54 20 00 00 00 04 00 00 00 blliEPYT .......
0x40000020: 62 6c 6c 69 00 00 00 00 00 00 00 00 00 00 00 00 blli............
0x40000030: 00 00 00 00 41 54 41 44 0c 00 02 00 00 00 02 00 ....ATAD........
================================================
Success!! We've successfully read LLB from the boot page, so now all we need to do it overwrite it right? Unfortunately no, there's actually multiple copies of LLB stored in the beginning of each chip. This is to prevent the possibility of one chip failing. If the BootROM is unable to verify the integrity of the LLB on the first chip, it will then try to read the LLB from the next chip and so on. In order to make sure we always boot into DFU mode we'll need to overwrite the LLB on each chip.
This is a 16GB device, so we have 4 chips containing LLB we need to overwrite.
================================================
> go call @h2fmiWriteBootpage 0 2 0x40000000
returned: 00000000
> go call @h2fmiWriteBootpage 1 2 0x40000000
returned: 00000000
> go call @h2fmiWriteBootpage 2 2 0x40000000
returned: 00000000
> go call @h2fmiWriteBootpage 3 2 0x40000000
returned: 00000000
================================================
Now, let's reboot the device now to ensure that it will always boot up into DFU mode
================================================
> reboot
localhost:greenpois0n posixninja$ irecovery -m
DFU Mode
================================================
Success!! We no long need to worry about booting the device into DFU mode manually after every crash. Now we can get started with our fuzzing much more quickly. I wrote a small bash script to detect which mode the device is in (DFU Mode or Recovery Mode) execute the correct commands to get us into a known state each time. This small script simply checks which mode the device is in and depending on whether it's in DFU mode (Meaning the device crashed) or in recovery mode (meaning the device did not crash). the script will either re-run my ./injectpois0n program or my ./reset.sh script to get the device back into a known state.
================================================
hfs_fuzz.sh:
1 #!/bin/bash
2 for i in {1..1000};
3 do echo "Fuzzing with seed ${i}"
4 if [ "$(irecovery -m)" = "$(echo 'DFU Mode')" ]
5 then ./injectpois0n -c
6 elif [ "$(irecovery -m)" = "$(echo 'Recovery Mode')" ]
7 then ./reset.sh
8 fi
9
10 irecovery -c "go fuzz seed ${i}"
11 irecovery -c "go break add 0x5FF12E3A"
12 irecovery -c "go fs mount nand0a /boot"
13 irecovery -c "go fs load /boot/private/etc/fstab 0x40000000"
14 sleep 1
15
16 if [ "$(irecovery -m)" = "$(echo 'DFU Mode')" ]
17 then echo "Crash with seed ${i}"
18 fi
19 done;
================================================
The reset script is quite simple, it just re-uploads iBEC, prepares it to run my cyanide payload by overwriting the "go" command and then jumps back into it
================================================
reset.sh:
1 #!/bin/bash
2
3 irecovery -qf iBEC.n90ap.new
4 irecovery -c "go image decrypt 0x40000000"
5 irecovery -c "go memory move 0x40000040 0x40000000 0x100000"
6 irecovery -c "go patch firmware 0x40000000 0x100000 command"
7 irecovery -c "go patch firmware 0x40000000 0x100000 permission"
8 irecovery -c "go jump 0x40000000"
9 sleep 5
10 irecovery -qf cyanide/bundles/iBEC/payload
11 irecovery -c "go"
================================================
Let's start our fuzzing process and see how long it takes for us to get a crash….
================================================
localhost:greenpois0n posixninja$ ./hfs_fuzz.sh
Fuzzing with seed 1
Fuzzing with seed 2
Fuzzing with seed 3
Crash with seed 3
================================================
Wow, only took 3 attempts to before the device crashed. Ok, so we found a bug, let's analyze it!!. This is running an ARM architecture so the first thing when analyzing crash reports is to look at the PC and LR, so see if they're in similar ranges of memory. PC contains the Program Counter, the equivalent of EIP on 32bit x86, and LR (Link Register) the equivalent of the return address on 32bit x86. Next check the values of SP (Stack Pointer) the equivalent of ESP on 32bit x86, and R7 (usually called the Frame Pointer) the equivalent of EBP on 32bit x86. If any of these values seem funky, or not similar, you may have a stack buffer overflow on your hands. Although usually they will be similar.
================================================
panic: arm_exception_abort: ARM data abort abort in supervisor mode at 0x5ff1a754 due to translation error:
far 0x60000000 fsr 0x00000857
r0 0x60000000 r1 0x600000d3 r2 0x00000001 r3 0x5ff66e20
r4 0x60000380 r5 0x60000040 r6 0x00000003 r7 0x5ff90424
r8 0x5ff1a754 r9 0x00000b4c r10 0x00000008 r11 0x5ffff380
r12 0x00000000 sp 0x5ff90414 lr 0x5ff20247 pc 0x5ff1a754
================================================
Looking at this crash we can see both PC and LR fall in similar regions of memory (0x5FF00000) where they're suppose to be. Also SP and R7 fall in similar regions (only about 0x10 bytes apart). This obviously isn't a stack buffer overflow. Perhaps checking the disassembly code at PC where it crashed can give us a little more info about why it crashed. We're not going to go through the process of decrypting the firmware, you can find info about that online, we're just going to load it up and taking a peek at where the crash happened. The crash appears to happen here….
================================================
ROM:5FF1A754 ; ---------------------------------------------------------------------------
ROM:5FF1A754
ROM:5FF1A754 loc_5FF1A754 ; DATA XREF: sub_5FF201CC:loc_5FF20236
ROM:5FF1A754 ; ROM:off_5FF20264o
ROM:5FF1A754 MCR p15, 0, R0,c7,c14, 1
ROM:5FF1A758 DMB SY
ROM:5FF1A75C BX LR
================================================
This function is calling an ARM coprocessor to clear the processors cache. Why on early would this be causing a crash? In order to gather a little more information about this bug, we're going to need place a hook on the HFS read function so we can view the arguments that are actually being passed into the function. First let's add a new function pointer to store our old HFS reading function so we can still call it later, and then implement our own function to print out the arguments before passing them to the real HFS read function we've saved
================================================
// Global variable to hold our bdev structure
BdevDescriptor* bdev2 = NULL;
// Function pointer to hold the old bdev read function
unsigned int (*old_hfs_read)(struct BdevDescriptor* bdev,
unsigned char* destination,
unsigned long long source,
unsigned int size) = NULL;
// Our new bdev read function to print out our arguments
unsigned int hfs_read(struct BdevDescriptor* bdev,
unsigned char* destination,
unsigned long long source,
unsigned int size) {
// Print out the arguments passed into the function
printf("hfs_read: 0x%08x 0x%08x 0x%016llx 0x%08x\n", bdev, destination, source, size);
// Pass the arguments into our old hfs read function
ret = old_hfs_read(bdev, 0x5FFA8080, source, size);
// Print out the return value
printf("Returned: %x\n", ret);
// Return the results from the actual HFS read function
return ret;
}
================================================
Now in our breakpoint function where we modified the HFS header we're going to add a few new commands. This will look up the block device structure for nand0a (primary filesystem) and save the old read function so we can call it again later, and replace it with our own read function which we will implement to print out the arguments that were passed into the original read function
================================================
// Replace the bdev read function
bdev2 = bdev_find_device("nand0a");
if (bdev2 != NULL) {
hfs_read = bdev2->read; //*((char**) hfs_read);
bdev2->read = &hfs_read;
}
================================================
Running our fuzzer again, here's the output we get before the crash. (with annotations)
================================================
[bdev] [buffer] [offset] [size]
hfs_read: 0x5ff8e480 0x5ff3da3c 0x000000000100a000 0x00000100
Returned: 100
hfs_read: 0x5ff8e480 0x5ff3db3c 0x000000000080a000 0x00000100
Returned: 100
hfs_read: 0x5ff8e480 0x5ffa6c80 0x0000000001216000 0x00002000
Returned: 2000
hfs_read: 0x5ff8e480 0x5ffa6c80 0x00000000014b4000 0xfff56000
================================================
The first 2 reads were 0x100 bytes long. These reads were the HFS BTree structures and returned their proper sizes, 0x100 bytes. The 3rd read was a normal index block telling HFS where to find the file at with a normal size of 0x2000 bytes. If you noticed, the weird thing about the 4th read is the fact that the length is so long (0xFFF56000). The code must be trying to copy in megabytes worth of stuff into a tiny little 0x2000 byte heap buffer. From this we know we definitely have a heap overflow on our hands. To discover if it's exploitable we'll need to know what data the read function is reading in, if we can control the data being read in, and what data is being overwritten by this data. Figuring out what data is being read in by the function is easy with cyanide. we can use the "bdev" function to directly read bytes in from any blockdevice from any offset we want.
================================================
> go bdev
usage: bdev <list/read/write> <bdev> [source] [size] [destination]
bdev name of blockdevice
source source address of transaction
size size of transaction in bytes
destination destination address of transation
> go bdev list
Block Devices:
(0x5ff8e480) name: nand0a size: 0x48000000
(0x5ff8d380) name: nand0 size: 0xbe5c9000
(0x5ff4c3c0) name: nand_syscfg size: 0x8000
(0x5ff4bc80) name: nand_firmware size: 0x300000
> go bdev read nand0a 0x14b4000 0x2000 0x40000000
reading... done
> go hexdump 0x40000000 0x40
0x40000000: 00 00 02 56 00 00 02 54 ff 01 00 09 00 00 00 2e ...V...T........
0x40000010: 00 00 00 00 2d e7 00 00 00 00 00 11 00 63 00 6f ....-........c.o
0x40000020: 00 6d 00 2e 00 61 00 70 00 70 00 6c 00 65 00 2e .m...a.p.p.l.e..
0x40000030: 00 64 00 65 00 63 00 6d 00 70 00 66 00 73 00 00 .d.e.c.m.p.f.s..
================================================
The data appears to be coming directly from the filesystem, and we even know the exact offset where it will read from!!! This is a very good thing. We now know as long as we can modify the filesystem we can control the exact data being read in and overflowing the heap buffer. Almost seems like this exploit is gonna be a little too easy huh? Unfortunally it's never quite that easy, we still don't know what exactly is being overwritten to cause iBoot the crash. In the crash log it seems to crash when it attempts to write to 0x60000000, so let's see if it crashes if we try yo write to 0x60000000.
================================================
> go mw 0x60000000 0xdeadbeef
> go md 0x60000000
0xdeadbeef
================================================
No it appears we can write to 0x60000000 just fine, so why on earth is iBoot crashing? The only logical explanation would be something being overwritten between our heap buffer and 0x60000000 is causing it to crash once it gets to 0x60000000. Dumping a lot of memory any analyzing it shows there's really only 2 distinct structure between our heap buffer (it's the last buffer allocated in heap so we're not corrupting any other heap buffers). These 2 structures are the stack (but we already know there was no stack corruption based on our earlier analysis), and a special structure in ARM called TLB (Translation Look-aside Buffer). The TLB is a specialized cache that holds a table of physical addresses which are mapped to virtual addresses in the system memory. In iBEC this structure starts at address 0x5FFF8000. This would make since because reason for the crash from the report states "Domain Error". A little research in the ARM technical reference shows that this happens when an illegal virtual to physical page mapping is attempted to be looked up. The TLB is essentially one 32bit address containing representing each physical to virtual mapping on the system.
================================================
> go hexdump 0x5fff8000 0x40
0x5fff8000: 0e 0c f0 5f 32 0c 10 00 32 0c 20 00 32 0c 30 00 ..._2...2. .2.0.
0x5fff8010: 32 0c 40 00 32 0c 50 00 32 0c 60 00 32 0c 70 00 2.@.2.P.2.`.2.p.
0x5fff8020: 32 0c 80 00 32 0c 90 00 32 0c a0 00 32 0c b0 00 2...2...2...2...
0x5fff8030: 32 0c c0 00 32 0c d0 00 32 0c e0 00 32 0c f0 00 2...2...2...2...
================================================
Here we see the beginning of the TLB. In C terms is would look something like…
================================================
uint32_t tlb[4096];
uint32_t* p_tlb = tlb;
================================================
tlb[0] would contain the permissions, domain, and other information for the page 0x00000000-0x00100000.
tlb[1] would contain the permissions, domain, and other information for the page 0x00100000-0x00200000.
iBoot lies at 0x5FF00000, so it's permissions and domain would be at tlb[0x5ff], although here you can see here it's also mapped to virtual address tlb[0] the virtual address for 0x0. This is so ARM knows where to find the exception vectors for IRQ interrupts, syscalls, and panics. Let's see what happens if we overwrite this TLB, and then try to write to 0x60000000.
================================================
> go memory fill 0x5FFF8000 0x0 0x4000
> go hexdump 0x5fff8000 0x40
0x5fff8000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x5fff8010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x5fff8020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x5fff8030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
> go mw 0x60000000 0xdeadbeef
panic: arm_exception_abort: ARM data abort abort in supervisor mode at 0x42003e74 due to translation error:
far 0x60000000 fsr 0x00000805
r0 0x00000003 r1 0x5ff90884 r2 0x60000000 r3 0xdeadbeef
r4 0x5ff00f41 r5 0x00000042 r6 0x5ff389e4 r7 0x5ff90948
r8 0x5ff908b8 r9 0x00000000 r10 0x00000004 r11 0x5ff907e8
r12 0x5ff907b8 sp 0x5ff907c8 lr 0x42003b28 pc 0x42003e74
================================================
Success!! Now we know what data is being overwritten to cause the crash, and we know we can control that data with whatever we want. Now in theory if we can write to the location on the filesystem that is being read from, and we can write this TLB back to the correct location, then we should be able to continue writing past 0x60000000. What good would that do us though if there's no data after 0x60000000. Who knows let's find out…
================================================
> go memory fill 0x60000000 0xEE 0xfff56000
================================================
Well, it didn't crash, but our screen turned blue (which just happens to be the same color as 0xEEEEEEEE). But the devices frame buffer is suppose to be at (insert address). It seems like something is wrapping around. Let's try a few more tests…
================================================
> go md 0x60000000
0xea00000e
> go mw 0x60000000 0xdeadbeef
> go md 0x60000000
0xdeadbeef
> go md 0x40000000
0xdeadbeef
> go mw 0x40000000 0xcafebabe
> go md 0x60000000
0xcafebabe
================================================
It seems like 0x60000000 is mirroring our load address 0x40000000. That's odd, perhaps this mirroring happens every 0x20000000 bytes. Let's check 0x7FF00000 and see if it contains the same data as 0x5FF00000 contains
================================================
> go hexdump 0x7FF00200 0x10
0x7ff00200: 69 42 45 43 20 66 6f 72 20 6e 39 30 61 70 2c 20 iBEC for n90ap,
================================================
Yup, just as suspected, memory is wrapped around every 0x20000000 bytes. Perhaps there's a chance we could rewrite out TLB in place to allow us to write past 0x60000000 and eventually start overwriting iBEC at 0x7FF00000. Let's see if we write to 0x7FF00000 it will also mirror at 0x5FF00000.
================================================
> go mw 0x7FF00000 0x0
panic: task_yield: reset vector overwritten while executing task 'usb req' (0x00000000)
================================================
Looks like there's a chance we might be able to overwrite this after all!!! We also need to know what exactly in the fuzzed input it causing the crash. This can be done easily by taking the known good input, and slowly copying the bad input into it until you find which field is causing it to crash. In this case the culprit was setting header->catalogFile.extents[1].blockCount to a high number. Now we're going to need to write our payload (including our TLB back in it's correct location) from the filesystem and see if we can wrap back around to 0x7FF00000 to overwrite iBEC itself. To write to the filesystem, we're going to need to boot a custom ramdisk containing code to open our filesystem seek to the correct location, and write the data we want precisely where we need it to be. Since this is a block device we can only write in 0x2000 block chunks which is going to make this a little trickier, but who said this was going to be easy? Let's first manually set the header in our breakpoint hook so we can find the address it's trying to read from in the filesystem so we know where to write our payload to.