CTFZone 2019 Quals Writeup December 16, 2019 # PPC ### Fridge In the $$n\times n$$ matrix, each $$(i,j)$$ operation will add a matrix to the original one, and modulo each entry with some $$P$$. (For the first several levels, $$P=2$$, and $$P=8$$ or $$16$$ later) We can compute a basis to solve this problem. ```python import socket import random import sys from copy import deepcopy _debug=False class Remote: def __init__(self,ip,port): self.s=socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.s.connect((ip,port)) self.buf='' def send(self,s): self.s.send(s.encode()) def recvbyte(self): if len(self.buf)==0: self.buf=self.s.recv(1024).decode().replace('\r','') res=self.buf[0] self.buf=self.buf[1:] if _debug: sys.stdout.write(res) sys.stdout.flush() return res def unrecv(self,s): self.buf=s+self.buf def recvuntil(self,s): res='' while res[-len(s):]!=s: res+=self.recvbyte() return res r=Remote('ppc-fridge.ctfz.one',31337) def getlvl(): s=[] s+=[list(map(lambda x:int(x,16),r.recvuntil('\n').split(' ')))] for i in range(len(s[0])-1): s+=[list(map(lambda x:int(x,16),r.recvuntil('\n').split(' ')))] return s def test(x,y): r.send(str(x)+','+str(y)+'\n') t=r.recvbyte() if t=='t': r.recvuntil('\n') return True r.unrecv(t) return False def hash(x): res=0 for i in x: for j in i: res=res*2+j return res def umax(s): res=0 for i in s: res=max(res,max(i)) return res def test8(cur,n,m): print('test8=============================') P=umax(cur)+1 def add(x,y): res=deepcopy(x) for i in range(n): for j in range(m): res[i][j]=(res[i][j]+y[i][j])%P return res def sub(x,y): res=deepcopy(x) for i in range(n): for j in range(m): res[i][j]=(res[i][j]+P-y[i][j])%P return res op=[[0 for i in range(m)]for j in range(n)] for i in range(n): for j in range(m): if test(i,j):return nxt=getlvl() op[i][j]=sub(nxt,cur) print(i,j,op[i][j]) cur=nxt for i in range(10): x=random.randint(0,n-1) y=random.randint(0,m-1) print('random test:',x,y) if test(x,y):return nxt=getlvl() assert nxt==add(op[x][y],cur) cur=nxt sc=[[0 for i in range(m)]for j in range(n)] sv=[[0 for i in range(m)]for j in range(n)] for i in range(n): for j in range(m): oc=op[i][j] ov=[[0 for i in range(m)]for j in range(n)] ov[i][j]=1 flag=False for x in range(n): if flag:continue for y in range(m): if oc[x][y]: if sc[x][y]==0: print(x,y,oc) sc[x][y]=oc sv[x][y]=ov flag=True break tc=sc[x][y] tv=sv[x][y] assert tc[x][y] while oc[x][y]: o=tc[x][y]//oc[x][y] for uu in range(o): tc=sub(tc,oc) tv=sub(tv,ov) oc,tc=tc,oc ov,tv=tv,ov sc[x][y]=tc sv[x][y]=tv oc=cur ov=[[0 for i in range(m)]for j in range(n)] print('try to find sol') for x in range(n): for y in range(m): if oc[x][y]: tc=sc[x][y] tv=sv[x][y] while oc[x][y]: oc=add(oc,tc) ov=add(ov,tv) for x in range(n): for y in range(m): assert oc[x][y]==0 print('find sol:',ov) for i in range(n): for j in range(m): for k in range(ov[i][j]): print('work:',i,j) if test(i,j):return cur=getlvl() def work(): global _debug print('try to get lvl') _debug=True s=getlvl() _debug=False n=len(s) m=len(s[0]) print(s) test8(s,n,m) while True: work() ``` ### Labyrinth A maze. However, there are some hidden mines in the map. So just mark them and avoid them. ```python import socket import random import sys from copy import deepcopy import json import os from pwn import * _debug=True r=remote('ppc-labyrinth.ctfz.one',4340) N=41 M=201 def recvmap(): res=[] for i in range(min(N,curx+601)): res.append(r.recvuntil('\n').decode().strip()) if res[-1][:9]=='Exception': print(res[-1]) markmine() r.interactive() return res known_map={} curx=0 def rmap(): global cury fe=False t=recvmap() n=len(t) for i in range(n): if len(t[i])!=201: if t[i][0]!='#':print(t) assert t[i][0]=='#' and t[i][-1]=='#' while len(t[i])<201: t[i]+=' ' for j in range(M): if t[i][j]=='@': cury=j ux=i t[i]=t[i][:j]+' '+t[i][j+1:] elif t[i][j]=='E': t[i]=t[i][:j]+' '+t[i][j+1:] fe=True elif t[i][j]!='#' and t[i][j]!=' ': print('find strange point:',i,j) print(t[i]) for i in range(n): px=i-ux+curx if px in known_map: for j in range(201): assert known_map[px][j]=='*' or known_map[px][j]==t[i][j] else: known_map[px]=t[i] vis={} def getm(x,y,tx,ty): if tx==x+1:return 'down' if tx==x-1:return 'up' if ty==y+1:return 'right' return 'left' def upmove(ss): global curx,guessy guessy=cury for s in ss: if s=='up':curx-=1 if s=='down':curx+=1 if s=='left':guessy=guessy-1 if s=='right':guessy=guessy+1 r.send(','.join(ss)+'\n') rmap() guessy=0 def markmine(): t=known_map[curx] t=t[:guessy]+'*'+t[guessy+1:] known_map[curx]=t save() def find_path(sx,sy,tx,ty): if sx==sy and tx==ty:return [] q=[(tx,ty)] fa={(tx,ty):0} i=0 res=[] while i=-500: for i in range(0,len(pt),40): upmove(pt[i:i+40]) print(i,len(pt),curx,cury,pt[i]) if curx<-500:break st=i+40 for i in range(st,len(pt),1): upmove(pt[i:i+1]) print(i,len(pt),curx,cury,pt[i]) print_whole_map() print(curx,cury) save() ``` ### StarWars When we successfully attacked some ship, our shield will be the max of our old one and shield of that ship, our attack will be the sum modulo 256. The action of other players are always the same. So the current state is only related with current shield and attack, and the remaining ships. So we can use dp to solve this. Let $$dp[i][j][k][mask]$$ be: round $$i$$, current shield $$j$$, current attack $$k$$, $$mask$$ stores remaining ships. The dp relation is obvious. ```cpp #include typedef std::pair pii; template inline T max(T a,T b){return a>b?a:b;} #define xx first #define yy second #define mp(a,b) std::make_pair(a,b) #define pb push_back #define fo0(i,n) for(int i=0,i##end=n;i f[25][33][256]; std::vector find_seq(int ti,int tj,int tk,int tl) { if(ti==0)return std::vector(); int i=ti-1; std::vector>ef; fo0(j,hc)fo0(k,256) { for(int l=f[i][j][k]._Find_first();l>t&1)continue; if(!work(mp(hv[j],k),spo[t][i],tmp))continue; mask=l|1<res=find_seq(ti-1,ef[0].xx.xx,ef[0].xx.yy,ef[0].yy.xx); res.pb(ef[0].yy.yy); return res; } int main() { fo0(i,16)ssp[i]=mp(system_ships_t[i][0],system_ships_t[i][1]); fo0(i,17)sp[i]=mp(ships_t[i][0],ships_t[i][1]); memset(hid,0xff,sizeof hid); fo0(i,16)if(!~hid[ssp[i].xx])hv[hid[ssp[i].xx]=hc++]=ssp[i].xx; fo0(i,17)if(!~hid[sp[i].xx])hv[hid[sp[i].xx]=hc++]=sp[i].xx; fo0(i,16) { spo[i][0]=sp[i+1]; fo0(j,24) { assert(work(spo[i][j],ssp[attack_order[i+1][j]],spo[i][j+1])); } } f[0][hid[sp[0].xx]][sp[0].yy]=1; fo0(i,24) { fo0(j,hc) { fo0(k,256) { for(int l=f[i][j][k]._Find_first();l>t&1)continue; if(!work(mp(hv[j],k),spo[t][i],tmp))continue; mask=l|1<ans=find_seq(i,j,k,l); ans.pb(x); for(auto i:ans)printf("%d ",i);puts(""); exit(0); } if(!work(tmp,ssp[attack_order[0][i]],tmp))continue; f[i+1][hid[tmp.xx]][tmp.yy][mask]=1; } } } } } } ``` After running this, we find the unique sequence that leads to win. However, the output part failed to run on my computer, so here is a python code to output the flag: ```python import hashlib s=[0,0,0,0,0,0,0,11,16,0,1,10,6,13,3,2,9,15,8,14,12,5,4,7] hs=hashlib.sha256(bytes(s)).digest() u=[514320763,1323926996,700138656,849420235,427957186,1264587960,734406206,1885512490] ut=[] for i in u: ut.append(i&255) ut.append(i>>8&255) ut.append(i>>16&255) ut.append(i>>24&255) ut=bytes(ut) ans=[] for i in range(32): v=(ut[i]^hs[i])%127 ans.append(v if v>=32 else v+32) print(bytes(ans)) ``` # Reverse ### Baby rev A dos executable. It reads a file and divide it into parts. In each part, it’s compared each byte to some value. The following script is to extract these compared values from the asm presented by IDA. ```python s=[-1 for i in range(480)] for i in open('tmp.txt').readlines(): t=i[23:].strip() if t.startswith('cmp byte ptr') and '+' in t: t=t[t.find('+')+1:] o=t[:t.find('h')] if ']' in o:o=o[:o.find(']')] o=int(o,16) t=t[t.find(',')+1:] v=t[:t.find('h')] v=int(v,16) s[o]=v s[0]=0xdb for i in range(480): if s[i]==-1:print(i) open('test.bin','wb').write(bytes(s)) ``` File tmp.txt contains asm like: ``` seg000:0214 loc_10214: ; CODE XREF: sub_101FA+15↑j seg000:0214 cmp byte ptr [si+1], 0DBh seg000:0218 jz short loc_1021D seg000:021A jmp loc_10426 seg000:021D ; --------------------------------------------------------------------------- seg000:021D seg000:021D loc_1021D: ; CODE XREF: sub_101FA+1E↑j seg000:021D cmp byte ptr [si+2], 0DBh seg000:0221 jz short loc_10226 seg000:0223 jmp loc_10426 ``` Finally run “BABY_REV test.bin” we got the flag. ### CSES The program requires lua, and it communicates with the server to execute commands. In the main function, an array is decoded, and then it’s the lua bytecode. I used luadec to decompile the bytecode, but some functions were incorrect. However, in the handle_answer function, we saw the command. ```lua handle_answer = function(answer) -- function num : 0_14 , upvalues : _ENV enc_index = (string.find)(answer, "encrypt::") dec_index = (string.find)(answer, "decrypt::") command = rc4_cipher("ctfzone2019", base64_dec("1bC87lzEebgL")) bin_index = (string.find)(answer, command) if enc_index == 1 or dec_index == 1 or bin_index == 1 then print_result(answer) end End ``` I patched this function, and find that command is “gpsjdeadk”. Then I send this to the server, an large base64 encoded string is returned. It contains an ELF file, the server. The server encrypts flag with an hard coded AES key, and use the encrypted flag as IV and key for later encryption. The following script is to find the encrypted flag. The final decryption is too easy, and I won’t put it here. ```python import socket import random import sys from copy import deepcopy from pwn import * r=remote('re-cses.ctfz.one',3607) def encrypt(s): r.send('encrypt::'+s+'\n') return r.recvuntil('\0')[:-2].decode('base_64') def decrypt(s): r.send('decrypt::'+s.encode('base_64').strip()+'\n') res='' for i in range(len(s)): t=r.recv(1) if t=='\0':break res+=t return res def xor(a,b): r='' for i in range(len(a)): r+=chr(ord(a[i])^ord(b[i])) return r a='1'*16 x=encrypt(a)[:16] xu=decrypt(x+x) b=xu[:16] c=xu[16:] print b.encode('hex'),c.encode('hex') rc=xor(c,x) flag_enc=xor(rc,a) print(flag_enc.encode('hex')) ``` ### Strange_pdf The comment starting with % in pdf file will be ignored by pdf-viewer, and there is one program in the comment just after header. Also, there is anther comment later which is “55 AA”, the end signature of MBR. So the program is a 16-bit x86 program and it print 99399 on screen, which is x.