Midnightsun CTF 2019 took place from April 5th, 16:00 to April 6th, 16:00 UTC.
This CTF is organized by HackingForSoju, a high level Swedish CTF team.
I played this CTF as a member of team dcua
.
We got 7640pts in total and won the 1st place!
I solved 3 challenges and gained 1598pts.
The challenges were really hard and all sophisticated. Thank you @HackingForSoju for the amazing CTF! I really enjoyed it.
- [crypt 223pts] EZDSA (57 solves)
- [programming 482pts] polyshell (22 solves)
- [network 893pts] Dr. Evil (8 solves)
[crypt 223pts] EZDSA (57 solves)
Description: Someone told me not to use DSA, so I came up with this. Service: nc ezdsa-01.play.midnightsunctf.se 31337 File: EZDSA.tar.gz
We are given a python script named ezdsa.py
:
from hashlib import sha1 from Crypto import Random from flag import FLAG class PrivateSigningKey: def __init__(self): self.gen = 0x44120dc98545c6d3d81bfc7898983e7b7f6ac8e08d3943af0be7f5d52264abb3775a905e003151ed0631376165b65c8ef72d0b6880da7e4b5e7b833377bb50fde65846426a5bfdc182673b6b2504ebfe0d6bca36338b3a3be334689c1afb17869baeb2b0380351b61555df31f0cda3445bba4023be72a494588d640a9da7bd16L self.q = 0x926c99d24bd4d5b47adb75bd9933de8be5932f4bL self.p = 0x80000000000001cda6f403d8a752a4e7976173ebfcd2acf69a29f4bada1ca3178b56131c2c1f00cf7875a2e7c497b10fea66b26436e40b7b73952081319e26603810a558f871d6d256fddbec5933b77fa7d1d0d75267dcae1f24ea7cc57b3a30f8ea09310772440f016c13e08b56b1196a687d6a5e5de864068f3fd936a361c5L self.key = int(FLAG.encode("hex"), 16) def sign(self, m): def bytes_to_long(b): return long(b.encode("hex"), 16) h = bytes_to_long(sha1(m).digest()) u = bytes_to_long(Random.new().read(20)) assert(bytes_to_long(m) % (self.q - 1) != 0) k = pow(self.gen, u * bytes_to_long(m), self.q) r = pow(self.gen, k, self.p) % self.q s = pow(k, self.q - 2, self.q) * (h + self.key * r) % self.q assert(s != 0) return r, s
We can get the for an arbitrary message.
So, we know and we want to know . is randomly generated and is unknown so far.
By Fermat's little theorem, is equivalent to . So, can be written as . However, we don't know the value of , so let's control it.
By Euler's criterion, must be either 1 or -1. We will get if is quadratic residue modulo , otherwise or . Let's see what happens when we give as .
1. Sign protocol 2. Quit 1 Enter data: STZM6SXqato9bbrezJnvRfLJl6U= (698847418084580852997663919979623019513778951409L, 629758878500372559472644038362239654961033814558L) Options: 1. Sign protocol 2. Quit 1 Enter data: STZM6SXqato9bbrezJnvRfLJl6U= (698847418084580852997663919979623019513778951409L, 629758878500372559472644038362239654961033814558L)
Yay! We get the same results regardless of the random value .
>>> pow(g, 1, p) % q 698847418084580852997663919979623019513778951409 >>> pow(g, q-1, p) % q 703683376471181625136032923000137438354697349212
It seems is quadratic residue, so is 1 now!
Thus, we can calculate the value for .
import hashlib def egcd(a, b): if a == 0: return (b, 0, 1) else: g, y, x = egcd(b % a, a) return (g, x - (b // a) * y, y) def modinv(a, m): g, x, y = egcd(a, m) if g != 1: raise Exception('modular inverse does not exist') else: return x % m k = 1 g = 0x44120dc98545c6d3d81bfc7898983e7b7f6ac8e08d3943af0be7f5d52264abb3775a905e003151ed0631376165b65c8ef72d0b6880da7e4b5e7b833377bb50fde65846426a5bfdc182673b6b2504ebfe0d6bca36338b3a3be334689c1afb17869baeb2b0380351b61555df31f0cda3445bba4023be72a494588d640a9da7bd16 q = 0x926c99d24bd4d5b47adb75bd9933de8be5932f4b p = 0x80000000000001cda6f403d8a752a4e7976173ebfcd2acf69a29f4bada1ca3178b56131c2c1f00cf7875a2e7c497b10fea66b26436e40b7b73952081319e26603810a558f871d6d256fddbec5933b77fa7d1d0d75267dcae1f24ea7cc57b3a30f8ea09310772440f016c13e08b56b1196a687d6a5e5de864068f3fd936a361c5 m = (q - 1) // 2 h = int(hashlib.sha1(b''.fromhex(hex(m)[2:])).hexdigest(), 16) r = 698847418084580852997663919979623019513778951409 s = 629758878500372559472644038362239654961033814558 x = (s * k - h) * modinv(r, q) % q print(b''.fromhex(hex(x)[2:]))
$ python solve.py b'th4t_w4s_e4sy_eh?'
[programming 482pts] polyshell (22 solves)
Description: You might be cool, but are you 5 popped shells cool? Service: nc polyshell-01.play.midnightsunctf.se 30000
Let's connect to the server.
$ nc polyshell-01.play.midnightsunctf.se 30000 Welcome to the polyglot challenge! Your task is to create a shellcode that can run on the following architectures: x86 x86-64 ARM ARM64 MIPS-LE The shellcode must run within 1 second(s) and may run for at most 100000 cycles. The code must perform a syscall on each platform according to the following paramters: Syscall number: 150 Argument 1: 54562 Argument 2: A pointer to the string "together" You submit your code as a hex encoded string of max 4096 characters (2048 bytes) Your shellcode:
So, we have to send a shellcode which executes a system call of a specified number with two arguments given after the connection. The point is the shellcode need to run on x86, x86-64, arm, arm64, mips! Is that really possible? Anyway let's work on it.
It's easy to write a code for each architecture but it's next to impossible to make it run on those 5 architectures. So, let's make a jump header to accomplish it. Our goal is to write a shellcode that jumps to arbitrary addresses for each architecture. We have to choose our mnemonic carefully not to crash the program.
As I googled, I found this repository which is similar to this challenge. It's a shellcode that works for x86, x86-64, arm, arm64. First, it distinguishes the intel and arm architecture by a small jump.
; arm andlo r0, r0, #0xeb000 ; arm64 orr w11, w23, #7 ; x86 jmp $+0xa / junk ; x86_64 jmp $+0xa / junk db 0xeb, (_x86 - $ - 2), 0x00, 0x32
And next, it distinguishes the arm and arm64 architecture.
; arm b _arm ($+0x10) ; arm64 ands x1, x0, x0 db ((_arm - $ - 8) / 4), 0x00, 0x00, 0xea ; arm64 b _arm64 ($+0x14) db ((_arm64 - $) / 4), 0x00, 0x00, 0x14
After the small jump for the intel architecture, it checks x86 and x86-64 by a condition jump.
; x86 xor eax, eax; ; x86_64 xor eax, eax; xor eax, eax ; x86 inc eax ; x86_64 REX + nop db 0x40 nop jz _x86_64
It seems very helpful. (Don't forget to change the jump offset.)
First of all, I made the shellcode for the 5 architectures. The pwntools is useful for crafting a shellcode.
x86:
shellcode_x86 = asm( """ mov eax, {arg2_1} push eax mov eax, {arg2_2} push eax mov ebx, {arg1} mov ecx, esp mov eax, {sysnum} int 0x80 """.format( arg1 = arg1, arg2_1 = u32(arg2[4:8]), arg2_2 = u32(arg2[0:4]), sysnum = sysnum ), arch = 'x86', os = 'linux', bits = 32 )
shellcode_x86_64 = asm( """ xor rax, rax push rax mov rdx, {arg2} push rdx mov rdi, {arg1} mov rsi, rsp mov rax, {sysnum} syscall nop """.format( arg1 = arg1, arg2 = u64(arg2), sysnum = sysnum, ), arch = 'amd64', os = 'linux', bits = 64 )
ARM:
shellcode_arm = asm( """ adr r1, arg2 movw r0, #{arg1_1} movt r0, #{arg1_2} mov r7, #{sysnum} svc 0 arg2: .ascii "{arg2}" """.format( arg1_1 = arg1 & 0xFFFF, arg1_2 = arg1 >> 16, arg2 = arg2.rstrip("\x00"), sysnum = sysnum ), arch = 'arm', os = 'linux', bits = 32 )
ARM64:
shellcode_arm64 = asm( """ adr x1, arg2 movz x0, #{arg1_1} movk x0, #{arg1_2}, lsl #16 mov x8, #{sysnum} svc 0 arg2: .ascii "{arg2}" """.format( arg1_1 = arg1 & 0xFFFF, arg1_2 = arg1 >> 16, arg2 = arg2.rstrip("\x00"), sysnum = sysnum ), arch = 'aarch64', os = 'linux', bits = 64 )
MIPS-LE:
shellcode_mips = asm( """ xor $v0, $v0, $v0 xor $a0, $a0, $a0 li $v0, {sysnum} li $a0, {arg1} li $t7, {arg2_1} sw $t7, -12($sp) li $t7, {arg2_2} sw $t7, -8($sp) sw $zero, -4($sp) la $a1, -12($sp) syscall """.format( arg1 = arg1, arg2_1 = u32(arg2[0:4]), arg2_2 = u32(arg2[4:8]), sysnum = sysnum ), arch = 'mips', os = 'linux', endian = 'little' )
We can check if our shellcode is correct just by sending them separately to the server. I found the 5 shellcode worked by themselves.
OK, so the problem is MIPS. I inserted the jump code for MIPS between the first small jump and the ARM detector.
# MIPS : b shellcode_mips shellcode += "\x22\x00\x00\x10"
This jump code is interpreted as a garbage in ARM and ARM64. However, it didn't work unless the next opecode (as MIPS) is valid. So, this works, for example:
b shellcode_mips li $v0, 1234
but this doesn't work:
b shellcode_mips b shellcode_mips
I don't know why it happens but I had to find a useful gadget which can be interpreted as a harmless operation in MIPS, ARM and ARM64. After many attempts, I found the next opecode can be used:
# MIPS : andi $v0, $v0, 1 # ARM : subcc r0, r2, r1 # ARM64: adr x1, 0x84001 shellcode += "\x01\x00B0"
This is the script which constructs the shellcode.
from pwn import * sock = remote("polyshell-01.play.midnightsunctf.se", 30000) sock.recvuntil("Syscall number: ") sysnum = int(sock.recvline()) sock.recvuntil("Argument 1: ") arg1 = int(sock.recvline()) sock.recvuntil("string \"") arg2 = sock.recvuntil("\"")[:-1] print("Syscall number = {}".format(sysnum)) print("Argument 1 = {}".format(arg1)) print("Argument 2 = '{}'".format(arg2)) assert len(arg2) < 8 arg2 += '\x00' * (8 - len(arg2)) shellcode = "" # x86 : jmp loader # x86-64: jmp loader # ARM : andlo r0, r0, #0xeb000 # ARM64 : orr w11, w23, #7 shellcode += "\xeb\x12\x0f\x32" # MIPS : b shellcode_mips # ARM : subcc r0, r2, r1 # ARM64: adr x1, 0x84001 shellcode += "\x22\x00\x00\x10" shellcode += "\x01\x00B0" # ARM : b shellcode_arm # ARM64 : ands x1, x0, x0 shellcode += "\x11\x00\x00\xea" # ARM64 : b shellcode_arm64 shellcode += "\x19\x00\x00\x14" ## loader # x86 : dec eax; xor eax, eax; # x86-64: xor rax, rax; shellcode += "\x48\x31\xc0" # x86 : inc eax; nop; # x86-64: REX; nop; shellcode += "\x40\x90" # x86 : jz shellcode_x86 # x86-64: jz shellcode_x86 shellcode += "\x74\x1a" # ^ header end ^ print(hex(len(shellcode))) shellcode_x86 = asm( """ mov eax, {arg2_1} push eax mov eax, {arg2_2} push eax mov ebx, {arg1} mov ecx, esp mov eax, {sysnum} int 0x80 """.format( arg1 = arg1, arg2_1 = u32(arg2[4:8]), arg2_2 = u32(arg2[0:4]), sysnum = sysnum ), arch = 'x86', os = 'linux', bits = 32 ) print("shellcode_x86 length = " + hex(len(shellcode_x86))) shellcode_x86_64 = asm( """ xor rax, rax push rax mov rdx, {arg2} push rdx mov rdi, {arg1} mov rsi, rsp mov rax, {sysnum} syscall nop """.format( arg1 = arg1, arg2 = u64(arg2), sysnum = sysnum, ), arch = 'amd64', os = 'linux', bits = 64 ) print("shellcode_x86_64 length = " + hex(len(shellcode_x86_64))) shellcode_arm = asm( """ adr r1, arg2 movw r0, #{arg1_1} movt r0, #{arg1_2} mov r7, #{sysnum} svc 0 arg2: .ascii "{arg2}" """.format( arg1_1 = arg1 & 0xFFFF, arg1_2 = arg1 >> 16, arg2 = arg2.rstrip("\x00"), sysnum = sysnum ), arch = 'arm', os = 'linux', bits = 32 ) shellcode_arm64 = asm( """ adr x1, arg2 movz x0, #{arg1_1} movk x0, #{arg1_2}, lsl #16 mov x8, #{sysnum} svc 0 arg2: .ascii "{arg2}" """.format( arg1_1 = arg1 & 0xFFFF, arg1_2 = arg1 >> 16, arg2 = arg2.rstrip("\x00"), sysnum = sysnum ), arch = 'aarch64', os = 'linux', bits = 64 ) shellcode_mips = asm( """ xor $v0, $v0, $v0 xor $a0, $a0, $a0 li $v0, {sysnum} li $a0, {arg1} li $t7, {arg2_1} sw $t7, -12($sp) li $t7, {arg2_2} sw $t7, -8($sp) sw $zero, -4($sp) la $a1, -12($sp) syscall """.format( arg1 = arg1, arg2_1 = u32(arg2[0:4]), arg2_2 = u32(arg2[4:8]), sysnum = sysnum ), arch = 'mips', os = 'linux', endian = 'little' ) shellcode += shellcode_x86 shellcode += shellcode_x86_64 shellcode += shellcode_arm shellcode += shellcode_arm64 shellcode += "\x00" * (8 - (len(shellcode) % 8)) shellcode += shellcode_mips #print("===== x86 =====") #print(disasm(shellcode_x86, bits=32, os='linux', arch='x86')) #print("===== x86-64 =====") #print(disasm(shellcode_x86_64, bits=64, os='linux', arch='amd64')) #print("===== arm =====") #print(disasm(shellcode_arm, bits=32, os='linux', arch='arm')) #print("===== arm64 =====") #print(disasm(shellcode_arm64, bits=64, os='linux', arch='aarch64')) #print(disasm(shellcode[0x15:], bits=32, os='linux', arch='x86')) #print(disasm(shellcode[0x15:], bits=64, os='linux', arch='x86-64')) #print(disasm(shellcode, bits=32, os='linux', arch='arm')) #print(disasm(shellcode, bits=64, os='linux', arch='aarch64')) #print(disasm(shellcode, bits=32, os='linux', arch='mips')) sock.sendline(shellcode.encode("hex")) sock.interactive()
Yay!
$ python solve.py [+] Opening connection to polyshell-01.play.midnightsunctf.se on port 30000: Done Syscall number = 200 Argument 1 = 41880 Argument 2 = 'include' 0x1b shellcode_x86 length = 0x1a shellcode_x86_64 length = 0x23 [*] Switching to interactive mode You submit your code as a hex encoded string of max 4096 characters (2048 bytes) Your shellcode: Results: x86: Success x86-64: Success ARM: Success ARM64: Success MIPS: Success Congratulations! Here is your flag: midnight{Its_shellz_all_the_w4y_d0wn} [*] Got EOF while reading in interactive $
[network 893pts] Dr. Evil (8 solves)
Description: We have managed to intercept communications from Dr. Evil's base but it seems to be encrypted. Can you recover the secret message. File: dr_evil.tar.gz
I didn't tried this chall but aventador, one of the team member, found something curious and told me about that. The evil bits for some of the packets are set to 1. We gathered all of the evil bits for the IP layer but found nothing.
I found the packets whose evil bit is set are sent by 52.15.194.28. So, I focused on the packets from only that sender and gathered the evil bits. Since I didn't know the beggining of the message, I shifted the result bit by bit and found the flag.
from scapy.all import * data = 0 def analyse(pkt): global data if pkt[IP].src != "52.15.194.28": return evil = int(pkt[IP].flags) >> 2 data |= evil data <<= 1 sniff(offline="dr-evil.pcap", filter="ip", store=0, prn=analyse) print(b''.fromhex(hex(data >> 1)[2:1000]))
Great.
$ python solve.py reading from file dr-evil.pcap, link-type LINUX_SLL (Linux cooked) b'Ladies and gentlemen, welcome to my underground lair. I have gathered here before me the world\'s deadliest assassins. And yet, each of you has failed to kill Austin Powers and submit the flag "midnight{1_Milli0n_evil_b1tz!}". That makes me angry. And when Dr. Evil gets angry, Mr. Bigglesworth gets upset. And when Mr. Bigglesworth gets upset, people DIE!!\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'