CTFするぞ

CTF以外のことも書くよ

Midnightsun CTF 2019 Writeup

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.

f:id:ptr-yudai:20190407101604p:plain

The challenges were really hard and all sophisticated. Thank you @HackingForSoju for the amazing CTF! I really enjoyed it.

[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  r, s for an arbitrary message.

 k = g^{um} \mod q

 r = (g^{k} \mod p) \mod q

 s = (k^{q - 2} \mod q)(h + rx) \mod q

So, we know  r, s, g, q, p, m, h and we want to know  x.  u is randomly generated and  k is unknown so far.

By Fermat's little theorem,  s is equivalent to  (k^{-1} \mod q)(h + rx) \mod q. So,  x can be written as  x = (sk - h)r^{-1}. However, we don't know the value of  k, so let's control it.

 k = g^{um} \mod q

By Euler's criterion,  g^{(q-1)/2} must be either 1 or -1. We will get  k = 1 if  g is quadratic residue modulo  q, otherwise  -1 or  1. Let's see what happens when we give  (q-1)/2 as  m.

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  u.

>>> pow(g, 1, p) % q
698847418084580852997663919979623019513778951409
>>> pow(g, q-1, p) % q
703683376471181625136032923000137438354697349212

It seems  g is quadratic residue, so  k is 1 now!

Thus, we can calculate the value for  x.

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
)

x86-64:

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'