复现下PlaidCTF2021的the-false-promise
题目分析
首先查看diff:
1 | diff --git a/src/builtins/promise-jobs.tq b/src/builtins/promise-jobs.tq |
修改了PromiseResolveThenableJob函数,把一些check给删掉了,但是对于这个也不是很熟悉,所以还是先找个办法下断点调试一下
为了调试方便我选择了8.7.242版本的d8,这个版本可以直接下Builtins_PromiseResolveThenableJob
的断点,非常方便
初次尝试
测试代码:
1 | var p1 = Promise.resolve({ |
该函数的完整签名是:
1 | transitioning builtin |
断点断下后,thenable就是我们的变量p1,then就是p1.then也就是那个函数,初次尝试,TaggedEqual(then, promiseThen)
的check没过,既然patch打到了这个判断上,那我们就要想办法进入这个if的分支
TaggedEqual(then, promiseThen)
没过的原因是:
上面执行到TaggedEqual(then, promiseThen),而这时候它们比较的内容:
我们测试代码里自定义的then(0x317208148ba9)过不去check
于是乎换了下写法:
1 | var p1 = Promise.resolve({ |
打了patch的版本直接DCHECK报错:
1 | abort: CSA_ASSERT failed: Torque assert 'Is<A>(o)' failed [src/builtins/cast.tq:657] [../../src/builtins/promise-jobs.tq:56] |
从报错显示的源码位置来看,我们的确进入打了patch的if分支,很好的第一步,后面发现把p1.then那一步删了也会触发报错,报错的应该是if分支里:
1 | return PerformPromiseThen( |
的UnsafeCast<JSPromise>(thenable)
的检查没过,但是这个check好像只有debug版本才有,release版本里没有,所以直接选择注释掉:
1 | // src/builtins/cast,tq |
到这里就能看出这里应该是可以类型混淆的,就是我们自定义的对象和JSPromise
的混淆
注释过后又出现了一个报错:
1 | # Fatal error in ../../src/runtime/runtime-promise.cc, line 67 |
这个报错在release版本中也有,但是在题目给的chrome浏览器中是没有这个报错的,就是正常的抛异常,有点迷惑
代码分析
经过蛋疼的调试之后,这个报错:
1 | # Fatal error in ../../src/runtime/runtime-promise.cc, line 67 |
是因为我们进入了:
1 | // src/builtins/promise-abstract-operations.tq |
[1]出的位置,也就是调用了runtime的PromiseRevokeReject,具体原因应该是promise.Status() == PromiseState::kPending
,我们构造的对象不符合这个条件,所以进入了下面的内容导致报错。
经过调试和查看源码(中间绕了好久,傻逼了),下个断点到Builtins_PerformPromiseThen:
查看这时候的rax,rbx,rcx和rdx:
1 | wndbg> job $rax |
和PerformPromiseThen传进来的参数一致:
1 | transitioning builtin |
所以我们的自定义对象被当成了一个JSPromise对象,的确是类型混淆了,再回到:
1 | @export |
如果我们能满足promise.Status() == PromiseState::kPending
,那我们就能进入[2]处,这里会将reaction写入promise.reactions_or_result
,也就是我们伪造对象的某个偏移
寻找偏移
在src/builtins/promise-misc.tq文件下的PromiseInit函数:
1 | @export |
在src/include/v8.h下的枚举定义:
1 | enum PromiseState { kPending, kFulfilled, kRejected }; |
结合测试代码:
1 | var a = Promise.resolve(4); |
输出:
1 | DebugPrint: 0x166b08108b21: [JSPromise] |
可以得出reactions_or_result的偏移就是0xc,而这个status的偏移是0x10
我们前面构造的函数还差了个属性,这次给他补上:
1 | var a = { |
其实叫不叫status都无所谓应该,反正第二个属性会在0x10的偏移处,setTimeout是为了能让里面的handler在Promise.resolve
之后执行
这个在debug版本下直接报错了,因为a.then被改成了promiseReactions:
此时的rcx和eax:
1 | pwndbg> job $rcx |
rcx就是我们自定义的对象,写入的也是我们猜测的0xc偏移的位置
exploit
该类型混淆会导致在一个对象0xc的偏移写入一个promiseReactions(这是一个很大的值),这个0xc的偏移很容易就想到数组的length,试一试看看:
1 | class myArray extends Array{ |
输出:
1 | 0x38a508088c4d <JSArray[0]> |
那既然有了越界数组,在oob_arr后面布置一个BigUint64Array和ArrayBuffer就ok了:
1 | class myArray extends Array{ |
但是,d8下是没问题了,到了题目的chrome浏览器却出现了毛病,它抛异常了:
1 | Uncaught (in promise) TypeError: Method Promise.prototype.then called on incompatible receiver [object Array] |
我去找了下别人的exp,也会抛异常,。。。。。这是为什么啊 orz,无法理解