This repository is a WIP of exploits for the (now legacy) Protostar challenge. See https://exploit.education/ for more information. This file documents bits of my though process and notes used for solving the exercises. Since this is a first-time learning experience on the topic for me, the solutions may be far from optimal. The exploits rely on pwntools for generating the payloads, but most of the research process is done with a barebone old gdb since the image is pretty old. I also used Ghidra locally to analyse the binaries. Environment =========== The vm can be downloaded (if needed) and started with `make runvm`. It depends on qemu, and re-binds all the necessary ports on localhost:<port>+10000 (e.g: SSH/22 with `ssh -p10022 user@localhost`). `make connect` is also provided as a helper. If the vm is started, all the exploits can be executed on that vm using `make` or `make all`. A few notes: - running them in parallel (`make -jX`) is supported - having `PWNLIB_DEBUG=1` in the environment is also very helpful since the exploits don't print much information. - a specific exploit can be run by expliciting its name (e.g: `make stack3`) - a specific category of exploit can be run by expliciting its name (e.g: `make stack`) - all the exploits depends on a Python2 virtual env in which pwntools is installed (this is automatic) - all the exploits assert at the end to make sure they did get executed properly. - FIXME: `net1` sometimes fails for some unknown reason. This may be a bug in the exploit, in the remote service or in pwntools. Investigation needed. Feel free to reuse that Makefile as a building block for solving all the exercises yourself. Exploits: net ============= First time using pwntools, nothing fancy. Packing and unpacking routines. Exploits: stack =============== stack5 ------ Coredumps are not enabled by default, but we can rely on dmesg to get information of the ip and sp after triggering a crash. NOTE0: we do grep the process to make it possible to run exploits in parallel NOTE1: values can be different depending on the env (shell, gdb, ...). NOTE2: it might be possible to use pwntools to leak that (see DynELF). We then reconstruct eax (pointer to the user buffer before call to gets()) just like main() does. Leaked sp is the value after main's ret. -4 because ret poped ip from the stack, and -4 again to account for the push ebp at the start of main. The rest corresponds to what's found in main() to build gets's argument as eax. .-- eax (buf, shellcode, 61616161) our ip target (61616174) v v | 0x50 - 0x10 | align (& ~0xF) | ebp pushed by main | __lib_start_main+X | 4 4 ^ sp at crash ---' -> buf = ((sp - 8) & ~0xf) - 0x40 stack6+7 -------- Same exploit (symlinked)... we should probably try another approach for stack6? Exploits: format ================ format1 ------- This one was the hardest challenge so far. Spent 3 full days on it. . . 4 | bp_vuln +================ (printf) 4 | vuln+X (call) +---------------- <-- sp_vuln ($0, content not leakable as "$0" is not valid) | fmt ptr 0x18 |---------------- $1 | ... .. +---------------- <-- bp_vuln $6 4 | bp_main +================ (vuln) $7 4 | main+X (call) +---------------- <-- sp_main $8 (content leakable!) | fmt ptr o------------------. 0x10 |---------------- | | ... | +---------------- | ? | align & ~0xf | +---------------- <-- bp_main | 4 | bp_start | +================ (main) | 4 | start+X (call) | +---------------- | ? | ... | +---------------- <------------------' (misalign possible) ? | "<fmt> ... '\0' +---------------- ? | ... `---------------- 0xbfffffff (stack) A few things that made this so hard for me: - The userinput is through program arguments, which means every different length will cause different stack addresses and alignments - Figuring out the printf arg index of argv[1] required a minimum of 2 pointers leaks - The index may not lend in the exact aligned position of argv[1] Did I miss something? format2 ------- Much simpler (a few minutes). . . +---------------- <-- sp_vuln ("$0") 4 | buf ptr o--. |---------------- | 4 | size (512) | |---------------- | 4 | stream (stdin) | |---------------- | 4 | ... | +---------------- <--' 0x208 | buf +---------------- <-- bp_vuln ? | ... `---------------- 0xbfffffff (stack) format3 ------- Tricky writing procedure, cute. . . +================ (printf) | printfbuffer+X +---------------- <-- sp_printbuffer ($0) | buf ptr o-------------------. 0x18 |---------------- | $1 | ... | .. +---------------- <-- bp_printbuffer | $6 4 | bp_vuln | +================ (printbuffer) | $7 4 | vuln+X | +---------------- <-- sp_vuln | $8 4 | buf ptr o--. | |---------------- | | $9 4 | size (512) | | |---------------- | | $10 4 | stream (stdin) | | |---------------- | | $11 4 | ... | | +---------------- <--'----------------' $12 0x208 | buf +---------------- <-- bp_vuln | bp_main +================ (vuln) ? | ... `---------------- 0xbfffffff (stack) format4 ------- Actually trivial if relying on pwntools for the payload. . . +================ (printf) 4 | vuln+X +---------------- <-- sp_vuln $0 | buf ptr o--. |---------------- | $1 | size (512) | |---------------- | $2 0x218 | stream (stdin) | |---------------- | $3 | ... | |---------------- <--' $4 | buf +---------------- <-- bp_vuln ? | ... `---------------- 0xbfffffff (stack) Exploits: heap ============== heap0 ----- In gdb: - bp before mallocs - info proc mapping malloc(64) 0x804a000 0x806b000 0x21000 0 [heap] -> user ptr: 0x804a008 (0x804a000+0x08) -> 8B prefix -> dump binary memory /tmp/dump.bin 0x804a000 0x806b000 -> hexdump -C /tmp/dump.bin .-- malloc(64) v 00000000 00 00 00 00 49 00 00 00 00 00 00 00 00 00 00 00 |....I...........| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00000040 00 00 00 00 00 00 00 00 00 00 00 00 b9 0f 02 00 |................| ^ ^^^^^^^^^^^ | `-- end of malloc(64) 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00021000 malloc(4) 0x804a000 0x806b000 0x21000 0 [heap] -> user ptr: 0x804a050 (0x804a000+0x50) -> unchanged -> dump binary memory /tmp/dump.bin 0x804a000 0x806b000 -> hexdump -C /tmp/dump.bin .-- malloc(64) v 00000000 00 00 00 00 49 00 00 00 00 00 00 00 00 00 00 00 |....I...........| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00000040 00 00 00 00 00 00 00 00 00 00 00 00 11 00 00 00 |................| ^ ^^^^^^^^^^^ | `-- end of malloc(64) .-- malloc(4) v 00000050 00 00 00 00 00 00 00 00 00 00 00 00 a9 0f 02 00 |................| =========== ^ ^^^^^^^^^^^ | `-- end of malloc(4) 00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00021000 Pretty destructive approach in the exploit but can't be helped due to not being able to insert zeroes (strcat). I could get away without looking at the malloc implementation this time. Not sure if it's going to last. heap1 ----- 00000000 00 00 00 00 11 00 00 00 01 00 00 00 18 a0 04 08 |................| ^^^^^^^^^^^ ^^^^^^^^^^^ a->val a->data 00000010 00 00 00 00 11 00 00 00 41 42 43 44 45 46 47 00 |........ABCDEFG.| ^^^^^^^^^^^^^^^^^^^^^^^ data (a) 00000020 00 00 00 00 11 00 00 00 02 00 00 00 38 a0 04 08 |............8...| ^^^^^^^^^^^ ^^^^^^^^^^^ b->val b->data 00000030 00 00 00 00 11 00 00 00 61 62 63 64 65 66 67 00 |........abcdefg.| ^^^^^^^^^^^^^^^^^^^^^^^ data (b) 00000040 00 00 00 00 c1 0f 02 00 00 00 00 00 00 00 00 00 |................| heap2 ----- This one has a bug in the auth struct size, which makes the code vulnerable to a heap overflow. According to the description, this is not what is supposed to be exploited, but instead the stale pointer (use after free). There is also a small bug in the "service" string length (not honoring the space). Anyway, a reset order frees the pointer without null-ing it, so the next alloc re-uses the same address and we can write whatever we want to control its content. heap3 ----- Heap evolution: 3x malloc: 00000000 00 00 00 00 29 00 00 00 00 00 00 00 00 00 00 00 |....)...........| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 29 00 00 00 |............)...| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000050 00 00 00 00 29 00 00 00 00 00 00 00 00 00 00 00 |....)...........| 00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000070 00 00 00 00 00 00 00 00 00 00 00 00 89 0f 00 00 |................| - 0x804c000 0x804d000 0x1000 0 [heap] - p0 = malloc(0x20) -> 0x804c008 - p1 = malloc(0x20) -> 0x804c030 - p2 = malloc(0x20) -> 0x804c058 3x strcpy (0x20 sized null-terminated arguments): 00000000 00 00 00 00 29 00 00 00 [61 61 61 61 62 61 61 61 |....)...aaaabaaa| 00000010 63 61 61 61 64 61 61 61 65 61 61 61 66 61 61 61 |caaadaaaeaaafaaa| 00000020 67 61 61 61 68 61 61 00] 00 00 00 00 29 00 00 00 |gaaahaa.....)...| 00000030 [69 61 61 61 6a 61 61 61 6b 61 61 61 6c 61 61 61 |iaaajaaakaaalaaa| 00000040 6d 61 61 61 6e 61 61 61 6f 61 61 61 70 61 61 00] |maaanaaaoaaapaa.| 00000050 00 00 00 00 29 00 00 00 [71 61 61 61 72 61 61 61 |....)...qaaaraaa| 00000060 73 61 61 61 74 61 61 61 75 61 61 61 76 61 61 61 |saaataaauaaavaaa| 00000070 77 61 61 61 78 61 61 00] 00 00 00 00 89 0f 00 00 |waaaxaa.........| free(p2): 00000000 00 00 00 00 29 00 00 00 [61 61 61 61 62 61 61 61 |....)...aaaabaaa| 00000010 63 61 61 61 64 61 61 61 65 61 61 61 66 61 61 61 |caaadaaaeaaafaaa| 00000020 67 61 61 61 68 61 61 00] 00 00 00 00 29 00 00 00 |gaaahaa.....)...| 00000030 [69 61 61 61 6a 61 61 61 6b 61 61 61 6c 61 61 61 |iaaajaaakaaalaaa| 00000040 6d 61 61 61 6e 61 61 61 6f 61 61 61 70 61 61 00] |maaanaaaoaaapaa.| 00000050 00 00 00 00 29 00 00 00 [00 00 00 00 72 61 61 61 |....).......raaa| ^^^^^^^^^^^ in payload + no other change 00000060 73 61 61 61 74 61 61 61 75 61 61 61 76 61 61 61 |saaataaauaaavaaa| 00000070 77 61 61 61 78 61 61 00] 00 00 00 00 89 0f 00 00 |waaaxaa.........| free(p1): 00000000 00 00 00 00 29 00 00 00 [61 61 61 61 62 61 61 61 |....)...aaaabaaa| 00000010 63 61 61 61 64 61 61 61 65 61 61 61 66 61 61 61 |caaadaaaeaaafaaa| 00000020 67 61 61 61 68 61 61 00] 00 00 00 00 29 00 00 00 |gaaahaa.....)...| 00000030 [50 c0 04 08 6a 61 61 61 6b 61 61 61 6c 61 61 61 |P...jaaakaaalaaa| ^^^^^^^^^^^ 0x804c058-8 (p2) 00000040 6d 61 61 61 6e 61 61 61 6f 61 61 61 70 61 61 00] |maaanaaaoaaapaa.| 00000050 00 00 00 00 29 00 00 00 [00 00 00 00 72 61 61 61 |....).......raaa| 00000060 73 61 61 61 74 61 61 61 75 61 61 61 76 61 61 61 |saaataaauaaavaaa| 00000070 77 61 61 61 78 61 61 00] 00 00 00 00 89 0f 00 00 |waaaxaa.........| free(p0): .-- p0 (0x804c008) v 00000000 00 00 00 00 29 00 00 00 [28 c0 04 08 62 61 61 61 |....)...(...baaa| ~~~~~~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^ p0 meta 0x0804c028 (p1-0x8), written on free(p0) 00000010 63 61 61 61 64 61 61 61 65 61 61 61 66 61 61 61 |caaadaaaeaaafaaa| 00000020 67 61 61 61 68 61 61 00] 00 00 00 00 29 00 00 00 |gaaahaa.....)...| ~~~~~~~~~~~~~~~~~~~~~~~ p1 meta .-- p1 (0x804c030) v 00000030 [50 c0 04 08 6a 61 61 61 6b 61 61 61 6c 61 61 61 |P...jaaakaaalaaa| ^^^^^^^^^^^ 0x0804c050 (p2-0x8), written on free(p1) 00000040 6d 61 61 61 6e 61 61 61 6f 61 61 61 70 61 61 00] |maaanaaaoaaapaa.| .-- p2 (0x804c058) v 00000050 00 00 00 00 29 00 00 00 [00 00 00 00 72 61 61 61 |....).......raaa| ~~~~~~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^ p2 meta NULL (no p3), written on free(p2) 00000060 73 61 61 61 74 61 61 61 75 61 61 61 76 61 61 61 |saaataaauaaavaaa| 00000070 77 61 61 61 78 61 61 00] 00 00 00 00 89 0f 00 00 |waaaxaa.........| ^^^^^^^^^^^^^^^^^^^^^^^ footer post 3allocs (d9 -> b1 -> b9) Bins evolution: (gdb) info address av_ Symbol "av_" is static storage at address 0x804b160. (gdb) x/20x 0x804b160 p0 malloc: 0x804b160 <av_>: 0x00000049 0x00000000 0x00000000 0x00000000 0x804b170 <av_+16>: 0x00000000 0x00000000 0x00000000 0x00000000 0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c028 (p1-0x8) <- next in line // this is _av[11] 0x804b190 <av_+48>: 0x00000000 0x00000000 0x00000000 0x0804b194 0x804b1a0 <av_+64>: 0x0804b194 0x0804b19c 0x0804b19c 0x0804b1a4 ... p1 malloc: 0x804b160 <av_>: 0x00000049 0x00000000 0x00000000 0x00000000 0x804b170 <av_+16>: 0x00000000 0x00000000 0x00000000 0x00000000 -0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c028 (p1-0x8) <- give this one +0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c050 (p2-0x8) <- next in line 0x804b1a0 <av_+64>: 0x0804b194 0x0804b19c 0x0804b19c 0x0804b1a4 p2 malloc: 0x804b160 <av_>: 0x00000049 0x00000000 0x00000000 0x00000000 0x804b170 <av_+16>: 0x00000000 0x00000000 0x00000000 0x00000000 -0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c050 (p2-0x8) <- give this one +0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c078 (potential p3-0x8) <- next in line 0x804b1a0 <av_+64>: 0x0804b194 0x0804b19c 0x0804b19c 0x0804b1a4 p2 free: -0x804b160 <av_>: 0x00000049 0x00000000 0x00000000 0x00000000 -0x804b170 <av_+16>: 0x00000000 0x00000000 0x00000000 0x00000000 +0x804b160 <av_>: 0x00000048 0x00000000 0x00000000 0x00000000 +0x804b170 <av_+16>: 0x0804c050 0x00000000 0x00000000 0x00000000 0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c078 0x804b190 <av_+48>: 0x00000000 0x00000000 0x00000000 0x0804b194 0x804b1a0 <av_+64>: 0x0804b194 0x0804b19c 0x0804b19c 0x0804b1a4 p1 free: 0x804b160 <av_>: 0x00000048 0x00000000 0x00000000 0x00000000 -0x804b170 <av_+16>: 0x0804c050 0x00000000 0x00000000 0x00000000 +0x804b170 <av_+16>: 0x0804c028 0x00000000 0x00000000 0x00000000 0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c078 0x804b190 <av_+48>: 0x00000000 0x00000000 0x00000000 0x0804b194 0x804b1a0 <av_+64>: 0x0804b194 0x0804b19c 0x0804b19c 0x0804b1a4 p0 free: 0x804b160 <av_>: 0x00000048 0x00000000 0x00000000 0x00000000 -0x804b170 <av_+16>: 0x0804c028 0x00000000 0x00000000 0x00000000 +0x804b170 <av_+16>: 0x0804c000 0x00000000 0x00000000 0x00000000 0x804b180 <av_+32>: 0x00000000 0x00000000 0x00000000 0x0804c078 0x804b190 <av_+48>: 0x00000000 0x00000000 0x00000000 0x0804b194 0x804b1a0 <av_+64>: 0x0804b194 0x0804b19c 0x0804b19c 0x0804b1a4 Random notes: - allocation header metadata is 8 bytes containing 2 offsets we'll call "next" and "prev". - sequence is: 3x malloc, 3x strcpy, 3x free, 1x puts. Our main lever is during the strcpys to affect the following frees. And we likely want to affect the puts by redirecting it to the winner function. - during the strcpy themselves, we can directly corrupt the following free(p2) and free(p1), respectively by overflowing p1 and p0. - looking at free() decompiled code, we can see can distinguish a few writing scenarios: * writing of pointers at symbols av_ (approximately 284 intptr), without much control over the content. We have ways of altering where we write in av_ though. * writing of pointers within payload at the end of free (we see that in dumps above), but very little control available here * 2 weird data swap The data swap is what gives us most control, so let's decompose it. Pseudo code is basically: a = z[2] b = z[3] a[3] = b b[2] = a Where z is an offset dynamically calculated according to p and its prev metadata. The swap can be represented like this: . . +---------------- z 4 | +---------------- z + 1 4 | +---------------- z + 2 (p) 4 | a (input) o----------. +---------------- z + 3 | 4 | b (input) o------. | +---------------- | | . | | . | | +---------------- <------'-- | --. 4 | | | +---------------- b + 1 | | 4 | | | +---------------- b + 2 | | 4 | a (output) o------. | | +---------------- | | | . | | | . | | | +---------------- <------'---' | 4 | | +---------------- a + 1 | 4 | | +---------------- a + 2 | 4 | | +---------------- a + 3 | 4 | b (output) o--------------' +---------------- . . We want to basically swap got.puts and winner, but the swap has the side effect of trying to write into winner's bytecode. This means we need an intermediate shellcode/jumper. Our a and b would be got.puts and the jumper. The simplest way to access the jumper seems to be the heap as it seems we can't really write what we want to av_ (too bad since the address doesn't change). We could also consider the stack (through argv) if the heap pointer wasn't guessable but the stack was. Applying the swap layout to our case would look like this after the swap is executed: . . +---------------- z 4 | +---------------- z + 1 4 | +---------------- z + 2 4 | sh o-------------. +---------------- z + 3 | 4 | got.puts-8 o--------. | +---------------- | | . | | . | | +---------------- <--------'--- | ---. got.puts-8 4 | | | +---------------- got.puts-4 | | 4 | | | +---------------- got.puts | | 4 | sh o--------. | | +---------------- | | | . | | | . | | | +---------------- sh <-----'----' | | jmp winner | | | 0xc | ... | | | | | +---------------- sh + 0xc | 4 | got.puts-8 o------------------' +---------------- . . Conditions required for the swap to work: - our jumper must fit within 12B (not problematic) - meta_next > _av[0] (0x49 initially, so 0x50) - meta_next & (1<<1) == 0 (true with 0x50) - meta_next & (1<<0) == 0 (true with 0x50) - find meta_prev such that z is the closest possible to our allocated data We have z = p - 8 - meta_prev. To have z+2 and z+3 (swap parameters) as close to p as possible, we would pick meta_prev=0, but this isn't possible because of strcpy, so instead we will arbitrarily pick -4: . . +---------------- p - 8 (meta_prev) 4 | 0xfffffffc (-4) +---------------- p - 4 (meta_next), z 4 | 0x50 +---------------- p 4 | <whatever> +---------------- z + 2 4 | sh (p1+0xc) o--------. +---------------- z + 3 | 4 | got.puts-8 | +---------------- <--------' sh | mov eax, winner 0xc | jmp eax | +---------------- 4 | <swap garbage dst> +---------------- . . Now we have the choice of when to execute the swap: in free(p2) or free(p1). I picked free(p1) so we corrupt the memory as late as possible. Also one cool thing about picking free(p1) and 0x50 for the swap trigger condition is that is it makes sure only one swap is triggered. Indeed, the condition on av_[11] that prevents the second swap is true with and only with meta_next == 0x50: indeed, _av[11] == 0x0804c078 is compared with p1 - 8 + (0x50 & 0xfffffffc). Doing the swap in free(p2) is definitely possible (the initial exploit was actually doing that) but it requires faking a 4th allocation at p1-8+0x50 with something like 0x1 (it needs to meet the condition x&1) in its meta_next. One thing I'm not satisfied with though is that the exploit has to hardcode the exploit heap address of the shellcode. I tried to corrupt the metadata such that free() would actually write the pointer itself in the "swap configuration" for the next free, but couldn't manage to do that. SAD.