某 IM 软件消息卡片异常分析
日志分析
通过 EXCEPTION_ACCESS_VIOLATION
可以判断异常类型是非法内存访问。
触发异常的指令地址位于 EIP=6931A5CC
,对应的汇编指令片段如下:
mov eax, [esi]
push dword ptr [eax+4]
call sub_56BCA6A8
栈帧底部地址位于 EBP=00CFBAD8
,可以帮助恢复函数调用栈的结构。
Type: EXCEPTION_ACCESS_VIOLATION
Error: Read address 0x00000004
Address: 6931A5CC
CallStack:
KernelUtil + 14A5CC
KernelUtil + DB3AB
MsgMgr + 5FA6A
MsgMgr + 5A6D8
MsgMgr + 55783
AppUtil + 2B176
GroupApp + 1F17C4
...
Regs:
EAX=00000000, EBX=00000000, ECX=00CFBBB0, EDX=00000001
ESI=1B9E13F8, EDI=00CFBBB0, EBP=00CFBAD8, ESP=00CFBAAC, EIP=6931A5CC
堆栈分析
函数调用链
通过日志记录的调用栈可以恢复函数调用链:
CTXMsgReplyOleCtrl::ParserSourceMsg()
Util::Msg::GetJumpUrlFromArkMeta()
Json::Value::getMemberNames()
CTXMsgReplyOleCtrl::ParserSourceMsg
函数负责对 json
消息卡片进行解析。
Util::Msg::GetJumpUrlFromArkMeta
函数负责对 json
消息卡片中的 meta
数据进行解析。
Json::Value::getMemberNames
函数负责获取 json
的所有成员名称。
函数参数
使用逆向工具加载 dmp
内存快照,通过回溯栈帧可以分析异常发生时各个函数传入的参数。
其中 Util::Msg::GetJumpUrlFromArkMeta
函数传入的参数是一个字符串,通过栈上的引用可以在堆中找到完整的 json
文本,内容如下:
{
"detail_1": {
"appid": "1109937557",
"desc": "0",
"gamePoints": "",
"gamePointsUrl": "",
"host": {
"nick": "0",
"uin": 0
},
"icon": "https:\/\/open.gtimg.cn\/open\/app_icon\/00\/95\/17\/76\/100951776_100_m.png?t=1631089970",
"preview": "pubminishare-30161.picsz.qpic.cn\/492935bc-2abe-47ee-a50b-75e64278ab80",
"qqdocurl": "https:\/\/b23.tv\/uRFc0c?share_medium=android&share_source=qq&bbid=XX463414F6720F0D766BD7EB79936116E8EF7&ts=1631426689501",
"scene": 1036,
"shareTemplateData": null,
"shareTemplateId": "8C8E89B49BE609866298ADDFF2DBABA4",
"showLittleTail": "",
"title": "哔哩哔哩",
"url": "m.q.qq.com\/a\/s\/0c3a31e2186f75749bc4e045d273d33e"
}
}
Json::Value::getMemberNames
函数传入的参数是一个 Json::Value::null
对象,对象中的空数据被作为指针解引用,从而触发了异常。
复现异常
将上面的 json
文本传入 Util::Msg::GetJumpUrlFromArkMeta
可以复现异常,方便后续的调试过程,相关代码如下:
HMODULE hDll = LoadLibraryW(L"KernelUtil.dll");
PVOID pGetJumpUrlFromArkMeta = (PVOID)GetProcAddress(hDll, "?GetJumpUrlFromArkMeta@Msg@Util@@YAHVCTXBSTR@@PAVCTXStringW@@@Z");
HGetJumpUrlFromArkMeta GetJumpUrlFromArkMeta = (HGetJumpUrlFromArkMeta)pGetJumpUrlFromArkMeta;
GetJumpUrlFromArkMeta((PWCHAR)jsonString);
代码分析
通过跟踪 Util::Msg::GetJumpUrlFromArkMeta
函数流程,可知其调用 Value::operator[]
函数来获取 json
的成员对象,并将成员对象加入到队列中等待处理。
Value::operator[]
函数在检索 shareTemplateData
时会返回 Json::Value::null
对象,正是这个对象触发了后续的异常,相关函数实现如下:
Value &
Value::operator[]( const char *key )
{
return resolveReference( key, false );
}
Value &
Value::resolveReference( const char *key,
bool isStatic )
{
JSON_ASSERT( type_ == nullValue || type_ == objectValue );
if ( type_ == nullValue )
*this = Value( objectValue );
#ifndef JSON_VALUE_USE_INTERNAL_MAP
CZString actualKey( key, isStatic ? CZString::noDuplication
: CZString::duplicateOnCopy );
ObjectValues::iterator it = value_.map_->lower_bound( actualKey );
if ( it != value_.map_->end() && (*it).first == actualKey )
return (*it).second;
ObjectValues::value_type defaultValue( actualKey, null );
it = value_.map_->insert( it, defaultValue );
Value &value = (*it).second;
return value;
#else
return value_.map_->resolveReference( key, isStatic );
#endif
}
随后 Json::Value::null
对象从队列中取出,并传入 Json::Value::getMemberNames
函数进行处理。
然而 KernelUtil
中使用的是早期版本的 JsonCpp
,相关函数实现如下:
Value::Members
Value::getMemberNames() const
{
JSON_ASSERT( type_ == nullValue || type_ == objectValue );
Members members;
members.reserve( value_.map_->size() );
#ifndef JSON_VALUE_USE_INTERNAL_MAP
ObjectValues::const_iterator it = value_.map_->begin();
ObjectValues::const_iterator itEnd = value_.map_->end();
for ( ; it != itEnd; ++it )
members.push_back( std::string( (*it).first.c_str() ) );
#else
ValueInternalMap::IteratorState it;
ValueInternalMap::IteratorState itEnd;
value_.map_->makeBeginIterator( it );
value_.map_->makeEndIterator( itEnd );
for ( ; !ValueInternalMap::equals( it, itEnd ); ValueInternalMap::increment(it) )
members.push_back( std::string( ValueInternalMap::key( it ) ) );
#endif
return members;
}
其中 Json::Value::getMemberNames
并没有检查对象的 type_
是否为 nullValue
。
导致 Json::Value::null
对象中的空数据被作为 value_.map
指针解引用,从而触发了异常。
JsonCpp
的这个问题已经在后续的 commit 中被修复。
至此消息卡片异常分析完毕。