PHP7.4 -FFI 特性
2023-3-3 00:4:13 Author: 白帽子(查看原文) 阅读量:18 收藏

PHP7.4正式版发布已经好久了,而主打的新特性是FFI,FFI扩展已经通过RFC,正式成为PHP 7.4核心扩展。

FFI(Foreign Function Interface)是用来与其它语言交互的接口,在有些语言里面称为语言绑定(language bindings),Java 里面一般称为 JNI(Java Native Interface) 或 JNA(Java Native Access)。由于现实中很多程序是由不同编程语言写的,必然会涉及到跨语言调用,比如 A 语言写的函数如果想在 B 语言里面调用,这时一般有两种解决方案:一种是将函数做成一个服务,通过进程之间通信(IPC)或网络协议通信(RPC,RESTful等);另一种就是直接通过 FFI 调用。前者需要至少两个独立的进程才能实现,而后者直接将其它语言的接口内嵌到本语言中,所以调用效率比前者高。

闲话不多说我们进入正题目前现在大多数的PHP扩展是对一些已有的C库的包装,某些常用的mysqli,curl,gettext等,PECL中也有大量的类似扩展。

原先的方式,当我们需要用一些已有的C语言的库的能力的时候,我们需要用C语言写包装器,把他们包装成扩展,这个过程中就需要大家去学习PHP的扩展怎么写,当然现在也有一些方便的方式,某种Zephir。但总还是有一些学习成本的,而有了FFI之后,我们就可以直接在PHP脚本中调用C语言写的库中的函数了。

而C语言几十年的历史中,积累了越来越多的优秀的库,FFI直接让我们可以方便的享受这个庞大的资源了。

今天用一个例子来介绍,我们如何使用PHP来调用libcurl,来抓取一个网页的内容,为什么要用libcurl呢?PHP不是已经有了curl扩展了么?,首先因为libcurl的api我比较熟,其次呢,正是因为有了,才好对比,传统扩展方式AS和FFI方式直接的易用性不是?

首先,某些我们就拿当前你看的这篇文章为例,我现在需要写一段代码来抓取它的内容,如果用传统的PHP的curl扩展,我们大概会这么写

     

首先要启用PHP7.4的ext/FFI,需要注意的是PHP-FFI要求LIBFFI-3以上。

然后,我们需要告诉PHP FFI我们要调用的函数原型是什么样子的,这个我们可以使用FFI::cdef, 它的定义是这样子的。

在string $cdef中,我们可以写C语言函数式申明,FFI会parse它,了解到我们要在string $lib这个库中调用的函数的签名是啥样的,在这个例子中,我们用到三个libcurl的函数,它们的申明我们都可以在libcurl的文档里找到,比如对于curl_easy_init。

具体到这个例子,我们写一个curl.php, 包含所有要声明的东西,代码如下:

这里有个地方是,文档中写的是返回值是CURL *,但事实上因为我们的例子中不会解引用它,只是传递,那就避免麻烦就用void *代替,然而还有个麻烦的事情是,PHP预定义好了CURLOPT_等option的值,但现在我们需要自己定义,简单的办法就是查看curl的头文件,找到对应的值,然后我们把值给加进去

这样的话,定义部分就算完成了,现在我们完成实际逻辑部分,整个代码段是这样子的:

怎么样,相比使用curl扩展的方式, 是不是一样简练呢?

接下来,我们稍微弄的复杂一点,即使如果我们不想要结果直接输出,而是返回成一个字符串, 对于PHP的curl扩展来说,我们只需要调用curl_setop 把CURLOPT_RETURNTRANSFER为1,但在libcurl中其实并没有直接返回字符串的能力,而是提供了一个WRITEFUNCTION的回调函数,在有数据返回的时候,libcurl会调用这个函数, 事实上PHP curl扩展也是这么做。

目前我们并不能直接把一个PHP函数作为回调函数通过FFI传递给libcurl, 那我们会有俩种方式来做:

1. 采用WRITEDATA, 默认的libcurl会调用fwrite作为回调函数,而我们可以通过WRITEDATA给libcurl一个fd,让它不要写入stdout,而是写入到这个fd
       2. 我们自己编写一个C的简单函数,通过FFI引入进来,传递给libcurl。

我们先用第一种方式,首先我们需要使用fopen,这次我们通过定义个C的头文件来申明原型(file.h):

