Appcall类笔记
Appcall 简述
Appcall 可以帮助我们在调试对象的上下文(过程中)调用某些函数,这在一些情况下是十分有用的,比如说:
- 标识为加密函数的函数:加密/解密/散列函数
- 显式调用不那么常用的函数:与其等待程序调用某个函数,不如直接调用
- 改变程序逻辑:通过调用某些被调试函数,可以改变程序的逻辑和内部状态
- 扩展你的程序:因为 Appcall 可以在条件断点的条件表达式中使用,所以可以通过这种方式扩展应用程序
- Fuzzing 应用程序:在功能级别轻松地对程序进行模糊测试
- ...
Appcall 的工作原理
给定一个具有正确原型的函数,Appcall 机制的工作方式如下:
- 保存当前线程上下文
- 序列化参数(我们不为参数分配内存,我们使用被调试者的堆栈)
- 修改有问题的输入寄存器
- 将指令指针设置为要调用的函数的开头
- 调整返回地址,使其指向我们有断点的特殊区域(我们将其称为控制断点)
- 恢复程序并等待直到我们得到一个异常或控制断点(在上一步中插入)
- 反序列化回输入(仅适用于通过引用传递的参数)并保存返回值
在手动 Appcall 的情况下,调试器模块将完成除最后两个步骤之外的所有步骤,从而使我们有机会以交互方式调试相关功能。
Appcall 的使用
根据 Appcall 的工作原理,我们可以知道要使用 Appcall 模块,首先需要让 IDA 的调试器在运行中,即调试你目前正在逆向的程序。
之后便可以通过 Appcall 提供的 api 来做我们想做的事情。
需要注意,Appcall 类在
ida_idd
模块中,不过 ida python 默认 import 该模块。
class Appcall
Appcall.UTF16(s)
接收一个字符串 s
,将其转化为 UTF16 的字节流
(bytes
)
Appcall.array(type_name)
创建数组类型,但需要指定一个类型名字。
返回值将是一个 array
对象。
需要通过 pack()
和 unpack()
来对这个数组对象操作。
Appcall.buffer(str=None, size=0, fill='\x00')
创建一个字符串缓冲区,通常是作为可变字符串数组使用(引用传递)。
接受一个字符串 str
,一个缓冲区长度
size
,一个初始化元素 fill
。
如果不指定字符串,默认为空字符串;如果不指定缓冲区长度,默认字符串长度;如果不指定初始化填充元素,则默认为
\x00
。
返回值将是一个 byref
对象。
可以使用 byref.value
获取缓冲区内的字符串内容
(str
)。
可以使用 byref.size
获取缓冲区大小
(int
)。
Appcall.byref(val)
创建一个不可变对象的引用,目前只支持 int
和
str
的引用类型,通常是作为不可变字符串数组使用(非引用传递)。
可以使用 byref.value
获取缓冲区内的字符串内容
(int / str
)。
Appcall.cleanup_appcall(tid=0)
与 IDC 的 CleanupAppcall()
同理,作为还原上下文所用的函数。
Appcall.int64(val)
创建一个 int64
对象,通常作为传递参数时使用。
接收一个 int
初始值 val
。
Appcall.obj(**kwds)
创建一个空对象,或者存在关键字参数的属性的对象。
Appcall.proto(name_or_ea, proto_or_tinfo, flags=None)
将所需要的原型实例化为 appcall
(callable
对象)。
name_or_ea
→ 函数名字符串或者函数地址(函数名将使用LocByName()
解析)proto_or_tinfo
→ 函数原型字符串或者函数类型的tinfo_t
对象(可以使用get_tinfo()
函数获取)
失败时,无法解析原型或者地址将会抛出异常;
成功时将会返回 callable
对象。
Appcall.typeobj(typedecl_or_tinfo, ea=None)
返回一个类型的 Appcall 对象,可以传入 tinfo_t
对象来获取。
获取失败将会抛出 ValueError
异常。
Appcall.unicode(s)
接收一个字符串 s
,将其转化为 Unicode 的字节流
(bytes
)。
Appcall.valueof(name, default=0)
返回给定名称字符串 name
的数值。
如果名称无法解析,则返回默认值 default
。
Appcall.Consts
实例变量,使用 Appcall.Consts.CONST_NAME
来访问常量,CONST_NAME
因程序的设定变化。
Appcall.FUNCTION_NAME
实例变量,可以如此使用直接调用对应函数,例如存在函数
int f(int x) { return x + 5; }
我们可以通过 result = Appcall.f(5)
来直接调用该函数。
class Appcall_array__(type_name)
需要实例化的类,通过静态方法 Appcall.array()
获取。
array.pack(L)
将列表 (list
) 或者元组 (tuple
) 打包到
byref
缓冲区中。
array.unpack(buf, as_list=True)
将数组解包回列表或对象,即存入 buf
对象中。
as_list
如果为 False
则将数组作为元组解包。
class Appcall_callable__
使用自然语法发出 appcall
的助手类:
appcall.FunctionNameInTheDatabase(参数,....)
要么
appcall["Function@8"](参数, ...)
要么
f8 = appcall["Function@8"] f8(arg1, arg2, ...)
要么
o = appcall.obj() i = byref(5) appcall.funcname(arg1, i, "hello", o)
使用给定函数 ea
初始化 appcall
appcall.ea
实例变量,返回与此对象关联的函数地址 ea
appcall.fields
实例变量,返回字段名
appcall.size
实例变量,返回类型大小
appcall.tif
实例变量,返回 tinfo_t
对象
appcall.type
实例变量,返回类型字符串
class Appcall_consts__
Appcall.Consts
是该类的实例化,专门用于通过属性访问检索常量。
比如说存在常量 static const int Wow = 5;
那么我们可以通过 Appcall.Consts.Wow
来访问它。
主要参考:https://www.hex-rays.com/products/ida/support/idapython_docs/ida_idd.html
Appcall 使用实例
调用函数
比如说我们注意到存在一个解密函数:
我们可以直接使用 IDA Python 调用该函数直接解密:
1 | SizeOfBuffer = 255 |
注入库
要在调试对象中注入库,只需 Appcall LoadLibrary()
:
1 | loadlib = Appcall.proto("kernel32_LoadLibraryA", "int __stdcall loadlib(const char *fn);") |
设置或获取最后一个错误
要检索最后一个错误值,我们可以从 TIB 手动解析它,也可以调用 GetLastError() API:
1 | getlasterror = Appcall.proto("kernel32_GetLastError", "DWORD __stdcall GetLastError();") |
同样,我们可以做同样的事情来设置最后一个错误代码值:
1 | setlasterror = Appcall.proto("kernel32_SetLastError", "void __stdcall SetLastError(int dwErrCode);") |
检索命令行值
要检索程序的命令行,我们可以从 PEB 解析它,也可以调用 GetCommandLineA() API:
1 | getcmdline = Appcall.proto("kernel32_GetCommandLineA", "const char *__stdcall getcmdline();") |
设置/重置事件
有时,被调试的程序在等待信号量或事件时可能会死锁。您可以手动释放信号量或发出事件信号。 也可以杀死线程:
1 | releaseem = Appcall.proto("kernel32_ReleaseSemaphore", |
更改被调试者的虚拟内存配置
可以更改内存页的保护。在下面的示例中,我们将 PE 头页保护更改为执行/读/写(通常是只读的):
1 | virtprot = Appcall.proto("kernel32_VirtualProtect", |
如果你需要分配一个新的内存页:
1 | virtalloc = Appcall.proto("kernel32_VirtualAlloc", |
加载库并调用导出的函数
使用 Appcall 还可以加载库、解析函数地址并调用它。让我们用一个例子来说明:
1 | def get_appdata(): |