2021 AIS3 EOF Final - Mugyu, Raffle 官方解

Tue, Feb 15, 2022 2-minute read

這次 EOF Final 擔任出題者出了兩題 Reverse,感覺再多加一點難度會更有意思。

題目:Mugyu, Raffle

Before we start


Mugyu

244 points / 13 solves

Challenge Info

むぎゅ〜〜〜

File: mugyu, n3k0, lov3, README.md

Solution

README.md

1
./mugyu n3k0

n3k0

n3k0 看起來是一個 flag checker

解完得到假 flag:

1
AIS3{The flag format: "EOF"{[\x20-\x7a|~]+}. Never gonna give you up :)}

直接執行 ./n3k0 時假 flag 可以通過檢查,但用 ./mugyu n3k0 就不能,明顯程式流程不一樣。

還有在 main 之前會執行 sub_1188,呼叫 syscall 0x8763。

mugyu

Symbol 給好給滿,應該不難發現 mugyu 是改過的 qemu

n3k0 會呼叫不存在的 syscall,所以可以朝這方面去觀察。

syscall 在 do_syscall1() 實作(main() -> cpu_loop() -> do_syscall() -> do_syscall1()),F5 下去跟原始碼沒兩樣:

也就是說 syscall 0x8763 有兩個參數:

  • edi:addr offset (0x4000000000 + edi)
  • esi:crystal index (crystal[esi])

會把 crystal[esi] 的 machine code patch 到 0x4000000000 + edi


另外 crystal 會在 load_elf_binary() 做初始化:讀入 lov3 並解密,然後存在 crystal

Patch n3k0

寫個腳本自己上 patch,n3k0 變成下面那樣,其實就是把 checker 內容換掉而已:

Flag

EOF{sin6u14r_uni7_de73ct3d_id_tr4cing_c0ordin4te_fixed_r3p0rt_c0mple7e.}


Raffle

436 points / 7 solves

Challenge Info

I will not raffle anything…

File: raffle

Solution

raffle 是 golang binary,並且有給 symbol。

這題逆向部分很簡單:main() 會建立三個 goroutine(node_XXXX()),每個 goroutine 又會再叫三個 goroutine,總共三層,共 39 個 node 被叫起來;每個 node 會對輸入做三個小運算。

運算都很簡單,重點在 node 的執行順序:

Method1: 靜態解

goroutine 的執行順序由 GMP 排程器決定,理論上不會固定。

但因為在 main() 之前會執行 runtime.GOMAXPROCS(1),這讓 goroutine 的執行順序與「從 runnable queue」取出順序一樣,原始碼在 /usr/lib/go-1.13/src/runtime/proc.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Get g from local runnable queue.
// If inheritTime is true, gp should inherit the remaining time in the
// current time slice. Otherwise, it should start a new time slice.
// Executed only by the owner P.
func runqget(_p_ *p) (gp *g, inheritTime bool) {
    // If there's a runnext, it's the next G to run.
    for {
        next := _p_.runnext
        if next == 0 {
            break
        }
        if _p_.runnext.cas(next, 0) {
            return next.ptr(), true
        }
    }

    for {
        h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with other consumers
        t := _p_.runqtail
        if t == h {
            return nil, false
        }
        gp := _p_.runq[h%uint32(len(_p_.runq))].ptr()
        if atomic.CasRel(&_p_.runqhead, h, h+1) { // cas-release, commits consume
            return gp, false
        }
    }
}

基本上就是一個 queue,但是最後一個放進去的 goroutine 會第一個執行,舉個例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main() {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(4)
    go node_0()
    go node_1()
    go node_2()
    go node_3()
    wg.Wait()
}

執行順序會是:node_3() -> node_0() -> node_1() -> node_2()

Method2: 動態解

解法由 @frozenkp 提供

在 node 設斷點會導致之後的執行順序亂掉,因此:

  1. 對全部 node 設斷點,如此一來第一個撞到斷點的 node 是第一個執行的 node
  2. 把第一個 node 的斷點去掉,並重新執行程式,第一個撞到斷點的 node 是第二個執行的 node
  3. 總共重複 39 次就能爆破出執行順序

Method3: 模仿(?)解

直接寫一隻程式模仿 raffle 創建 goroutine,把順序印出來。


知道執行順序之後就看要用 z3 或是寫個解密腳本拿 flag。

Flag

EOF{w3_4r3_901ng_70_raffl3_0fF_A_g0...g0-r0u71n3!}