像file.h一样,我们把所有的libcurl的函数声明也放到curl.h中去

然后我们就可以使用FFI::load来加载.h文件:

但是怎么告诉FFI加载哪个对应的库呢?如上面,我们通过定义了一个FFI_LIB的宏,来告诉FFI这些函数来自libcurl.so, 当我们用FFI::load加载这个h文件的时候,PHP FFI就会自动载入libcurl.so。

那为什么fopen不需要指定加载库呢,那是因为FFI也会在全局符号表中查找符号,而fopen是一个标准库函数,它早就存在了。

现在整个代码会是:

但这种方式呢就是需要一个临时的中转文件,还是不够优雅, 现在我们用第二种方式,要用第二种方式,我们需要自己用C写一个回调函数传递给libcurl:

注意此处的init函数,因为在PHP FFI中,就目前的版本(2020-03-11)我们没有办法直接获得一个函数指针,所以我们定义了这个函数,返回own_writefunc的地址。

最后我们定义上面用到的头文件write.h:

注意到我们在头文件中也定义了FFI_LIB, 这样这个头文件就可以同时被write.c和接下来我们的PHP FFI共同使用了。

然后我们编译write函数为一个动态库:

好了, 现在整个的代码会变成:

此处, 我们使用FFI::new ($write->new)来分配了一个struct _write_data的内存:

$own表示这个内存管理是否采用PHP的内存管理,默认的情况下,我们申请的内存会经过PHP的生命周期管理,不需要主动释放,但是有的时候你也可能希望自己管理,那么可以设置$own为flase,那么在适当的时候,你需要调用FFI::free去主动释放。

然后我们把$data作为WRITEDATA传递给libcurl, 此处我们使用了FFI::addr来获取$data的实际内存地址:

然后我们把own_write_func作为WRITEFUNCTION传递给了libcurl,这样再有返回的时候,libcurl就会调用我们的own_write_func来处理返回,同时会把write_data作为自定义参数传给我们的回调函数。

最后我们使用了FFI::string来把一段内存转换成PHP的string:

当不提供$size的时候,FFI::string会在遇到Null-byte的时候停止。

然而毕竟直接在PHP中每次请求都加载so的话,会是一个很大的性能问题,所以我们也可以采用preload的方式,这种模式下, 我们通过opcache.preload来在PHP启动的时候就加载好:

ffi_preload.inc:

但我们引用载入的FFI呢?为此我们需要修改一下这俩个.h头文件,加入FFI_SCOPE, 比如curl.h:

对应的我们给write.h也加入FFI_SCOPE为"write", 然后我们的脚本现在看起来应该是这样

也就是,我们现在使用FFI::scope来代替FFI::load,引用对应的函数。

然后还有另外一个问题,FFI虽然给了我们很大的灵活性,但是毕竟直接调用C库函数,还是非常具有风险性的,我们应该只容许用户调用我们确认过的函数,于是,ffi.enable=preload就该上场了,当我们设置ffi.enable=preload的话,那就只有在opcache.preload的脚本中的函数才能调用FFI,而用户写的函数是没有办法直接调用的。

我们稍微修改下ffi_preload.inc变成ffi_safe_preload.inc

也就是,我们把所有会调用FFI API的函数都定义在preload脚本中,然后我们的例子会变成(ffi_safe.php):

这样一来通过ffi.enable=preload, 我们就可以限制,所有的FFI API只能被我们可控制的preload脚本调用,用户不能直接调用。从而我们可以在这些函数内做好尽可能的安全保证工作,从而保证一定的安全性。经过这个例子,大家应该对FFI有了一个比较深入的理解了。

E

N

D

Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。

团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室。团队公众号自创建以来,共发布原创文章370余篇,自研平台达到26个,目有15个平台已开源。此外积极参加各类线上、线下CTF比赛并取得了优异的成绩。如有对安全行业感兴趣的小伙伴可以踊跃加入或关注我们


文章来源: http://mp.weixin.qq.com/s?__biz=MzAwMDQwNTE5MA==&mid=2650246548&idx=2&sn=c5e48b365917426b8206c38746222207&chksm=82ea563db59ddf2bccd8afea1718f5256735c708af497264b4ac659f32a22f2803169e9970cb#rd
如有侵权请联系:admin#unsafe.sh