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.
一次(可能算是)失败的 BGP 尝试 December 6, 2019 ### 背景 教育网的 ipv6 大多是免流的。那么问题是如何找一个合适的梯子实现高速免流。教育网 ipv6 的出国线路大概有 HE、Cogentco、HKIX(有多个 ISP 都会经过 HKIX,就不详细列出了)。 HE 是大多数 ip 都会走线路,于是他 QoS 也比较严重。Cogentco 和 HKIX 则 QoS 的比较少。走 HKIX 的 VPS 还是有不少(自行搜索),不过价格一般还是不便宜。 而 Cogentco 线路有一些大服务商有,比如 vultr 的达拉斯回程和亚特兰大的去程,但是这两个的相反方向却都只是普通的HE 线路。要综合这两个线路得到一个双向 Cogentco 线路的梯子,其实可以通过一些操作,使得本地给 A 发包,如何 B 返回结果。但是感觉设置比较麻烦,就咕了。 这时我想到我恰好有一个(算是)闲置的 ASN 和一些 ipv6 资源,能不能用 BGP 实现这个双向 Cogentco 呢? ### 前提条件 一个 ASN,一些 ip 段。这些许多商家有售(比如 hostus)。比较便宜的可以去各大论坛或者群里问(比如 [https://t.me/MoeQing](https://t.me/MoeQing)) 首先在 vultr 开两台机,装好必要的软件。 接下来在两台机器上都配置广播 ip,可以参考 [https://blog.ni-co.moe/public/560.html](https://blog.ni-co.moe/public/560.html)。 ### 配置去程 由于我们想让这个 ip 段强制走其中一台进入,需要把他们之间连起来,并且再设置一下路由优先级。 把他们之间连起来可以参考 [https://lantian.pub/article/modify-website/join-dn42-experimental-network.lantian/](https://lantian.pub/article/modify-website/join-dn42-experimental-network.lantian/)。这里需要注意,为了让 zerotier 能正常分配公网 ipv6,需要运行 `zerotier-cli set 你的网络id allowGlobal=1`。 接下来是设置路由优先级。这里的需求很简单,只是为了让路由全都走某一台,那么可以让另一台的 AS Path 足够长,这样就不会被优先选择了。 具体来说,在 `/etc/bird/bird6.conf` 中,`protocol bgp vultr` 一段里,把 `export all;` 改成: ``` export filter { bgp_path.prepend(你的 AS 号); bgp_path.prepend(你的 AS 号); ... bgp_path.prepend(你的 AS 号); bgp_path.prepend(你的 AS 号); }; ``` 这里要加多少个就自己决定了,只要让别人走不过来就行。 配好之后,重启 bird6,等一会路由更新,然后 trace 一下,发现去程确实走了 Cogentco。 ### 配置回程 回程按理来说是不需要配置的,但是在达拉斯的机器上运行 ``` mtr 2402:f000:2:f001::240:1 ``` 是走 GTT 到 Cogentco;而运行 ``` mtr -a 你的ip 2402:f000:2:f001::240:1 ``` 却是走 NTT 到 HE。 为了配置这一部分,需要想办法调整回程线路,也就是 VPS 上的出口线路。 而根据 Vultr 给出的一些参考([https://www.vultr.com/docs/as20473-bgp-customer-guide](https://www.vultr.com/docs/as20473-bgp-customer-guide)),可以用 BGP Community 来控制宣告。 比如可以在 `export filter` 中加入 ``` bgp_community.add((64609, 3257)); bgp_community.add((64699, 3257)); bgp_community.add((20473, 6000)); ``` 来只对外宣告到 AS3257(GTT)。 然而,这样仍然不能使上面的 mtr 都走 Cogentco。 ### 问题分析 我找 Vultr 客服把我的 BGP 模式改成了全表。在所有出口路径中,我搜索了 45576(贵清的 ASN),发现只有 ``` 64515 65534 20473 6939 23911 23910 24348 45576 ``` 一条。也就是直接走 HE 的。 那么 GTT 和 Cogentco 是怎么走出来的就非常奇怪了。 这里我也分析不下去了,如果有人知道原因欢迎联系我。 后来我换了几个不同的贵清 ip 测试,发现有的是两种 mtr 都走 NTT-HE,有的是两种都走 GTT-Cogentco。 ### 总结 BGP 实在是玄妙啊。。。