Appcall类笔记

Appcall 简述

Appcall 可以帮助我们在调试对象的上下文(过程中)调用某些函数,这在一些情况下是十分有用的,比如说:

  • 标识为加密函数的函数:加密/解密/散列函数
  • 显式调用不那么常用的函数:与其等待程序调用某个函数,不如直接调用
  • 改变程序逻辑:通过调用某些被调试函数,可以改变程序的逻辑和内部状态
  • 扩展你的程序:因为 Appcall 可以在条件断点的条件表达式中使用,所以可以通过这种方式扩展应用程序
  • Fuzzing 应用程序:在功能级别轻松地对程序进行模糊测试
  • ...

Appcall 的工作原理

给定一个具有正确原型的函数,Appcall 机制的工作方式如下:

  1. 保存当前线程上下文
  2. 序列化参数(我们不为参数分配内存,我们使用被调试者的堆栈)
  3. 修改有问题的输入寄存器
  4. 将指令指针设置为要调用的函数的开头
  5. 调整返回地址,使其指向我们有断点的特殊区域(我们将其称为控制断点
  6. 恢复程序并等待直到我们得到一个异常或控制断点(在上一步中插入)
  7. 反序列化回输入(仅适用于通过引用传递的参数)并保存返回值

在手动 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)

创建一个不可变对象的引用,目前只支持 intstr 的引用类型,通常是作为不可变字符串数组使用(非引用传递)。

可以使用 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
2
3
4
5
6
7
8
9
SizeOfBuffer = 255
# 显式创建缓冲区为 byref 对象
s_in = Appcall.byref("SomeEncryptedBuffer")
# 缓冲区总是由引用返回
s_out = Appcall.buffer("", SizeOfBuffer)
# 调用被调试对象
Appcall.decrypt_buffer(s_in, s_out, SizeOfBuffer)
# 打印结果
print("decrypted=", s_out.value)

注入库

要在调试对象中注入库,只需 Appcall LoadLibrary()

1
2
loadlib = Appcall.proto("kernel32_LoadLibraryA", "int __stdcall loadlib(const char *fn);") 
hmod = loadlib("dll_to_inject.dll")

设置或获取最后一个错误

要检索最后一个错误值,我们可以从 TIB 手动解析它,也可以调用 GetLastError() API:

1
2
getlasterror = Appcall.proto("kernel32_GetLastError", "DWORD __stdcall GetLastError();") 
print("lasterror=", getlasterror())

同样,我们可以做同样的事情来设置最后一个错误代码值:

1
2
setlasterror = Appcall.proto("kernel32_SetLastError", "void __stdcall SetLastError(int dwErrCode);") 
setlasterror(5)

检索命令行值

要检索程序的命令行,我们可以从 PEB 解析它,也可以调用 GetCommandLineA() API:

1
2
getcmdline = Appcall.proto("kernel32_GetCommandLineA", "const char *__stdcall getcmdline();")
print("command line:", getcmdline())

设置/重置事件

有时,被调试的程序在等待信号量或事件时可能会死锁。您可以手动释放信号量或发出事件信号。 也可以杀死线程:

1
2
3
4
5
6
releaseem = Appcall.proto("kernel32_ReleaseSemaphore", 
"BOOL __stdcall ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);")
resetevent = Appcall.proto("kernel32_SetEvent",
"BOOL __stdcall SetEvent(HANDLE hEvent);")
termthread = Appcall.proto("kernel32_TerminateThread",
"BOOL __stdcall TerminateThread(HANDLE hThread, DWORD dwExitCode);")

更改被调试者的虚拟内存配置

可以更改内存页的保护。在下面的示例中,我们将 PE 头页保护更改为执行/读/写(通常是只读的):

1
2
3
4
5
virtprot = Appcall.proto("kernel32_VirtualProtect",
"BOOL __stdcall VirtualProtect(LPVOID addr, DWORD sz, DWORD newprot, PDWORD oldprot);")
r = virtprot(0x400000, 0x1000, Appcall.Consts.PAGE_EXECUTE_READWRITE, Appcall.byref(0));
print("VirtualProtect returned:", r)
RefreshDebuggerMemory()

如果你需要分配一个新的内存页:

1
2
3
4
virtalloc = Appcall.proto("kernel32_VirtualAlloc",
"int __stdcall VirtualAlloc(int addr, SIZE_T sz, DWORD alloctype, DWORD protect);")
m = virtualalloc(0, Appcall.Consts.MEM_COMMIT, 0x1000, Appcall.Consts.PAGE_EXECUTE_READWRITE)
RefreshDebuggerMemory()

加载库并调用导出的函数

使用 Appcall 还可以加载库、解析函数地址并调用它。让我们用一个例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def get_appdata():
hshell32 = loadlib("shell32.dll")
if hshell32 == 0:
print "failed to load shell32.dll"
return False
print "%x: shell32 loaded" % hshell32
# 确保刷新调试器内存加载新库后
RefreshDebuggerMemory()
# 解析函数地址
p = getprocaddr(hshell32, "SHGetSpecialFolderPathA")
if p == 0:
print "shell32.SHGetSpecialFolderPathA() not found!"
return False
# 创建原型
shgetspecialfolder = Appcall.proto(p,
"BOOL SHGetSpecialFolderPath(HWND hwndOwner, LPSTR lpszPath, int nFolder, BOOL fCreate);")
print "%x: SHGetSpecialFolderPath() resolved..."
# 创建缓冲区
buf = Appcall.buffer("\x00" * 260)
# CSIDL_APPDATA = 0x1A
if not shgetspecialfolder(0, buf, 0x1A, 0):
print "SHGetSpecialFolderPath() failed!"
else:
print "AppData Path: >%s<" % Appcall.cstr(buf.value)
return True