如果想运行可以自己实现调度功能,主要就是按KiDispatchInterrupt,KiReadyThread,SwapContext等函数实现,很复杂,当然还有一些其他的细节需要注意,可以参考sinister大牛的NT 内核的进程调度分析笔记 和WRK相关代码。下面这是老外的那个思路,我也不知这个文档怎么跑我硬盘上了,哈哈。不过这个能过内存搜索吗?
1. 介绍
--------------------------------------------------------------------------------
Joanna Rutkowska 编写的Klister 0.4 是一个很棒的程序,它通过遍历KiWaitInListHead,
KiWaitOutListHead 和 KiDispatcherReadyListHead数组列举存在的线程.它是为W2K专门设计的,无法运用于其他的操作系统。Joanna 做过这样的假设,每个线程要么等待,要么就绪运行,它们都属于这些链表中的一个。然而,我们都记得Joanna在Klister 0.3中的错误 - 她认为操作系统中的每一个进程都必需有独一无二的进程标识(PID)。所以,那时她所说的“从刚才提到的内部调度链表中删除隐藏进程中的线程是不太可能的”,在现在看来还对吗?因为我们的隐藏进程将不会得到任何的CPU运行时间[1]。答案是否定的。从这些链表中删除线程是可能的,并且运行我们自己的调度程序来仅仅调度我们自己的隐藏线程也是完全可能的。
2. NT调度程序工作方式的简单描述
--------------------------------------------------------------------------------
接近第一阶段初始化结束的时候,MmInitSystem启动两个线程:KeBalanceSetManager
和 KeSwapProcessOrStack。这就是平衡集管理器(balance set manager)。
KeSwapProcessOrStack 启动一个处理交换(swap)事件的无限循环。交换事件由KiSwapEvent
触发。有四种不同类型的事件。
- 将内核堆栈交换出去(由BOOLEAN KiStackOutSwapRequest指定);
- 将进程交换出去 (需要交换出去的进程存放在KiProcessOutSwapListHead中)
- 将进程交换进来 (需要交换出去的进程存放在KiProcessInSwapListHead中)
- 将内核堆栈交换进来(需要交换进来的线程存放在KiStackInSwapListHead中).
KeBalanceSetManager也一直循环着并等待着一个MmWorkingSetManagerEvent事件(当内存低时
调整工作集的大小)和另一个定时器。定时器事件处理程序周期性地将KiStackOutSwapRequest设置为 TRUE,并且触发KiSwapEvent信号通知KeSwapProcessOrStack线程,KeSwapProcessOrStack 线程不得不将长时间等待某个东西的线程的内核堆栈交换出去。KeBalanceSetManager也调用KiScanReadyQueues 来提高在就绪队列中线程(KiDispatcherReadyListHead数组)的优先级。对于每一个提高了优先级的线程, KiReadyThread将会被调用,所以马上将PRCB.NextThread设置为提高了优先级的线程也是很有可能的(KiReadyThread 会抢占原先的NextThread)。
KeUpdateSystemTime 直接由HAL的定时中断处理程序调用。它随后调用KeUpdateRunTime,
“更新当前线程的运行时间,当前线程所属进程的运行时间和减少当前线程的时间片”。当KeUpdateRunTime注意到当前线程不是Idle线程并且它的时间片用完了,它通过触发一个调度中断请求时间片结束。
KiDispatchInterrupt检查是否请求了时间片结束或已经选择了PRCB.NextThread。如果是,
它设置PRCB.CurrentThread指向PRCB.NextThread,将PRCB.NextThread 清0,最后通过调用 KiReadyThread 让 PRCB.CurrentThread 就绪运行。
KiReadyThread 检查线程所属进程的状态,如果进程已经被交换出去,就将它的内存交换
进来(他将进程插入到KiProcessInSwapListHead中并触发KiSwapEvent事件)。如果线程的内核堆栈不驻留在内存的,KiReadyThread将线程插入到KiStackInSwapListHead中,触发KiSwapEvent 将线程内核堆栈交换进来。
如果线程所属的进程内存和线程的内核堆栈都是驻留的,KiReadyThread寻找一个空闲的
处理器,如果至少有一个空闲的处理器,它设置(每个空闲处理器)各自的PRCB的NextThread字段到特定的线程。如果有很多个空闲的处理器,线程想要运行的处理器将成为优先选择。
如果没有空闲的处理器,将检查IdealProcessorPRCB.NextThread优先级。如果
IdealProcessorPRCB.NextThread是可以抢占的(指定的线程具有较高的优先级),KeReadyThread 设置它为指定的线程。如果IdealProcessorPRCB.NextThread没有被设置,KiReadyThread检查 IdealProcessorPRCB.CurrentThread是否能抢占。如果可以,NextThread就被设置为指定的线程,再次请求调度中断。
如果没有线程可以被抢占,KiReadyThread 根据线程的优先级,将其插入到调度程序队列
(KiDispatcherReadyListHead)中去,修正各自的KiReadySummary位,表示这个优先级数组非空。
当线程准备好了,KiDispatchInterrupt调用SwapContext在线程之间进行上下文切换。
3. 代码段抽取
--------------------------------------------------------------------------------
3.1 引擎信息
--------------------------------------------------------------------------------
主要的想法是创建一个系统线程调度程序的拷贝,有必要对它作一些修补并且运行一个由
驱动创建的线程,用来在所有隐藏线程之间(仅仅在它们之间)共享时间片。因此 ,我们需要一个引擎,用来从指定的入口点开始创建代码段执行树的拷贝。
在通常情况下,从头到尾分析每一个控制路径是不可能。
- 区分数据段和代码段总是不现实的;
- 代码段很可能包含不可预测的跳转和调用,例如call/jcc [mem32],
call/jcc [reg32] and call/jcc reg32.
由于这个原因,代码段抽取引擎只能分析控制路径到不可预测的跳转。这里是该引擎工作方式的
大致介绍。
1. 用户指定导出符号的名字,ntoskrnl的RVA(相对虚地址),VA(虚地址)和服务ID
作为所需函数的入口点。在服务ID的情况下,引擎使用了我曾经在“一种发现KiServiceTable较稳定的方法”("A more stable way to find real KiServiceTable")一文中所描述过的方法来决定它们的RVA [2]。
2. 引擎查找一个导入的内核文件。接着创建一个到这个文件“映像”的视图并打开它。
假设挂钩程序没有在不工作时修补过这个视图:打开的视图就认为是没有被挂过钩的。
3. 两个bit域根据ntoskrnl映像的大小创建。第一个(称为pCoverage)将是一个“覆盖”
数组(它储存属于所选子程序执行图的字节信息),第二个(称为p0pcodeStart)- 操作码开始数组(专为在反汇编过程中的破坏了的重定位项和其他一些反汇编错误检查)
4. 对于在第1步指定的每个RVA, 我们开始遍历它的代码段。换句话说,我们在pCoverage
位域中标记指令字节,在pOpcodeStart位域中标记操作码开始字节。我们进入需要调用的子函数并在条件跳转处“交叉”执行。如果我们碰到ret,iret,不可预见的控制跳转或者是已经分析过的代码段,那么执行分支就结束了。如果任何的 call/jcc rel32/rel8跳转执行已经分析过的代码段 ,但目标的第一个字节没有在pOpcodeStart中有相应位设置,汇编过程将以"general disasm error" (一般性汇编错误)结束。
5. 从没有挂钩的视图复制所有相关的字节到分配好的非分页内存中。这块空间的大小和
涉及到的代码段字节数相同 - 我们无隙地复制了代码段区域。当合并这些区域时,在重构过的新代码段中重新找到用户指定函数的RVA也变得很简单了,同样在用户缓存中储存它们也很简单。
6. 引擎在输出的代码段中重新链接所有的相对跳转/调用,因为在删除位于涉及到的代码段区域
之间的代码段之后,这些指针都被破坏了。短跳转(rel8)不能被扩展到near(rel32),因为我们
仅仅减少所有的偏移。如果代码段覆盖步骤已经正确地完成,每个relXX操作码在相应的代码段中都
有自己的目的地址。
7. 引擎必须要做的最后一件事是修正重定位表。所有指向相关代码段的定位项都被修正到
真正的ntoskrnl映橡基址。这是必须的,因为我们不想处理数据段 - 我们把所有的绝对指针看成数据引用,如果有任何的代码引用,仅仅忽略它们。例如通过“push offs32,ret”或通过在SEH结构中造成一个异常,可以跳转执行到那儿。但我认为,在现成的内核映像中挂钩这些代码偏移没有很大的意义,因为它们在代码段中太深了 - 所以,它们实在太底层了,无法用已知的结构来操作。
3.2. 用法
--------------------------------------------------------------------------------
一个用法的例子,你可以参考Phide2主引擎代码。这是代码段抽取引擎的原型。
NTSTATUS NTAPI PullOutCode(
// filled array of IMPORT_ENTRY structures
IN PIMPORT_ENTRY Import,
// array for code entries in the resulting code
IN OUT PULONG CodeEntries,
// address of the buffer with the resulting code
OUT PUCHAR *NewCode OPTIONAL,
// size of the resulting code
OUT PULONG NewCodeSize OPTIONAL,
// imagebase of the module which imports need to be included to the code
IN PUCHAR pModuleForImportPatching OPTIONAL,
// user-defined callback for fixing absolute pointers in the resulting code
IN FIXRELOCS_CALLBACK FixRelocsCallback OPTIONAL
);
一个回调函数为了修补在生成的代码段中重定位项:
typedef VOID (__stdcall *FIXRELOCS_CALLBACK)(
// imagebase of the parsed module
PUCHAR pImage,
// address of the pointer in the resulting code
PULONG pFixedAddress,
// RVA of the pointer in the parsed module
ULONG OriginalFixupRva,
// RVA this pointer points to in the parsed module
ULONG TargetRva
);
引擎会返回下面状态码中的一个。
STATUS_SUCCESS
一切正常,代码段的拷贝可以使用
STATUS_PASSIVE_LEVEL_REQUIRED
PullOutCode()在提升的IRQL下被调用。
STATUS_NTOSKRNL_NOT_FOUND
引擎无法在磁盘上发现内核映像。不可思议。
STATUS_MAP_IMAGE_FAILED
引擎无法视图内核映像
STATUS_ADD_FUNCTION_FAILED
引擎无法添加一个函数到代码段遍历器的入口点链表(譬如,没有找到导出)
STATUS_COVERAGE_ERROR
引擎无法创建一个代码段树。
STATUS_CODE_REBUILDING_FAILED
引擎无法修正复制来的代码段
STATUS_UNSUCCESSFUL
一般性错误发生。
4. Phide2
--------------------------------------------------------------------------------
4.1 运行一个新的调度程序所必需的东西
--------------------------------------------------------------------------------
当我们从KiWaitInListHead, KiWaitOutListHead 和 KiDispatcherReadyListHead中
排除线程,klister再也不会看到它们。问题是:如何调度这些排除的线程呢?我们不得不一直给它们提供时间片。但我们不能调用KiReadyThread,,这是因为如果隐藏线程处于等待状态并且它的堆栈被交换出去了(这是一种很常见的情况),而这时SwapContext试图从隐藏线程的上下文中读取数据,那么SwapContext就会向系统报错。 接着
- 从KiWaitIn/OutListHead上解开会造成KE无法成功地满足线程的等待。
- 线程的堆栈不能被交换进来。
解决方法很简单: 我们应该创建一个新的链表,从原来的链表中移除隐藏线程,把它们
放到我们的链表中,接着创建一个足够大的调度程序代码段的拷贝,将所有指向老链表的指针修改
为指向到我们的链表。
当我们要调用拷贝过的代码段,我们确定其中一个隐藏线程经过验证比当前线程(也许不隐藏)
的优先级要高,因此如果当前进程只有较低的优先级,那么就抢占当前线程。此外,隐藏线程将不再会移动原来的调度程序链表,因为我们的代码段拷贝不知道原先的链表在那儿。
有一个很有用的函数NtYieldExecution,“它可以让任何就绪的线程暂停执行,等待下一个
时间片的来临”如果我们修补了所有的执行树,使用我们的链表代替原来的,它只会调度执行我们的线程。
我们还应做的另一件事是运行我们自己的平衡管理器,因为当隐藏线程的等待条件刚好被满足
并且没有内核堆栈驻留时,有人就必须将隐藏线程的内核堆栈交换进来。
这些指针必须在NtYieldExecution, KeBalanceSetManager 和 KeSwapProcessOrStack的
执行树中修补,以使我们的调度程序的拷贝与原来的相隔离,并且要使它可行。
KiDispatcherReadyListHead (包含32个LIST_ENTRY结构的数组 - 修补 Flink and Blink)
KiWaitInListHead (LIST_ENTRY)
KiWaitOutListHead (LIST_ENTRY)
(XP 特殊之处: 只有 KiWaitListHead LIST_ENTRY 代替上面两个)
KiStackInSwapListHead (NT, 2k: LIST_ENTRY, XP: SINGLE_LIST_ENTRY)
KiProcessInSwapListHead (NT, 2k: LIST_ENTRY, XP: SINGLE_LIST_ENTRY)
KiProcessOutSwapListHead (NT, 2k: LIST_ENTRY, XP: SINGLE_LIST_ENTRY)
KiReadySummary (ULONG)
KiReadyQueueIndex (ULONG)
KiStackOutSwapRequest (BOOLEAN)
KiSwapEvent (KEVENT)
KiSwappingThread (PETHREAD)
(XP 特殊之处: NT and 2k 没有这个全局变量)
我们需要4个线程运行我们的调度程序。
第一个修补过的KeBalanceSetManager
第二个修补过的KeSwapProcessOrStack
第三个周期性地从原来的链表中排除隐藏对象(线程和进程),将它们插入到我们新的链表中去
- 这是需要的一些函数会将线程放到调度程序链表(例如KeSetThreadPriority函数)
第四个周期性地调用修补过的NtYieldExecution给隐藏的线程让出时间片。
4.2. 如何找到所需的非导出符号
--------------------------------------------------------------------------------
硬编码的偏移真的很糟。实在有太多不同的ntoskrnl了 - check/free Build版本,
单/多处理器,PAE(物理地址扩展),不同的热修补(hotfix)等等。所以我决定通过分析内核而不使用任何的代码特征查找所有需要的符号。
4.2.1. NtYieldExecution
--------------------------------------------------------------------------------
这是最简单的一个。ZwYieldExecution由ntoskrnl导出。因此,通过读取在第一
条指令"mov eax, imm32"的imm32(32位立即数),我们可以发现它的服务id。接着通过查找KiServiceTable 数组,我们应该可以得到NtYieldExecution地址,这种方法可以在[2]中找到。
4.2.2. KiWait[In/Out]ListHead, KeDispatcherReadyListHead, KiReadySummary
--------------------------------------------------------------------------------
首先,我们查找KiWaitInListHead和KiWaitOutListHead ( XP中的KiWaitListHead)。
达到这一点,我们可以通过遍历KeWaitForSingleObject, KeWaitForMultipleObjects 和 KeDelayExecutionThread (它们都是输出的)并且不用进入它们的子函数。对于每个函数,我们创建一个被这些函数使用过的全局变量表(不包含子函数)。接着我们计算出三组全局变量(表)的交集。在NT/2K中这个交集包括KiWaitInListHead.Flink, KiWaitOutListHead.Flink, KeTickCount.LowPart,在 XP中包括KiWaitListHead.Flink, KiWaitListHead.Blink, KeTickCount.LowPart。KeTickCount 也是输出的,所以我们轻易地将其从(刚才得到的)结果集中去除。如果余下的两个有邻接地址 (Flink/Blink),那么我们判断这是一个XP内核,得到的是KiWaitListHead。否则,我们发现的是 KiWaitInListHead和KiWaitOutListHead。我们并不关心发现的两个全局变量哪个是KiWaitInListHead 和KiWaitOutListHead。
第二, 我们查找KiDispatcherReadyListHead和KiReadySummary。在NT/2K中我们查找
KeSetAffinityThread和NtYieldExecution的共享全局变量,在XP中查找KeDelayExecutionThread 和NtYieldExecution的共享全局变量。(使用)这种方法,我们应该可以发现KiDispatcherReadyListHead和 KiReadySummary,但是我们不知道哪个是哪个。KiDispatcherReadyListHead是一个指针,所以说很难碰到由编译器生成"or KiDispatcherReadyListHead, <some>"(这样的指令)。但是由于SetMember宏,或操作(OR)对于KiReadySummary是很常见的。所以我们遍历在ntoskrnl中的所有绝对地址指针,并且检查修改两个特定全局变量的某种或(OR)指令。能够找到OR(指令)的全局变量是KiReadySummary,找不到的是KiDispatcherReadyListHead。
4.2.3. KeBalanceSetManager, KeSwapProcessOrStack
--------------------------------------------------------------------------------
这两个全局变量是作为PsCreateSystemThread的参数StartRoutine压入堆栈的,而MmInitSystem
又会调用PsCreateSystemThread函数。我们从ntoskrn入口点开始遍历代码段(迟早会到MnInitSystem 函数)。对于碰到的每一个"push"和"pop"指令,我们恰当地修正"esp"指向的虚地址并保存每个"push"参数。当碰到"call PsCreateSystemThread"时, 我们寻找堆栈跟踪缓存并查找第六个参数 - 这就是StartRoutine。我们并不关心对esp的加减操作,因为当它执行一个到stdcall函数的调用时,编译器并不产生这些指令。
在我们发现另一个StartRoutine后, 应该检查哪个才是真正的KeBalanceSetManager和KeSwapProcessOrStack。
我们创建一个由这个StartRoutine调用的函数表,并且检查在得到的调用表中是否包括对应于 KeBalanceSetManager 和 KeSwapProcessOrStack的“特征集”。
NT4 KeBalanceSetManager应该至少调用
- KeSetPriorityThread,
- KeInitializeTimer,
- KeSetTimer,
- KeWaitForMultipleObjects.
NT4 KeSwapProcessOrStack 应该至少调用
- MmQuerySystemSize,
- KeSetPriorityThread,
- KeWaitForSingleObject.
2k/XP KeBalanceSetManager 应该至少调用
- MmQuerySystemSize,
- KeSetPriorityThread,
- KeInitializeTimer,
- KeSetTimer,
- KeWaitForMultipleObjects.
2k/XP KeSwapProcessOrStack 应该至少调用
- KeSetPriorityThread,
- KeWaitForSingleObject.
这个检查用于彼此区分KeBalanceSetManager 和 KeSwapProcessOrStack 线程足够了。
4.2.4. KiStackInSwapListHead, KiProcess[In/Out]SwapListHead, KiSwapEvent,
KiStackOutSwapRequest
--------------------------------------------------------------------------------
现在我们查找KeBalanceSetManager和KeSwapProcessOrStack使用的全局变量。
KeSwapProcessOrStack使用这些全局变量:
- KiSwapEvent,
- KiStackOutSwapRequest,
- KiProcessOutSwapListHead,
- KiProcessInSwapListHead,
- KiStackInSwapListHead,
- KiStackProtectTime (只有NT4有 - 后来(这个全局变量)被移到了KeBalanceSetManager),
- KiSwappingThread (只有XP有).
KeBalanceSetManager 使用这些全局变量:
- MmWorkingSetManagerEvent,
- KiStackProtectTime (在NT4中的KeBalanceSetManager没有使用),
- KiStackOutSwapRequest,
- KiSwapEvent (在XP中的KeBalanceSetManager没有使用, 它替而调用KiSetSwapEvent).
更确切地说,KeStackOutSwapRequest总是共用的,我们可以发现它。现在我们应该
查找KiSwapEvent。注意KiSwapEvent是在KeSwapProcessOrStack使用的全局变量中唯一的 PKEVENT类型 - 我们将使用这一点。想法是遍历在ntoskrnl中的所有指针,找出那些像 PKEVENT的 - 它们应该有"KEVENT.Header.Size=4"的MOV指令。我们知道KiSwapEvent是一个同步事件(SynchronizationEvent)。也就是说,它由KeInitializeEvent宏来初始化, KeInitializeEvent宏随后包含一个mov指令:"KEVENT.Header.Type = SynchronizationEvent"。通过检查在KeSwqpProcessOrStack全局变量中这两个属性的存在,我们可以找到KiSwapEvent指针。
下一步,我们该查找三个Ki*ListHead指针。如果我们检查KeSwapProcessOrStack,
我们知道它在XP中使用的第一个全局变量就是KiSwappingThread,而在NT4中就是KiStackProtectTime。 2k的KeSwapProcessOrStack只有5个全局变量,其中的两个我们已经找到。如果内核是NT4或XP, 我们简单地忽略第一个使用的全局变量。剩下的我们将要查找的三个全局变量是: KiStackInSwapListHead 和 KiProcess[In/Out]SwapListHead。
最后要做的是在这三个指针中查找KeStackInSwapListHead。这是必须的,因为新的
调度程序的平衡集管理器(Balance Manage)对于进程列表和线程列表的操作是不同的。我们利用这样的一个事实,相对于KiInitSystem 和 KeSwapProcessOrStack 函数中使用全局变量KiProcess[In/Out]SwapListHead 和 KiStackInSwapListHead, KiStackInSwapListHead总是最后一个被处理的。如果我们查找所有使用这三个彼此非常接近的列表(List) 的代码段(只在内核的两处地方,一处在这些列表的初始化部分 - KiInitSystem,另一处在 KeSwapProcessOrStack的分支),我们选择最后一个,那个就是KiStackInSwapListHead。
4.2.5. KiReadyQueueIndex
--------------------------------------------------------------------------------
KiReadyQueueIndex只由KiScanReadyQueues使用,而KeBalanceSetManager又调用了
KiScanReadyQueues。所以,我们必须检查KeBalanceSetManager所有的子函数。对于它调用的每一个函数,我们创建一个它使用过的全局变量表。KiReadyQueueIndex使用这些全局变量:
- KiReadySummary,
- KeTickCount,
- KiReadyQueueIndex,
- KiDispatcherReadyListHead,
- 两个对于RtlAssert调用参数的数据偏移 (只存在于checked build版本中).
我们知道KeTickCount, KiReadySummary和KiDispatcherreadyListHead的指针并且
假设KiReadyQueueIndex没有被压入堆栈中(事实上这只是一个被Mov的值)。这已足够了: KiScanReadyQueue的候选者在Free Build版本下必须有4个全局变量,在Checked Build版本下有6个(其中两个必须被PUSH的), 其中3个已经被发现了。最后一个要核对就是KiReadyQueueIndex:它在内核映像(不是在内存中)的数据值应该为1。
4.2.6. PsActiveProcessHead
--------------------------------------------------------------------------------
这个地址对于一个新的调度器来说不是必须的,但我们不得不查找它,因为我们并
不打算仅针对klister进行隐藏。
我们将利用故障转储(crash dump)文件头格式在NT和XP没有变化的事实。转储文件头以
"PAGEDUMP"标志开头并且它们在文件+0x1C偏移处保存了PsActiveProcessHead的值。转储文件可以通过IoWriteCrashDump函数写。写缓存区应以这种方式准备:第一次IoWriteCrashDump调用用"PAGE"填充缓存区内存,接着"DUMP”被写入到+0x4偏移处。不久通过 "mov dword ptr [reg32+1Ch], offset _PsActiveProcessHead"指令将PsActiveProcessHead写到偏移+0x1C处。所以,我们的算法将会很简单:我们遍历在ntoskrnl中的所有段,去发现"PAGE"常量。接着我们检查附近是否有"mov [reg32+4], 'PMUD'"的指令存在。如果存在,我们在随后的100条指令中搜索"mov [reg32+1Ch], imm32"指令。如果发现这个imm32,我们通过遍历在内核映像中的所有直接指针来核对这是一个指针而不是一个数据常量。
4.3. 用法
--------------------------------------------------------------------------------
NTSTATUS ProcessHide(
IN ISPROCESSHIDDEN_CALLBACK IsProcessHidden
);
主引擎的入口。必须在PASSIVE_LEVEL条件下调用。参数指定一个用户的回调函数,回调函数
接收一个PEPROCESS 作为参数并决定 - 这个进程是否隐藏。但不要考虑隐藏System(#8)进程 :)
NTSTATUS ShutdownPhide( );
关闭函数。注意它不停止修补过的系统线程(KeBalanceSetManager 和 KeSwapProcessOrStack
线程)。所以,不会释放它们的代码段内存。
typedef BOOLEAN (__stdcall *ISPROCESSHIDDEN_CALLBACK)(
PEPROCESS Process
);
用户提供的做出主要决定的函数。
引擎会返回下面状态码中的一个。
STATUS_SUCCESS
一切正常,调度程序在线。
STATUS_ALREADY_STARTED
试图运行引擎两次。替而和你的回调函数的交互。
STATUS_UNSUPPORTED_OS
引擎不支持这个操作系统(2k3和以上的)
参见3.2. 说明了这些状态
STATUS_PASSIVE_LEVEL_REQUIRED
STATUS_NTOSKRNL_NOT_FOUND
STATUS_MAP_IMAGE_FAILED
STATUS_ADD_FUNCTION_FAILED
STATUS_COVERAGE_ERROR
STATUS_CODE_REBUILDING_FAILED
STATUS_UNSUCCESSFUL