1 S2E中的插件(作者:曹鼎;邮箱:caoding8483@163.com)
在S2E构造函数中,会调用initPlugins()来初始化各个插件,其主要作用是这个函数主要是读取配置文件中的plugins域中设置的插件,并且检查这些插件是否依赖于其他插件。initPlugins()函数的实现如下:
void S2E::initPlugins()
{
m_pluginsFactory = new PluginsFactory();
//CorePlugin必不可少
m_corePlugin = dynamic_cast<CorePlugin*>(
m_pluginsFactory->createPlugin(this, "CorePlugin"));
assert(m_corePlugin);
m_activePluginsList.push_back(m_corePlugin);
m_activePluginsMap.insert(
make_pair(m_corePlugin->getPluginInfo()->name, m_corePlugin));
if(!m_corePlugin->getPluginInfo()->functionName.empty())
m_activePluginsMap.insert(
make_pair(m_corePlugin->getPluginInfo()->functionName, m_corePlugin));
//读取配置文件中的plugins域
vector<string> pluginNames = getConfig()->getStringList("plugins");
//对每个插件进行检查和装载
/* Check and load plugins */
foreach(const string& pluginName, pluginNames) {
const PluginInfo* pluginInfo = m_pluginsFactory->getPluginInfo(pluginName);
if(!pluginInfo) {
std::cerr << "ERROR: plugin '" << pluginName
<< "' does not exists in this S2E installation" << std::endl;
exit(1);
} else if(getPlugin(pluginInfo->name)) {
std::cerr << "ERROR: plugin '" << pluginInfo->name
<< "' was already loaded "
<< "(is it enabled multiple times ?)" << std::endl;
exit(1);
} else if(!pluginInfo->functionName.empty() &&
getPlugin(pluginInfo->functionName)) {
std::cerr << "ERROR: plugin '" << pluginInfo->name
<< "' with function '" << pluginInfo->functionName
<< "' can not be loaded because" << std::endl
<< " this function is already provided by '"
<< getPlugin(pluginInfo->functionName)->getPluginInfo()->name
<< "' plugin" << std::endl;
exit(1);
} else {
Plugin* plugin = m_pluginsFactory->createPlugin(this, pluginName);
assert(plugin);
m_activePluginsList.push_back(plugin);
m_activePluginsMap.insert(
make_pair(plugin->getPluginInfo()->name, plugin));
if(!plugin->getPluginInfo()->functionName.empty())
m_activePluginsMap.insert(
make_pair(plugin->getPluginInfo()->functionName, plugin));
}
}
//检查插件是否有依赖
/* Check dependencies */
foreach(Plugin* p, m_activePluginsList) {
foreach(const string& name, p->getPluginInfo()->dependencies) {
if(!getPlugin(name)) {
std::cerr << "ERROR: plugin '" << p->getPluginInfo()->name
<< "' depends on plugin '" << name
<< "' which is not enabled in config" << std::endl;
exit(1);
}
}
}
//对每个插件进行初始化,这里的初始化函数initialize()是虚函数
/* Initialize plugins */
foreach(Plugin* p, m_activePluginsList) {
p->initialize();
}
}
不可缺少的是CorePlugin插件,它是所有插件的基础。其中定义了一系列信号,用于在检测到指令时给插件们发送信号(emit),相应的插件会进行连接(connect)并且调用相应的函数。CorePlugin中定义的信号在CorePlugin.h中:
/** Signal that is emitted on begining and end of code generation
for each QEMU translation block.
在qemu将每一个tb翻译为tcg的时候,都会发送这个信号*/
sigc::signal<void, ExecutionSignal*,
S2EExecutionState*,
TranslationBlock*,
uint64_t /* block PC */>
onTranslateBlockStart;
/** Signal that is emitted upon end of translation block
在一个tb结束的时候(翻译跳转指令)的时候,发送这个信号 */
sigc::signal<void, ExecutionSignal*,
S2EExecutionState*,
TranslationBlock*,
uint64_t /* ending instruction pc */,
bool /* static target is valid */,
uint64_t /* static target pc */>
onTranslateBlockEnd;
/** Signal that is emitted on code generation for each instruction */
sigc::signal<void, ExecutionSignal*,
S2EExecutionState*,
TranslationBlock*,
uint64_t /* instruction PC */>
onTranslateInstructionStart, onTranslateInstructionEnd;
/**
Triggered after each instruction is translated to notify
plugins of which registers are used by the instruction.
Each bit of the mask corresponds to one of the registers of
the architecture (e.g., R_EAX, R_ECX, etc).
*/
sigc::signal<void,
ExecutionSignal*,
S2EExecutionState* /* current state */,
TranslationBlock*,
uint64_t /* program counter of the instruction */,
uint64_t /* registers read by the instruction */,
uint64_t /* registers written by the instruction */,
bool /* instruction accesses memory */>
onTranslateRegisterAccessEnd;
/** Signal that is emitted on code generation for each jump instruction */
sigc::signal<void, ExecutionSignal*,
S2EExecutionState*,
TranslationBlock*,
uint64_t /* instruction PC */,
int /* jump_type */>
onTranslateJumpStart;
/** Signal that is emitted upon exception */
sigc::signal<void, S2EExecutionState*,
unsigned /* Exception Index */,
uint64_t /* pc */>
onException;
/** Signal that is emitted when custom opcode is detected
在遇到s2e_op时,发送这个信号
*/
sigc::signal<void, S2EExecutionState*,
uint64_t /* arg */
>
onCustomInstruction;
/** Signal that is emitted on each memory access /
/ XXX: this signal is still not emmited for code */
sigc::signal<void, S2EExecutionState*,
klee::ref<klee::Expr> /* virtualAddress */,
klee::ref<klee::Expr> /* hostAddress */,
klee::ref<klee::Expr> /* value */,
bool /* isWrite */, bool /* isIO */>
onDataMemoryAccess;
/** Signal that is emitted on each port access */
sigc::signal<void, S2EExecutionState*,
klee::ref<klee::Expr> /* port */,
klee::ref<klee::Expr> /* value */,
bool /* isWrite */>
onPortAccess;
sigc::signal onTimer;
/** Signal emitted when the state is forked */
sigc::signal<void, S2EExecutionState* /* originalState */,
const std::vector<S2EExecutionState*>& /* newStates */,
const std::vector<klee::ref<klee::Expr> >& /* newConditions */>
onStateFork;
sigc::signal<void,
S2EExecutionState*, /* currentState */
S2EExecutionState*> /* nextState */
onStateSwitch;
/** Signal emitted when spawning a new S2E process */
sigc::signal<void, bool /* prefork */,
bool /* ischild */,
unsigned /* parentProcId */> onProcessFork;
/**
Signal emitted when a new S2E process was spawned and all
parent states were removed from the child and child states
removed from the parent.
*/
sigc::signal<void, bool /* isChild */> onProcessForkComplete;
/** Signal that is emitted upon TLB miss */
sigc::signal<void, S2EExecutionState*, uint64_t, bool> onTlbMiss;
/** Signal that is emitted upon page fault */
sigc::signal<void, S2EExecutionState*, uint64_t, bool> onPageFault;
/** Signal emitted when QEMU is ready to accept registration of new devices */
sigc::signal<void> onDeviceRegistration;
/** Signal emitted when QEMU is ready to activate registered devices */
sigc::signal<void, struct PCIBus*> onDeviceActivation;
/**
The current execution privilege level was changed (e.g., kernel-mode=>user-mode)
previous and current are privilege levels. The meaning of the value may
depend on the architecture.
在处理中断或者执行返回指令时,特权级发生变化时发送这个信号
*/
sigc::signal<void,
S2EExecutionState* /* current state */,
unsigned /* previous level */,
unsigned /* current level */>
onPrivilegeChange;
/**
The current page directory was changed.
This may occur, e.g., when the OS swaps address spaces.
The addresses correspond to physical addresses.
在页目录发生变化时,发送这个信号
*/
sigc::signal<void,
S2EExecutionState* /* current state */,
uint64_t /* previous page directory base */,
uint64_t /* current page directory base */>
onPageDirectoryChange;
/**
S2E completed initialization and is about to enter
the main execution loop for the first time.
*/
sigc::signal<void,
S2EExecutionState* /* current state */>
onInitializationComplete;
在由guest binary转化为tcg ir时,遇到s2e_op(0x13f)时会进行如下处理:
case 0x13f: /* s2e_op */
{
#ifdef CONFIG_S2E
uint64_t arg = ldq_code(s->pc);
s2e_tcg_emit_custom_instruction(g_s2e, arg);
#else
/* Simply skip the S2E opcodes when building vanilla qemu */
ldq_code(s->pc);
#endif
s->pc+=8;
break;
}
其中调用了s2e_tcg_emit_custom_instruction(g_s2e, arg):
void s2e_tcg_emit_custom_instruction(S2E*, uint64_t arg)
{
TCGv_ptr t0 = tcg_temp_new_i64();
tcg_gen_movi_i64(t0, arg);
TCGArg args[1];
args[0] = GET_TCGV_I64(t0);
tcg_gen_helperN((void*) s2e_tcg_custom_instruction_handler,
0, 2, TCG_CALL_DUMMY_ARG, 1, args);
tcg_temp_free_i64(t0);
}
void s2e_tcg_custom_instruction_handler(uint64_t arg)
{
assert(!g_s2e->getCorePlugin()->onCustomInstruction.empty());
try {
g_s2e->getCorePlugin()->onCustomInstruction.emit(g_s2e_state, arg);
} catch(s2e::CpuExitException&) {
s2e_longjmp(env->jmp_env, 1);
}
}
这里CorePlugin会发送onCustomInstruction信号,定义的每个插件会在相应的初始化函数中进行connect操作,完成相应的功能。
最后会对每一个插件调用initialize()方法进行初始化,而这个initialize()是一个虚函数,在每一个插件中都有自己的初始化函数。如BaseInstruction中,初始化函数如下定义:
void BaseInstructions::initialize()
{
s2e()->getCorePlugin()->onCustomInstruction.connect(
sigc::mem_fun(*this, &BaseInstructions::onCustomInstruction));
}
当插件BaseInstruction接收到Guest发来的消息时,就会调用onCustomInstruction来对每一个命令进行相应的处理。
WindowsMonitor插件实现了对模块和进程加载和卸载的监视,其他插件将它称为Interceptor。这个插件捕获特定内核函数的调用来监视这些事件。目前仅支持Windows XP SP2和SP3。
配置文件:
pluginsConfig.WindowsMonitor = {
--指明需要监视的操作系统版本
version="XPSP3",
--指明是否追踪用户模式下的事件,如DLL加载或卸载
userMode=true,
--指明是否追踪内核模式下的事件,如驱动的加载或卸载
kernelMode=true,
--以下三个选项都是为调试设置的,一般都设置为true
monitorModuleLoad=true,
monitorModuleUnload=true,
monitorProcessUnload=true,
--需要监视的模块列表
modules={
module_id1={
...
}
module_id2={
...
}
}
}
WindowsMonitor 插件不需要依赖其他插件。
1.3.3.1 WindowsMonitor 插件的初始化
WindowsMonitor 插件首先初始化,做了如下几项工作:
1.读取配置文件中的参数;
2.获取系统基本相关信息,如内核起始地址、检查版本信息等;
3.创建两个Interceptor对象,一个是用户模式下的,一个是内核模式下的,用于处理不同模式下的模块或者驱动的加载或者卸载;
4.连接CorePlugin发出的信号:onTranslateInstructionStart信号和onPageDirectoryChange信号,并调用相关处理函数;
5.读取配置文件中的模块列表。[注:这里的模块列表用于内核模式下的一些处理]
在WindowsMonitor.cpp中实现,代码如下:
void WindowsMonitor::initialize()
{
//读取配置文件中的内容
string Version = s2e()->getConfig()->getString(getConfigKey() + ".version");
m_UserMode = s2e()->getConfig()->getBool(getConfigKey() + ".userMode");
m_KernelMode = s2e()->getConfig()->getBool(getConfigKey() + ".kernelMode");
//For debug purposes
m_MonitorModuleLoad = s2e()->getConfig()->getBool(getConfigKey() + ".monitorModuleLoad");
m_MonitorModuleUnload = s2e()->getConfig()->getBool(getConfigKey() + ".monitorModuleUnload");
m_MonitorProcessUnload = s2e()->getConfig()->getBool(getConfigKey() + ".monitorProcessUnload");
m_monitorThreads = s2e()->getConfig()->getBool(getConfigKey() + ".monitorThreads", true);
//读取内核起始地址,返回固定值0x80000000
m_KernelBase = GetKernelStart();
//标志是否是第一次调用插件的相关函数
m_FirstTime = true;
//XXX: do it only when resuming a snapshot.
//具体作用有待进一步分析
m_TrackPidSet = true;
unsigned i;
for (i=0; i<(unsigned)MAXVER; ++i) {
if (Version == s_windowsKeys[i]) {
m_Version = (EWinVer)i;
break;
}
}
if (i == (EWinVer)MAXVER) {
s2e()->getWarningsStream() << "Invalid windows version: " << Version << std::endl;
s2e()->getWarningsStream() << "Available versions are:" << std::endl;
for (unsigned j=0; j<MAXVER; ++j) {
s2e()->getWarningsStream() << s_windowsKeys[j] << ":\t" << s_windowsStrings[j] << std::endl;
}
exit(-1);
}
switch(m_Version) {
case XPSP2_CHK:
case XPSP3_CHK:
s2e()->getWarningsStream() << "You specified a checked build of Windows XP." <<
"Only kernel-mode interceptors are supported for now." << std::endl;
break;
default:
break;
}
//XXX: Warn about some unsupported features
if (m_Version != XPSP3 && m_monitorThreads) {
s2e()->getWarningsStream() << "WindowsMonitor does not support threads for the chosen OS version.\n"
<< "Please use monitorThreads=false in the configuration file\n"
<< "Plugins that depend on this feature will not work.\n";
}
m_pKPCRAddr = 0;
m_pKPRCBAddr = 0;
m_UserModeInterceptor = NULL;
m_KernelModeInterceptor = NULL;
if (m_UserMode) {
//定义一个用户模式的Interceptor对象
m_UserModeInterceptor = new WindowsUmInterceptor(this);
}
if (m_KernelMode) {
//定义一个内核模式的Interceptor对象
m_KernelModeInterceptor = new WindowsKmInterceptor(this);
}
//在翻译每条指令之前,CorePlugin插件都会发送onTranlateInstructionStart信号,S2E会进行相应的处理
s2e()->getCorePlugin()->onTranslateInstructionStart.connect(
sigc::mem_fun(*this, &WindowsMonitor::slotTranslateInstructionStart));
s2e()->getCorePlugin()->onPageDirectoryChange.connect(
sigc::mem_fun(*this, &WindowsMonitor::onPageDirectoryChange));
//读取配置文件中的模块列表
readModuleCfg();
}
1.3.3.2 对onTranslateInstructionStart信号的处理
S2E中的qemu在翻译每条指令之前,都会让CorePlugin插件发送onTranslateInstructionStart信号,WindowsMonitor插件连接onTranslateInstructionStart信号之后调用 slotTranslateInstructionStart函数进行相应的处理。 slotTranslateInstructionStart函数根据用户模式和内核模式,主要做了以下两方面的工作:
1.如果是用户模式:
如果是第一次调用此函数,则计算CPU相关数据结构的信息(这里主要获取KPCR结构);并且通过读取进程的PEB结构枚举进程所加载的所有模块。
InitializeAddresses(state); //计算CPU相关数据结构的信息[a]
m_UserModeInterceptor->GetPids(state, m_PidSet); //m_PidSet中存放的是当前进程的页目录表,在处理onPageDirectoryChange信号时会用到
m_UserModeInterceptor->CatchModuleLoad(state); //列举进程所加载的模块,并且发送onModuleLoad信号
m_PidSet.erase(state->getPid());
说明:
[a] KPCR是处理器控制区(Processor Control Region)的缩写,每一个CPU中都有一个KPCR结构,其中有一个域KPRCB(Kernel Processor Control Block)结构,这两个结构用来保存与线程切换相关的全局信息。KPCR是一个很大的数据结构,其中跟我们实际需求相关的比较重要的是KdVersionBlock指针,它指向了一个DBGKD_GET_VERSION64结构,这个结构中包含了PsLoadedModuleList信息,它是Windows加载的所有内核模块构成的链表的表头。还包括了内核加载地址、版本之类的重要信息。这个对用户模式下的模块追踪暂时看来没有很大的用处。
如果不是第一次调用此函数,则根据当前状态的pc进行以下处理:
if (pc == GetLdrpCallInitRoutine() && m_MonitorModuleLoad) { //[a]
signal->connect(sigc::mem_fun(*this, &WindowsMonitor::slotUmCatchModuleLoad));
}else if (pc == GetNtTerminateProcessEProcessPoint() && m_MonitorProcessUnload) { //[b]
signal->connect(sigc::mem_fun(*this, &WindowsMonitor::slotUmCatchProcessTermination));
}else if (pc == GetDllUnloadPc() && m_MonitorModuleUnload) { //[c]
signal->connect(sigc::mem_fun(*this, &WindowsMonitor::slotUmCatchModuleUnload));
}
说明:
[a]根据当前pc判断是否在装载dll。dll的装载和连接是通过ntdll中的一个函数LdrInitializeThunk实现的,这个函数实质上是ntdll.dll的入口函数,所以GetLdrpCallInitRoutine()函数中根据ntdll.dll的loadbase和nativebase计算此函数的地址,从而判断进程是否正在加载dll,并发送onModuleLoad信号。
[b]根据当前pc判断是否是进程结束,并发送onProcessUnload信号。
[c]根据当前pc判断是否是处在模块卸载,并发送onModuleUnload信号。
注:windows中一些相关的地址,如ntdll的加载地址、进程结束地址、模块卸载地址等都是固定的,以上实现都是通过写定windows中相关的地址(定义在WindowsMonitor.cpp中)计算得到的。
2.如果是内核模式(没有仔细看,留白):
如果是第一次调用,则更新模块列表,通知所有线程模块的加载: slotKmUpdateModuleList(state, pc); notifyLoadForAllThreads(state);如果不是第一次调用,则根据当前状态的pc进行以下处理:
if (pc == GetDriverLoadPc()) { //[a]
signal->connect(sigc::mem_fun(*this, &WindowsMonitor::slotKmModuleLoad));
}else if (pc == GetDeleteDriverPc()) { //[b]
signal->connect(sigc::mem_fun(*this, &WindowsMonitor::slotKmModuleUnload));
}else if (m_monitorThreads && pc == GetKeInitThread()) { [c]
signal->connect(sigc::mem_fun(*this, &WindowsMonitor::slotKmThreadInit));
}else if (m_monitorThreads && pc == GetKeTerminateThread()) { [d]
signal->connect(sigc::mem_fun(*this, &WindowsMonitor::slotKmThreadExit));
}
说明:
[a]根据当前状态的pc判断是否在加载驱动,并发送onModuleLoad信号;
[b]根据当前状态的pc判断是否卸载驱动,并发送onModuleUnload信号。
[c]根据当前状态的pc判断是否在创建内核线程,并发送onThreadCreate信号;
[d]根据当前状态的pc判断是否结束线程,并发送onThreadExit信号。
1.3.3.3 关于WindowsMonitor插件的onModuleLoad信号
WindowsMonitor? 扫描当前进程所有加载的模块,每加载一个模块都会发送onModuleLoad信号。WindowsMonitor插件是通过PEB结构枚举进程内所有已经加载的模块,这个功能通过WindowsUmInterceptor::FindModules来实现。基本原理和步骤如下:
1.找到进程的PEB结构。
进程的PEB结构在用户空间,如果当前状态是运行在用户空间,则通过FS:[30h]获取当前进程的PEB结构,如果运行在内核空间,则通过读取当前进程的EPROCESS结构来获取PEB结构。EPROCESS是进程的执行进程体,包含了进程的各种属性,位于系统空间。 if (state->getPc() < 0x80000000) { if(!state->readMemoryConcrete(fsBase + 0x18, &Peb, 4)) {//cdboot ask: why read 0x18 first? return false; } //通过fs:[30h]获取当前进程的PEB结构 if(!state->readMemoryConcrete(Peb+0x30, &Peb, 4)) { return false; } }else { //We are in kernel mode, do it by reading kernel-mode struc uint32_t curProcess = -1;
curProcess = m_Os->getCurrentProcess(state);
if (!curProcess) {
return false;
}
Peb = m_Os->getPeb(state, curProcess);
}
2.通过PEB结构找到PEB_LDR_DATA结构。
进程的PEB(Process Environment Block)结构中存放了进程的信息,每个进程都有自己的PEB信息。WindowsMonitor插件中的定义如下:
typedef struct _PEB32 {
uint8_t Unk1[0x8];
uint32_t ImageBaseAddress;
uint32_t Ldr; /* PEB_LDR_DATA */
} __attribute__((packed))PEB32;
其中Ldr指向PEB_LDR_DATA结构。加载的模块列表主要就是从这个数据结构中获得。WindowsMonitor插件中的定义如下:
typedef struct _PEB_LDR_DATA32
{
uint32_t Length;
uint32_t Initialized;
uint32_t SsHandle;
LIST_ENTRY32 InLoadOrderModuleList;
LIST_ENTRY32 InMemoryOrderModuleList;
uint32_t EntryInProgress;
} __attribute__((packed))PEB_LDR_DATA32;
这里有两个模块列表,InLoadOrderModuleList和InMemoryOrderModuleList,分别表示按照加载顺序的模块列表和按照内存顺序的模块列表。WindowsMonitor插件实现的是按照InLoadOrderModuleList来查找模块。
3.根据PEB_LDR_DATA获取InLoadOrderModuleList.Flink,开始遍历循环列表,枚举每个加载的模块。
具体实现代码如下(UserModeInterceptor.cpp):
//通过进程的PEB结构枚举进程内所有已加载的模块
bool WindowsUmInterceptor::FindModules(S2EExecutionState *state)
{
s2e::windows::LDR_DATA_TABLE_ENTRY32 LdrEntry;
s2e::windows::PEB_LDR_DATA32 LdrData;
if (!WaitForProcessInit(state)) {
return false;
}
//读取PEB_LDR_DATA的内容,m_LdrAddr是由WaitForProcessInit得来的
if (!state->readMemoryConcrete(m_LdrAddr, &LdrData, sizeof(s2e::windows::PEB_LDR_DATA32))) {
return false;
}
//计算LDR_DATA_TABLE_ENTRY真正的起始地址
uint32_t CurLib = CONTAINING_RECORD32(LdrData.InLoadOrderModuleList.Flink,
s2e::windows::LDR_DATA_TABLE_ENTRY32, InLoadOrderLinks);
uint32_t HeadOffset = m_LdrAddr + offsetof(s2e::windows::PEB_LDR_DATA32, InLoadOrderModuleList);
if (LdrData.InLoadOrderModuleList.Flink == HeadOffset) {
return false;
}
//枚举所有已经加载的dll
do {
if (!state->readMemoryConcrete(CurLib, &LdrEntry, sizeof(s2e::windows::LDR_DATA_TABLE_ENTRY32))) {
return false;
}
std::string s;
state->readUnicodeString(LdrEntry.BaseDllName.Buffer, s, LdrEntry.BaseDllName.Length);
std::transform(s.begin(), s.end(), s.begin(), ::tolower);
if (s.length() == 0) {
if (LdrEntry.DllBase == 0x7c900000) {
//XXX
//ntdll.dll总是被加载在固定的地址
s = "ntdll.dll";
}else {
s = "<unnamed>";
}
}
//if (m_SearchedModules.find(s) != m_SearchedModules.end()) {
//Update the information about the library
ModuleDescriptor Desc;
Desc.Pid = state->getPid();
Desc.Name = s;PEB结构枚举进程内所有已加载的模块
Desc.LoadBase = LdrEntry.DllBase;
Desc.Size = LdrEntry.SizeOfImage;
//XXX: this must be state-local
if (m_LoadedLibraries.find(Desc) == m_LoadedLibraries.end()) {
m_LoadedLibraries.insert(Desc);
NotifyModuleLoad(state, Desc);
}
CurLib = CONTAINING_RECORD32(LdrEntry.InLoadOrderLinks.Flink,
s2e::windows::LDR_DATA_TABLE_ENTRY32, InLoadOrderLinks);
}while(LdrEntry.InLoadOrderLinks.Flink != HeadOffset);
return true;
}
通过以上三个步骤,可以枚举所有模块,每一个模块都由一个ModuleDescriptor类型来描述模块属性,如模块名称、加载地址、镜像大小等。每遇到一个模块,加入到集合中去,并且调用NotifyModuleLoad函数来发送onModuleLoad信号。
ModuleExecutionDetector插件的作用是当进入或离开一个我们感兴趣的模块时告知其他插件。它依赖于一个OS monitor插件来定位模块在内存中的地址。
配置文件: ? pluginsConfig.ModuleExecutionDetector = { --将所有模块的加载和卸载事件通知给其他插件,但是不追踪这些模块的执行 trackAllModules=false, --把所有的模块都作为我们感兴趣的模块,如果此域为 true,则忽略下面配置的各 模块 configureAllModules=false, module_id1 = { --要监视的模块名称 moduleName=”notepad.exe", --标志此模块是位于内核空间还是用户空间 kernelMode= false, }, module_id2 ={ ... }, }
####1.4.3.1 ModuleExecutionDetector 插件的初始化
?
ModuleExecutionDetector插件首先初始化,完成以下几项工作:
1、连接OSMonitor插件发送的onModuleLoad、onModuleUnload和onProcessUnload信号;
2、连接CorePlugin插件发送的onTranslateBlockStart、onTranslateBlockEnd、onException和onCustomInstruction信号;
3、读取配置文件中的信息。 初始化在ModuleExecutionDetector.cpp中实现,代码如下:
void ModuleExecutionDetector::initialize()
{
m_Monitor = (OSMonitor*)s2e()->getPlugin("Interceptor");
assert(m_Monitor);
//对 OSMonitor 插件发送的相关信号的连接
m_Monitor->onModuleLoad.connect(
sigc::mem_fun(*this, &ModuleExecutionDetector::moduleLoadListener));
m_Monitor->onModuleUnload.connect(
sigc::mem_fun(*this, &ModuleExecutionDetector::moduleUnloadListener));
m_Monitor->onProcessUnload.connect(
sigc::mem_fun(*this, &ModuleExecutionDetector::processUnloadListener));
//对 CorePlugin 插件发送的相关信号的连接
s2e()->getCorePlugin()->onTranslateBlockStart.connect(
sigc::mem_fun(*this, &ModuleExecutionDetector::onTranslateBlockStart));
s2e()->getCorePlugin()->onTranslateBlockEnd.connect(
sigc::mem_fun(*this, &ModuleExecutionDetector::onTranslateBlockEnd));
s2e()->getCorePlugin()->onException.connect(
sigc::mem_fun(*this, &ModuleExecutionDetector::exceptionListener));
s2e()->getCorePlugin()->onCustomInstruction.connect(
sigc::mem_fun(*this, &ModuleExecutionDetector::onCustomInstruction));
//读取配置文件中的信息,主要是配置的需要追踪的模块的id和名称存入集合
//m_ConfiguredModulesId 和 m_ConfiguredModulesName 中
initializeConfiguration();
}
####1.4.3.2?对onModuleLoad信号的处理?
ModuleExecutionDetector插件连接OSMonitor发送的onModuleLoad信号,这里的OSMonitor插件以WindowsMonitor为例。前面讲到WindowsMonitor插件在翻译每条指令时,根据当前状态的pc判断如果正在加载模块,就发送onModuleLoad信号。这里ModuleExecutionDetector插件连接此信号后,调用moduleLoadListener函数进行相应处理。
此函数主要完成以下几项工作:
1、如果配置文件中定义所有模块都是感兴趣的模块(configureAllModules=true),则将当前模块加入到事先定义的集合(m_Descriptors)中去,并且发送ModuleExecutionDetector插件的onModuleLoad信号。
if (m_ConfigureAllModules) {
if (plgState->exists(&module, true)) { //该模块已经加载过
s2e()->getDebugStream() << " [ALREADY REGISTERED]" << std::endl;
}else {
//该模块未加载过
s2e()->getDebugStream() << " [REGISTERING]" << std::endl;
//加入到 m_Descriptors 集合中
plgState->loadDescriptor(module, true);
onModuleLoad.emit(state, module);
}
return;
}
2、对配置文件中定义的模块,判断当前模块是否是我们感兴趣的模块,则将当前模块加入到事先定义的集合(m_Descriptors)中去,并且发送ModuleExecutionDetector插件的onModuleLoad信号。
ConfiguredModulesByName::iterator it = m_ConfiguredModulesName.find(cfg);
if (it != m_ConfiguredModulesName.end()) {
if (plgState->exists(&module, true)) {
s2e()->getDebugStream() << " [ALREADY REGISTERED ID=" << (*it).id
<< "]" << std::endl;
}else {
s2e()->getDebugStream() << " [REGISTERING ID=" << (*it).id << "]" <<
std::endl;
plgState->loadDescriptor(module, true);
onModuleLoad.emit(state, module);
}
return;
}
3、如果配置文件中定义了trackAllModules=true,则加入事先定义的集合m_NotTrackedDescriptors中去,并且发送ModuleExecutionDetector插件的onModuleLoad信号。
if (m_TrackAllModules) {
if (!plgState->exists(&module, false)) {
s2e()->getDebugStream() << " [REGISTERING NOT TRACKED]" <<
std::endl;
plgState->loadDescriptor(module, false);
onModuleLoad.emit(state, module);
}
return;
}
[模块的描述]
每一个模块都由ModuleDescriptor结构来描述:
struct ModuleDescriptor
{
uint64_t Pid;
//The name of the module (eg. MYAPP.EXE or DRIVER.SYS)
std::string Name;
//Where the the prefered load address of the module.
//This is defined by the linker and put into the header of the image.
uint64_t NativeBase;
//Where the image of the module was actually loaded by the OS.
uint64_t LoadBase;
//The size of the image of the module
uint64_t Size;
//The entry point of the module
uint64_t EntryPoint;
//A list of sections
ModuleSections Sections;
ModuleDescriptor() {
Pid = 0;
NativeBase = 0;
LoadBase = 0;
Size = 0;
EntryPoint = 0;
}
bool Contains(uint64_t RunTimeAddress) const {
uint64_t RVA = RunTimeAddress - LoadBase;
return RVA < Size;
}
uint64_t ToRelative(uint64_t RunTimeAddress) const {
uint64_t RVA = RunTimeAddress - LoadBase;
return RVA;
}
uint64_t ToNativeBase(uint64_t RunTimeAddress) const {
return RunTimeAddress - LoadBase + NativeBase;
}
uint64_t ToRuntime(uint64_t NativeAddress) const {
return NativeAddress - NativeBase + LoadBase;
}
bool EqualInsensitive(const char *Name) const{
return strcasecmp(this->Name.c_str(), Name) == 0;
}
struct ModuleByLoadBase {
bool operator()(const struct ModuleDescriptor& s1,
const struct ModuleDescriptor& s2) const {
if (s1.Pid == s2.Pid) {
return s1.LoadBase + s1.Size <= s2.LoadBase;
}
return s1.Pid < s2.Pid;
}
};
bool operator()(const struct ModuleDescriptor* s1,
const struct ModuleDescriptor* s2) const {
if (s1->Pid == s2->Pid) {
return s1->LoadBase + s1->Size <= s2->LoadBase;
}
return s1->Pid < s2->Pid;
}
struct ModuleByName {
bool operator()(const struct ModuleDescriptor& s1,
const struct ModuleDescriptor& s2) const {
return s1.Name < s2.Name;
}
};
bool operator()(const struct ModuleDescriptor* s1,
const struct ModuleDescriptor* s2) const {
return s1->Name < s2->Name;
}
void Print(std::ostream &os) const {
};
os << "Name=" << Name << std::hex <<
" NativeBase=0x" << NativeBase << " LoadBase=0x" << LoadBase <<
" Size=0x" << Size <<
" EntryPoint=0x" << EntryPoint <<
std::dec << std::endl;
typedef std::set<struct ModuleDescriptor, ModuleByLoadBase> MDSet;
};
}
其中包含了很多信息,包括模块的名称、大小、加载地址等,并且提供了一些计算地址的函数。
除了onModuleload信号外还有onModuleUnload和onProcessUnload信号,对这两个信号的处理是通过调用函数ModuleExecutionDetector::moduleUnloadListener和ModuleExecutionDetector::processUnloadListener来实现的,这两个函数的功能很简单,只是输出相应的提示信息后分别调用各自相应的处理函数,代码如下所示:
void ModuleExecutionDetector::moduleUnloadListener(
S2EExecutionState* state, const ModuleDescriptor &module)
{
DECLARE_PLUGINSTATE(ModuleTransitionState, state);
//输出提示信息
s2e()->getDebugStream() << "Module " << module.Name << " is unloaded" << '\n';
//调用处理函数
plgState->unloadDescriptor(module);
}
void ModuleExecutionDetector::processUnloadListener(
S2EExecutionState* state, uint64_t pid)
{
DECLARE_PLUGINSTATE(ModuleTransitionState, state);
//输出提示信息
s2e()->getDebugStream() << "Process " << hexval(pid) << " is unloaded\n";
//调用处理函数
plgState->unloadDescriptorsWithPid(pid);
}?
####1.4.3.3?对onTranslationBlockStart信号的处理
ModuleExecutionDetector插件连接OSMonitor发送的onTranslationBlockStart信号,这里的OSMonitor插件以WindowsMonitor为例。前面讲到WindowsMonitor插件在翻译每个基本块时都会发送这个信号。这里ModuleExecutionDetector插件连接此信号后,调用onTranslateBlockStart函数进行相应处理。函数主要完成的工作是判断当前模块是否需要追踪,如果是我们感兴趣的模块,则发送ModuleExecutionDetector的onModuleTranslateBlockStart信号。 代码如下:
void ModuleExecutionDetector::onTranslateBlockStart(
ExecutionSignal *signal,
S2EExecutionState *state,
TranslationBlock *tb,
uint64_t pc)
{
DECLARE_PLUGINSTATE(ModuleTransitionState, state);
uint64_t pid = m_Monitor->getPid(state, pc);
//根据 pid 和 pc 判断当前运行的是否是我们关注的模块(在集合 m_Descriptors 中
查找)[a]
const ModuleDescriptor *currentModule =
plgState->getDescriptor(pid, pc);
if (currentModule) {
//是我们感兴趣的模块
//S2E::printf(s2e()->getDebugStream(), "Translating block %#"PRIx64"
belonging to %s\n",pc, currentModule->Name.c_str());
//连接 onExecution 信号,并调用 onExecution 函数处理,主要是针对模块
转换的,这里不仔细看
signal->connect(sigc::mem_fun(*this,
&ModuleExecutionDetector::onExecution));
}
}
//发送 onModuleTranslateBlockStart 信号
onModuleTranslateBlockStart.emit(signal, state, *currentModule, tb, pc);
[a] getDescriptor()函数负责查找当前模块是否是我们关注的模块。原理是将pc和集合中每个模块的加载地址进行比较,判断pc是否在其中。
####1.4.3.4 对 onTranslationBlockEnd 信号的处理
ModuleExecutionDetector 插件连接 corePlugin发送的onTranslationBlockEnd 信号。这里 ModuleExecutionDetector 插件连接此信号后,调用onTranslateBlockEnd 函数进行相应处理。该函数首先获得当前模块(currentModule),然后判断是否是静态目标,如果是则得到目标模块(targetModule)并判断目的模块是否和当前模块不同,如果是则直接连接到传入的执行信号(ExecutionSignal)signal,对该信号的处理是通过调用oduleExecutionDetector::onExecution函数来实现的,否则不做任何处理;如果目标是非静态的,即动态的,则直接链接到signal。最后函数会发送onModuleTranslateBlockEnd信号。代码如下所示:
void ModuleExecutionDetector::onTranslateBlockEnd(
ExecutionSignal *signal,
S2EExecutionState* state,
TranslationBlock *tb,
uint64_t endPc,
bool staticTarget,
uint64_t targetPc)
{
DECLARE_PLUGINSTATE(ModuleTransitionState, state);
//获取当前模块
const ModuleDescriptor *currentModule = getCurrentDescriptor(state);
if (!currentModule) {
// Outside of any module, do not need
// to instrument tb exits.
return;
}
if (staticTarget) {//如果目标是静态的
//获取目标模块
const ModuleDescriptor *targetModule =
plgState->getDescriptor(m_Monitor->getPid(state, targetPc), targetPc);
if (targetModule != currentModule) {//判断目标模块是否和当前模块不同
//Only instrument in case there is a module change
//TRACE("Static transition from %#"PRIx64" to %#"PRIx64"\n",
// endPc, targetPc);
//连接到signal
signal->connect(sigc::mem_fun(*this,
&ModuleExecutionDetector::onExecution));
}
}else {//目标是动态的
//TRACE("Dynamic transition from %#"PRIx64" to %#"PRIx64"\n",
// endPc, targetPc);
//In case of dynamic targets, conservatively
//instrument code.
signal->connect(sigc::mem_fun(*this,
&ModuleExecutionDetector::onExecution));
}
if (currentModule) {//发送onMuduleTranslateBlockEnd信号
onModuleTranslateBlockEnd.emit(signal, state, *currentModule, tb, endPc,
staticTarget, targetPc);
}
}
####1.4.3.5 对onException信号的处理
ModuleExecutionDetector 插件连接 corePlugin发送的 onException信号。这里 ModuleExecutionDetector 插件连接此信号后,调用exceptionListener 函数进行相应处理。 该函数的功能很简单,如果plgState的指向先前的模块的指针(m_PreviousModule)不为空的话则发送onModuleTransition信号并使指针为空,代码如下:
void ModuleExecutionDetector::exceptionListener(
S2EExecutionState* state,
unsigned intNb,
uint64_t pc
)
{
//std::cout << "Exception index " << intNb << '\n';
//onExecution(state, pc);
DECLARE_PLUGINSTATE(ModuleTransitionState, state);
//gTRACE("pid=%#"PRIx64" pc=%#"PRIx64"\n", pid, pc);
if (plgState->m_PreviousModule != NULL) {
onModuleTransition.emit(state, plgState->m_PreviousModule, NULL);
plgState->m_PreviousModule = NULL;
}
}
####1.4.3.6 对onCustomInstruction信号的处理
ModuleExecutionDetector 插件连接 corePlugin发送的onCustomInstruction信号。这里 ModuleExecutionDetector 插件连接此信号后,调用onCustomInstruction 函数进行相应处理。该函数首先会检查传入的表达式operand中是否含有MODULE_EXECUTION_DETECTOR_OPCODE,如果不包含则直接结束,如果包含则获取表达式operand中的一个8位的函数代码(8-bit function code ),然后根据这个8位的代码的不同情况采取不同的处理,代码如下:
void ModuleExecutionDetector::onCustomInstruction(
S2EExecutionState *state,
uint64_t operand
)
{
//检测operand中的制定的opcode
if (!OPCODE_CHECK(operand, MODULE_EXECUTION_DETECTOR_OPCODE)) {
return;
}
uint64_t subfunction = OPCODE_GETSUBFUNCTION(operand);
switch(subfunction) {
case 0: {
if (opAddModuleConfigEntry(state)) {
tb_flush(env);
state->setPc(state->getPc() + OPCODE_SIZE);
throw CpuExitException();
}
break;
}
}
}
?
FunctionMonitor 插件捕获所有的call/return机器指令,并且可以定义自己的回调函数来处理。
-
自己编写一个插件P;
-
在插件P的initialize()初始化函数中得到FunctionMonitor插件的实例;
FunctionMonitor m_monitor =static_cast<FunctionMonitor>(s2e()->getPlugin("FunctionMonitor"));
连接一个将S2EExecutionState类型的变量作为参数的信号,如所有的CorePlugin发出的信号。
s2e()->getCorePlugin()->onTranslateBlockStart.connect(sigc::mem_fun(*this,&Example::slotTranslateBlockStart));?
-
得到想要监控的函数的地址fa;
-
注册call信号;
callSignal = m_monitor->getCallSignal(state, functionAddress, -1); callSignal->connect(sigc::mem_fun(*this, &P::myFunctionCallMonitor)); //P::myFunctionCallMonitor 是自己定义的回调函数
-
如果需要的话注册ret信号。
####1.5.3.1?前置知识
1、函数调用的描述:使用数据结构CallDescriptor描述,其中包括进程的pid和CallSignal信号。
struct CallDescriptor {
uint64_t pid;
// TODO: add sourceModuleID and targetModuleID
FunctionMonitor::CallSignal signal;
};
整个函数调用的管理和组织使用CallDescriptorsMap来描述,这个结构是<pc,CallDescriptor>对:
typedef std::tr1::unordered_multimap<uint64_t, CallDescriptor> CallDescriptorsMap;
2、函数返回的描述:
使用数据结构ReturnDescriptor描述,其中包括进程的pid和ReturnSignal信号。
struct ReturnDescriptor {
//S2EExecutionState *state;
uint64_t pid;
// TODO: add sourceModuleID and targetModuleID
FunctionMonitor::ReturnSignal signal;
};
整个函数返回的管理和组织使用ReturnDescriptorsMap来描述,这个结构是<pc,ReturnDescriptor>对:
typedef std::tr1::unordered_multimap<uint64_t, ReturnDescriptor> ReturnDescriptorsMap;
####1.5.3.2?初始化
FunctionMonitor? 插件初始化时连接CorePlugin的两个信号:onTranslateBlockEnd和onTranslateJumpStart。
1、onTranslateBlockEnd(处理CallSignal信号)
首先判断当前的tb的类型是否是TB_CALL或者TB_CALL_IND类型,tb的类型是通过tb的s2e_tb_type域来记录的,根据汇编指令来判断,如果是,则先调用函数FunctionMonitor::slotCall(),经过预处理DECLARE_PLUGINSTATE(FunctionMonitorState, state)后, 最 终 调 用 FunctionMonitorState::slotCall 函数进行处理:
void FunctionMonitor::slotCall(S2EExecutionState *state, uint64_t pc)
{
DECLARE_PLUGINSTATE(FunctionMonitorState, state);
return plgState->slotCall(state, pc);
}
void FunctionMonitorState::slotCall(S2EExecutionState *state, uint64_t pc)
{
target_ulong pid = state->getPid();
target_ulong eip = state->getPc();
if (!m_newCallDescriptors.empty()) {
m_callDescriptors.insert(m_newCallDescriptors.begin(),
m_newCallDescriptors.end());
m_newCallDescriptors.clear();
}
/* Issue signals attached to all calls (pc==-1 means catch-all) */
//追踪进程的所有的函数调用
if (!m_callDescriptors.empty()) {
std::pair<CallDescriptorsMap::iterator, CallDescriptorsMap::iterator>
range = m_callDescriptors.equal_range((uint64_t)-1);
for(CallDescriptorsMap::iterator it = range.first; it != range.second; ++it) {
CallDescriptor cd = (*it).second;
if (m_plugin->m_monitor) {
pid = m_plugin->m_monitor->getPid(state, pc);
}
if(it->second.pid == (uint64_t)-1 || it->second.pid == pid) {
cd.signal.emit(state, this);
}
}
if (!m_newCallDescriptors.empty()) {
m_callDescriptors.insert(m_newCallDescriptors.begin(),
m_newCallDescriptors.end());
m_newCallDescriptors.clear();
}
}
/* Issue signals attached to specific calls */
//追踪特定的函数调用
if (!m_callDescriptors.empty()) {
std::pair<CallDescriptorsMap::iterator, CallDescriptorsMap::iterator>
range;
range = m_callDescriptors.equal_range(eip);
for(CallDescriptorsMap::iterator it = range.first; it != range.second; ++it) {
CallDescriptor cd = (*it).second;
if (m_plugin->m_monitor) {
pid = m_plugin->m_monitor->getPid(state, pc);
}
if(it->second.pid == (uint64_t)-1 || it->second.pid == pid) {
cd.signal.emit(state, this);
}
}
if (!m_newCallDescriptors.empty()) {
m_callDescriptors.insert(m_newCallDescriptors.begin(),
m_newCallDescriptors.end());
m_newCallDescriptors.clear();
}
}
}
这个函数中根据追踪特定的函数调用或者追踪所有的函数调用(根据pc值查找m_callDescriptors映射,pc=-1时表示追踪所有的call),分别发送CallSignal信号。
2、onTranslateJumpStart(处理ReturnSignal信号)
首先判断当前tb的类型是否是JT_RET或者JT_LRET类型,如果是,则先调用FunctionMonitor::slotRet函数,进行预处理DECLARE_PLUGINSTATE(FunctionMonitorState, state)后最终调用FunctionMonitorState::slotRet函数进行处理,代码如下:
void FunctionMonitor::slotRet(S2EExecutionState *state, uint64_t pc)
{
DECLARE_PLUGINSTATE(FunctionMonitorState, state);
return plgState->slotRet(state, pc, true);
}
void FunctionMonitorState::slotRet(S2EExecutionState *state, uint64_t pc, bool emitSignal)
{
target_ulong cr3 = state->readCpuState(CPU_OFFSET(cr[3]), 8*sizeof(target_ulong));
target_ulong esp;
bool ok = state->readCpuRegisterConcrete(CPU_OFFSET(regs[R_ESP]),
&esp, sizeof(target_ulong));
if(!ok) {
target_ulong eip = state->readCpuState(CPU_OFFSET(eip),
8*sizeof(target_ulong));
m_plugin->s2e()->getWarningsStream(state)
<< "Function return with symbolic ESP!" << '\n'
<< " EIP=" << hexval(eip) << " CR3=" << hexval(cr3) << '\n';
return;
}
if (m_returnDescriptors.empty()) {
return;
}
//m_plugin->s2e()->getDebugStream() << "ESP AT RETURN 0x" << std::hex << esp <<
// " plgstate=0x" << this << " EmitSignal=" << emitSignal << std::endl;
bool finished = true;
do {
finished = true;
std::pair<ReturnDescriptorsMap::iterator, ReturnDescriptorsMap::iterator>
range = m_returnDescriptors.equal_range(esp);
for(ReturnDescriptorsMap::iterator it = range.first; it != range.second; ++it) {
if (m_plugin->m_monitor) {
cr3 = m_plugin->m_monitor->getPid(state, pc);
}
if(it->second.cr3 == cr3) {
if (emitSignal) {
it->second.signal.emit(state);
}
m_returnDescriptors.erase(it);
finished = false;
break;
}
}
} while(!finished);
}
这个函数根据栈顶地址sp,以及pid,判断当前进程是否正在执行ret指令,然后发送ReturnSignal信号。
####1.5.3.3 注册一个call信号
在我们自己的插件中,得到需要监视的函数地址后,需要注册一个call信号,用于连接FunctionMonitor发送出的call信号。
注册call信号时传入的参数是需要监视的函数地址和进程的pid,返回一个CallSignal。
FunctionMonitor::CallSignal* FunctionMonitorState::getCallSignal(
uint64_t pc, uint64_t pid)
{
std::pair<CallDescriptorsMap::iterator, CallDescriptorsMap::iterator>
range = m_callDescriptors.equal_range(pc);
}
//在已存在的call信号中查找是否存在和申请注册的函数的pid一致的信号,如果存在则将已存在的
//call信号的信号返回
for(CallDescriptorsMap::iterator it = range.first; it != range.second; ++it) {
if(it->second.pid == pid)
return &it->second.signal;
}
//如果没有已存在的一致的信号,则按照传入的函数的参数创建一个call信号,并将其返回
CallDescriptor descriptor = { pid, FunctionMonitor::CallSignal() };
CallDescriptorsMap::iterator it =
m_newCallDescriptors.insert(std::make_pair(pc, descriptor));
return &it->second.signal;
###1.6.1 初识LibraryCallMonitor插件
LibraryCallMonitor插件能够捕获指定模块对导入的库函数的调用事件。这就遇到一个问题,如果函数是通过序号导入的,这个插件就无法监控到这个函数的调用。
###1.6.2 LibraryCallMonitor 插件的使用
配置文件:
pluginsConfig.LibraryCallMonitor = {
displayOnce=true,
moduleIds = {id1,id2,...}
}
这个插件依赖于"Interceptor", "FunctionMonitor", "ModuleExecutionDetector"插件。注:这个moduleIds列表中的moduleIds是ModuleExecutionDetector插件中定义的module的id。
###1.6.3 LibraryCallMonitor 插件的分析
初始化过程主要是连接两个信号,ModuleExecutionDetector的onModuleLoad信号和OSMonitor的onModuleUnload信号,并调用相应的函数处理。
1. onModuleLoad信号:
处理onModuleLoad信号的函数是LibraryCallMonitor::onModuleLoad(),函数的主要工作是得到指定模块的导入函数,得到各个导入函数的地址后,连接FunctionMonitor插件的CallSignal信号,进行相应处理,并发送onLibraryCall信号。
[问题:]我的实验使用onModuleLoad信号无法取得导入表,换为onModuleTranslationBlockStart信号才可用,哪里出错?
2. onModuleUnload信号:
处理onModuleUnload信号的函数是LibraryCallMonitor::onModuleUnload(),函数的主要工作是关闭FunctionMonitor的信号。
qemu中实现内存管理时,提供了SoftMMU机制。而在QEMU的softmmu代码中很多函数的功能是相似的,只是处理的数据类型不同,例如定义ld和st操作时,softmmu_defs.h文件中声明的函数有16个:
uint8_t __ldb_mmu(target_ulong addr, int mmu_idx);
void __stb_mmu(target_ulong addr, uint8_t val, int mmu_idx);
uint16_t __ldw_mmu(target_ulong addr, int mmu_idx);
void __stw_mmu(target_ulong addr, uint16_t val, int mmu_idx);
uint32_t __ldl_mmu(target_ulong addr, int mmu_idx);
void __stl_mmu(target_ulong addr, uint32_t val, int mmu_idx);
uint64_t __ldq_mmu(target_ulong addr, int mmu_idx);
void __stq_mmu(target_ulong addr, uint64_t val, int mmu_idx);
uint8_t __ldb_cmmu(target_ulong addr, int mmu_idx);
void __stb_cmmu(target_ulong addr, uint8_t val, int mmu_idx);
uint16_t __ldw_cmmu(target_ulong addr, int mmu_idx);
void __stw_cmmu(target_ulong addr, uint16_t val, int mmu_idx);
uint32_t __ldl_cmmu(target_ulong addr, int mmu_idx);
void __stl_cmmu(target_ulong addr, uint32_t val, int mmu_idx);
uint64_t __ldq_cmmu(target_ulong addr, int mmu_idx);
void __stq_cmmu(target_ulong addr, uint64_t val, int mmu_idx);
其中以ld操作为例,__ld函数分为mmu和cmmu两类,每种类型又根据返回类型分为4个类型,例如返回值为uint8_t时,函数名以__ldb开头,表示字节操作。同理,st操作也有相同的特点。根据这个特点,定义这些函数只用了两个函数体,在softmmu_template.h文件中定义:
#define DATA_SIZE (1 << SHIFT)
#if DATA_SIZE == 8
#define SUFFIX q
#define USUFFIX q
#define DATA_TYPE uint64_t
#elif DATA_SIZE == 4
#define SUFFIX l
#define USUFFIX l
#define DATA_TYPE uint32_t
#elif DATA_SIZE == 2
#define SUFFIX w
#define USUFFIX uw
#define DATA_TYPE uint16_t
#elif DATA_SIZE == 1
#define SUFFIX b
#define USUFFIX ub
#define DATA_TYPE uint8_t
#else
#error unsupported data size
#endif
static DATA_TYPE glue(glue(slow_ld, SUFFIX), MMUSUFFIX)(target_ulong addr,
int mmu_idx,
void *retaddr)
{
...
}
#ifndef SOFTMMU_CODE_ACCESS
static void glue(glue(slow_st, SUFFIX), MMUSUFFIX)(ENV_PARAM
target_ulong addr,
DATA_TYPE val,
int mmu_idx,
void *retaddr)
{
...
}
其中glue是一个宏定义(osdep.h):
#define xglue(x, y) x ## y
#define glue(x, y) xglue(x, y)
在exec.c和op_helper.c中真正实际定义了这些函数,例如在op_helper.c中定义了mmu系列的函数:
#define MMUSUFFIX _mmu
#define SHIFT 0
#include "softmmu_template.h"
#define SHIFT 1
#include "softmmu_template.h"
#define SHIFT 2
#include "softmmu_template.h"
#define SHIFT 3
#include "softmmu_template.h"
这相当于定义了ld和st操作的所有mmu系列的8个函数。
在由guest binary转化为tcg时,遇到ld和st操作就会调用上述的函数,在这些函数中会调用S2E_TRACE_MEMORY(),定义如下:
#ifdef S2E_LLVM_LIB //符号执行
#define S2E_TRACE_MEMORY(vaddr, haddr, value, isWrite, isIO) \
tcg_llvm_trace_memory_access(vaddr, haddr, \
value, 8*sizeof(value), isWrite, isIO);
#define S2E_FORK_AND_CONCRETIZE(val, max)
tcg_llvm_fork_and_concretize(val, 0, max)
#else // S2E_LLVM_LIB //具体执行
#define S2E_TRACE_MEMORY(vaddr, haddr, value, isWrite, isIO) \
s2e_trace_memory_access(vaddr, haddr, \
(uint8_t*) &value, sizeof(value), isWrite, isIO);
#define S2E_FORK_AND_CONCRETIZE(val, max) (val)
#endif // S2E_LLVM_LIB
在符号执行时它定义为 tcg_llvm_trace_memory_access(),在具体执行时,它定义为 s2e_trace_memory_access()。
不管实际调用的是哪个函数,都会发送CorePlugin的onDataMemoryAccess信号。这样,MemoryChecker插件连接这个信号,进行相应的内存检查。
检查程序访存错误的插件。需要在程序进行st或者ld操作的时候显式的添加对所操作的内存进行监视。
配置文件的写法:
pluginsConfig.MemoryChecker = {
checkMemoryErrors = true,
checkMemoryLeaks = true,
checkResourceLeaks = true,
terminateOnErrors = true,
terminateOnLeaks = true,
//标记是否需要记录在MemoryTracer插件的log文件中
traceMemoryAccesses = false,
}
依赖的插件:"ModuleExecutionDetector", "ExecutionTracer", "MemoryTracer", "Interceptor"
####1.7.4.1 初始化
MemoryChecker插件初始化时主要工作是连接ModuleExecutionDetector插件的onModuleTransition信号,和CorePlugin的onStateSwitch和onException信号。
[1]onModuleTransition
处理onModuleTransition信号的函数是MemoryChecker::onModuleTransition(),这个函数的主要工作是连接CorePlugin的onDataMemoryAccess信号。
连接onDataMemoryAccess信号后,调用函数MemoryChecker::onDataMemoryAccess()进行相应处理。这个函数的主要工作如下:
a、判断是否进行访存检查,如果不检查则返回;在执行中断或处理异常、对符号内存进行访存时直接返回,不进行访存检查;
b、进行预检查,发送OnPreCheck信号;预检查是预留给其他插件的信号,其他插件连接后可以进行更细粒度的检查。[可以编写新的插件]
c、调用checkMemoryAccess()函数进行内存访问的简单检查;
d、如果内存访问检查有错误,则发送OnPostCheck信号。其他插件可以连接这个信号,进行后续的检查,目前StackChecker插件就是连接这个信号的。[也可以编写新的插件对这个信号连接,检查访存错误]
函数中最主要的是调用了checkMemoryAccess()函数对访存错误进行检查,返回标志hasError=true表示有错误,hasError=false表示没有错误。
函数主要完成以下工作:
a、判断ld或者st的对象值的长度是否是0或者负数;
if(size != uint64_t(-1) && start + size < start) {
err << "MemoryChecker::checkMemoryAccess: "
<< "BUG: freeing region of " << (size == 0 ? "zero" : "negative")
<< " size!" << std::endl;
hasError = true;
break;
}
b、判断ld或者st指令的操作地址是否在事先定义的需要监视的范围内:(说法不一定准确)
MemoryChecker中维持了一个内存区域映射,映射的组织是一个二叉树结构,按照地址大小存储,左节点比右节点的地址小。其中,内存映射MemoryMap的定义:
typedef klee::ImmutableMap<MemoryRange, const MemoryRegion*,
MemoryRangeLT> MemoryMap;
struct MemoryRange {
uint64_t start;
uint64_t size;
};
struct MemoryRegion {
MemoryRange range; //内存区域
MemoryChecker::Permissions perms; //内存区域的访问权限
uint64_t allocPC;
std::string type; //自己设定的内存区域类型,
uint64_t id; //标志ID,一般用地址
bool permanent; //标志是否是永久的
};
struct MemoryRangeLT {
bool operator()(const MemoryRange& a, const MemoryRange& b) const {
return a.start + a.size <= b.start;
}
};
MemoryChecker插件在内存映射中查找访存的地址是否存在访存错误。
MemoryMap &memoryMap = plgState->getMemoryMap();
MemoryRange range = {start, size};
//找到内存映射树中的符合要求的节点,lookup_previous()函数的实现见后面
const MemoryMap::value_type *res = memoryMap.lookup_previous(range);
if(!res) {
//在memory map中找不到比start地址小的地址,也就是说memory tree中没有登记start开始的内存
err << "MemoryChecker::checkMemoryAccess: "
<< "BUG: memory range at " << hexval(start) << " of size " << hexval(size)
<< " cannot be accessed by instruction " << getPrettyCodeLocation(state)
<< ": it is not mapped!" << std::endl;
hasError = true;
break;
}
if(res->first.start + res->first.size < start + size) {
//访存的数据大小大于树中登记的内存
err << "MemoryChecker::checkMemoryAccess: "
<< "BUG: memory range at " << hexval(start) << " of size " << hexval(size)
<< " can not be accessed by instruction " << getPrettyCodeLocation(state)
<< ": it is not mapped!" << std::endl
<< " NOTE: closest allocated memory region: " << *res->second << std::endl;
hasError = true;
break;
}
if((perms & res->second->perms) != perms) {
//访问权限不匹配
err << "MemoryChecker::checkMemoryAccess: "
<< "BUG: memory range at " << hexval(start) << " of size " << hexval(size)
<< " can not be accessed by instruction " << getPrettyCodeLocation(state)
<< ": insufficient permissions!" << std::endl
<< " NOTE: requested permissions: " << hexval(perms) << std::endl
<< " NOTE: closest allocated memory region: " << *res->second << std::endl;
hasError = true;
break;
}
这里首先调用getMemoryMap()来得到内存区域,然后调用lookup_previous()函数来查找内存映射树中比start地址小的最大的地址的节点。lookup_previous()代码实现如下:
//在tree中查找如果能找到K,则返回k;找不到则返回K的前一个节点(树中所有节点中比K小的最大的节点)
ImmutableTree<K,V,KOV,CMP>::lookup_previous(const key_type &k) const {
Node *n = node;
Node *result = 0;
while (!n->isTerminator()) {
key_type key = key_of_value()(n->value);
//key_compare()(k,key)的作用表示,如果k<key,返回true;如果k>key,返回false
if (key_compare()(k, key)) {
n = n->left;
} else if (key_compare()(key, k)) {
result = n;
n = n->right;
} else {
return &n->value;
}
}
return result ? &result->value : 0;
}
要想成功使用MemoryChecker插件,需要首先在其他插件中事先在内存映射memoryMap中登记需要监视的内存区域,如果不在其他插件中调用MemoryChecker的相应的函数来设置内存区域,那么树是空的,这就解释了为什么在测试Linux中的echo程序中出现那么多“it is not mapped”的警告。设置内存区域的函数是setMemoryMap()函数。需要在自己的插件中直接或者间接调用setMemoryMap()函数,来设置需要监视的内存区域。以下列出了MemoryChecker中一些调用setMemoryMap()的函数。一般使用grantMemory()函数登记需要监视的内存区域。下一节会介绍grantMemory()函数。
[2]onStateSwitch
处理这个信号的函数是MemoryChecker::onStateSwitch()。处理这个信号的过程和处理onModuleTransition信号的过程一样。
[3]onException
处理这个信号的函数MemoryChecker::onException()。这个函数只将与CorePlugin信号的连接切断
####1.7.4.2 其他函数
[1] grantMemory函数
这个函数的作用是可以对我们指定的内存区域进行认证,确保在使用MemoryChecker插件时可以对这些内存区域访问。一般用于申请内存空间时使用。
void MemoryChecker::grantMemory(S2EExecutionState *state,
uint64_t start, uint64_t size, Permissions perms,
const std::string ®ionType, uint64_t regionID,
bool permanent)
{
DECLARE_PLUGINSTATE(MemoryCheckerState, state);
MemoryMap &memoryMap = plgState->getMemoryMap();
//生成新的MemoryRegion对象,用于存储需要认证的内存区域
MemoryRegion *region = new MemoryRegion();
region->range.start = start;
region->range.size = size;
region->perms = perms;
region->allocPC = state->getPc();
region->type = regionType;
region->id = regionID;
region->permanent = permanent;
s2e()->getDebugStream(state) << "MemoryChecker::grantMemory("
<< *region << ")" << '\n';
/********************************************/
/* Write a log entry about the grant event */
//ExecutionTracer插件为grant事件记录一个log入口,此段关系不大,暂时省略
/********************************************/
//判断需要认证的内存区域大小是否为0或者负数
if(size == 0 || start + size < start) {
s2e()->getWarningsStream(state) << "MemoryChecker::grantMemory: "
<< "detected region of " << (size == 0 ? "zero" : "negative")
<< " size!" << std::endl
<< "This probably means a bug in the OS or S2E API annotations" << std::endl;
delete region;
return;
}
//判断需要认证的内存区域是否与现有的区域重叠
const MemoryMap::value_type *res = memoryMap.lookup_previous(region->range);
if (res && res->first.start + res->first.size > start) {
s2e()->getWarningsStream(state) << "MemoryChecker::grantMemory: "
<< "detected overlapping ranges!" << '\n'
<< "This probably means a bug in the OS or S2E API annotations" << '\n'
<< "NOTE: requested region: " << *region << '\n'
<< "NOTE: overlapping region: " << *res->second << '\n';
delete region;
return;
}
//将新的MemoryRegion对象存入memoryMap中
plgState->setMemoryMap(memoryMap.replace(std::make_pair(region->range, region)));
}
2 S2E插件的开发(作者:曹鼎;邮箱:caoding8483@163.com)
该插件用于将输入数据符号化,并在进行不安全操作(如拷贝、分配内存)时进行相应断言,以检测漏洞。该插件可以适用于整数溢出插件和缓冲区溢出插件。
该插件对每一条指令进行监控,进行如下操作:
1.当运行到接收输入数据的函数时(如ReadFile、recv函数),对读入的缓冲区进行符号化,将缓冲区的每一个字节都标记为符号变量;
2.当运行到不安全操作函数时(如malloc、memcpy函数),对这些函数进行相应的断言,检测漏洞是否存在。
结合静态分析和动态分析,在实际测试之前将接收输入数据函数的第一条指令地址写入文件中(./data_recv_functions.txt)。文件中的组织结构如下:
recvfrom:0x71a22ff7
WSARecv:0x71a24cb5
recv:0x71a2676f
WSARecvFrom:0x71a2f66a
ReadFile:0x7c801812
RtlAllocateHeap:0x7c9300a4
RtlReAllocateHeap:0x7c939b80
在插件初始化时,将其中的函数及其首地址读入定义好的向量中,对向量中的每个地址进行监控。当程序实际运行到这些地址时,根据函数调用时栈中的存储规则,函数相应参数依次压栈,此时的esp寄存器中存放的是函数的返回地址(及call的下一条指令),将返回地址也加入需监视的地址列表中。当程序运行到返回地址时,输入数据刚刚存入缓冲区中,此时将输入数据缓冲区进行符号化。过程示意图如图1.
结合静态分析和动态分析,在实际测试之前将接收输入数据函数的第一条指令地址写入文件中(./unsafe_functions.txt)。文件中的组织结构如下:
rep movsd:0x0038096b
malloc:0x00401ab2
_memcpy:0x004037a0
在插件初始化时,将其中的函数及其首地址读入定义好的向量中,对向量中的每个地址进行监控。当程序实际运行到这些地址时,根据不同检测策略进行相应的断言,检测是否存在漏洞。