Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

AWD Pwn 修复工具

赶在国赛前几天写的一个通防脚本, 附赠一个变量膨胀自动扩展栈的功能, 但是遇到了最臭不可闻的 AWDP 大人, 拼尽全力服务异常

但是还是记录一下, 学到许多 IDApython 的神奇API

IDApython SDK version: 9.1

Requirement: keystone-engine, capstone

实现通防

思路

思路其实很简单, 就是通过安装沙箱的方式直接禁止掉常用的 get shell 和读文件的系统调用即可

由于通常没有太多无用的可执行段来放安装沙箱的代码, 所以选择inline hook从_start开始控制流必经的第一个大小足够进行inline hook的函数进行 hook 跳转到安装沙箱的代码, 安装完沙箱后恢复被 hook 函数的字节并将其所在页访问权限设置成r-x

找到第一个可用函数来安装hook

思路是从_start开始通过遍历引用自这个函数的函数地址来得到下一个要访问的函数达到 DFS 的效果, 直到找到第一个可用函数, 当然这个方案有很多问题, 这个方法得到的函数只存在可能的调用链而不是一定会被调用的, 这个问题可以用Unicorn模拟执行来缓解, 还需要在Unicorn中设置所有函数的返回值来得到更真实全面的控制流, 但是这样得到的控制流也没有用, 无法保证要的函数一定会被执行, 所以我选择最省事的让用户自己输入要 hook 的函数(

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
26
27
28
29
30
31
32
33
34
35
36
37
38
def get_first_good_func() -> int | None:
"""
Find the first function with size greater than `minimal_size` reachable from _start.
"""
start_ea = idc.get_name_ea_simple('_start')
if start_ea == idc.BADADDR or start_ea is None:
return None

start_func = get_func(start_ea)
if not start_func:
start_func = get_func(start_ea)
if not start_func:
return None

visited = set()
q = [start_func.start_ea]

while q:
f_ea = q.pop()
if f_ea in visited:
continue
visited.add(f_ea)

func = get_func(f_ea)
if not func:
continue

size = func.end_ea - func.start_ea
if size > minimal_size:
return func.start_ea

for insn_ea in idautils.FuncItems(func.start_ea):
for xr in idautils.XrefsFrom(insn_ea):
tgt = xr.to
tgt_func = get_func(tgt)
if tgt_func and tgt_func.start_ea not in visited:
q.append(tgt_func.start_ea)
return None

安装hook

安装在目标函数头部的inline hook主要的功能是将自身所在页到安装沙箱的代码所在页的属性都设置为rwx来让沙箱代码有执行权限, 这一步也可以通过修改ELF节表头中某个段的属性来实现, 但是有些比赛查ELF头部信息, 所以还是用mprotect实现

安装这个hook 的一个难点是在开启PIE的情况下的重定位问题, jmp/call near这类使用偏移量来跳转的指令还可以不用修改, 但是对于访问自己相对位置内存的指令就无法硬编码指令, 这些指令在x86_64下可以使用[rip + offset]来获得目标地址, i386下则需要使用call $+5;pop reg的方式获取自身地址, 最终选定的inline hook汇编:

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
CALLTHIS = bytes.fromhex('E8 00 00 00 00')    # call $+5
inline_hook_asm = [
'lea rdi, [rip]',
'and rdi, -0x1000',
'push rdi',
'xor rdx, rdx',
'mov dl, 7',
f'mov rsi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'xor rax, rax',
'mov al, 0xa',
'syscall', # mprotect(hook_addr & -0x1000, length, PROT_READ | PROT_WRITE | PROT_EXEC)
f'jmp {hook_addr:#x}'
] if is64 else [
CALLTHIS,
'pop ebx',
'and ebx, -0x1000',
'push ebx',
f'mov ecx, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'xor edx, edx',
'mov dl, 7',
'xor eax, eax',
'mov al, 0x7d',
'int 0x80', # mprotect(hook_addr & -0x1000, length, PROT_READ | PROT_WRITE | PROT_EXEC)
f'jmp {hook_addr:#x}'
]

这套hook 汇编的另一个问题是会破坏几个寄存器储存的值, 可以先把要保留的上下文都都push到栈上后续跳回时再pop回

而安装沙箱的目标代码原本决定放在.eh_frame中但是发现这个段不够长, 于是默认选择的是.eh_frame_hdr.eh_frame结尾

安装沙箱

安装沙箱的两条代码为prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog), 对应的汇编:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
install_sandbox_asm = [
'xor r8d, r8d',
'xor r10d, r10d',
'xor edx, edx',
'mov esi, 1',
'mov edi, 0x26',
'mov eax, 0x9d',
'syscall', # prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
'lea rdx, [rip + 0x57]', # &filters
'mov qword ptr [rdx - 8], rdx',
'sub rdx, 0x10',
'mov esi, 2',
'mov edi, 0x16',
'mov eax, 0x9d',
'syscall', # prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)
f'lea rdi, [rip - {hook_addr - to_hook + 0x40:#x}]',
f'lea rsi, [rip + 0x98]',
f'mov ecx, {len(inline_hook_bytes):#x}',
'rep movsb', # recover the original bytes before the hook
'pop rdi',
f'mov rsi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'mov edx, 5',
'mov eax, 0xa',
'syscall', # mprotect(to_hook & -0x1000, length, PROT_READ | PROT_EXEC)
f'jmp {to_hook:#x}',
] if is64 else [
'xor ebx, ebx',
'xor ecx, ecx',
'xor edx, edx',
'xor esi, esi',
'xor edi, edi',
'mov bl, 0x26',
'mov cl, 1',
'mov eax, 0xac',
'int 0x80', # prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
CALLTHIS,
'pop edx',
'lea edx, [edx + 0x57]', # &filters
'mov dword ptr [edx - 4], edx',
'sub edx, 0x8',
'xor ebx, ebx',
'mov bl, 0x16',
'xor ecx, ecx',
'mov cl, 2',
'mov eax, 0xac',
'int 0x80', # prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)
CALLTHIS,
'pop edi',
f'lea edi, [edi - {hook_addr - to_hook + 0x38:#x}]',
CALLTHIS,
'pop esi',
f'lea esi, [esi + 0x8d]',
f'mov ecx, {len(inline_hook_bytes):#x}',
'rep movsb', # recover the original bytes before the hook
'pop ebx',
f'mov esi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'mov edx, 5',
'mov eax, 0x7d',
'int 0x80', # mprotect(to_hook & -0x1000, length, PROT_READ | PROT_EXEC)
f'jmp {to_hook:#x}'
]

安装完沙箱后就会把被 hook 的函数复原并复原页访问权限, 这里把几个访问的参数(待复原的函数地址, 备份的被替换字节)偏移硬编码进了汇编里, 剩下的会通过算式计算, 这导致了这段汇编本身和沙箱规则都不能被随意修改, 有意者可以自己调整偏移算法达到完美自适应让这些代码和下面的沙箱规则都可以修改

沙箱规则的选择:

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
26
27
28
29
30
31
32
amd64 sandbox:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x07 0x00 0x0000003b if (A == execve) goto 0012
0005: 0x15 0x06 0x00 0x00000142 if (A == execveat) goto 0012
0006: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0012
0007: 0x15 0x04 0x00 0x00000101 if (A == openat) goto 0012
0008: 0x15 0x03 0x00 0x00000029 if (A == socket) goto 0012
0009: 0x15 0x02 0x00 0x0000002a if (A == connect) goto 0012
0010: 0x15 0x01 0x00 0x00000065 if (A == ptrace) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
#################################################################
i386 sandbox:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0x40000003 if (A == ARCH_I386) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x06 0x00 0x0000000b if (A == execve) goto 0011
0005: 0x15 0x05 0x00 0x00000166 if (A == execveat) goto 0011
0006: 0x15 0x04 0x00 0x00000005 if (A == open) goto 0011
0007: 0x15 0x03 0x00 0x00000127 if (A == openat) goto 0011
0008: 0x15 0x02 0x00 0x00000066 if (A == socketcall) goto 0011
0009: 0x15 0x01 0x00 0x0000001a if (A == ptrace) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS

完整功能:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def install_sandbox(to_hook: int = None, hook_addr: int = eh_frame, max_size: int = eh_frame_size) -> bool:
"""
Install sandbox by inline hook the function at `to_hook` (or the first suitable function if `to_hook` is None) to the compact sandbox code at `hook_addr`.Temporily support only AMD64 architecture.
"""
if to_hook is None:
to_hook = get_first_good_func()
if to_hook is None:
print('[-] No suitable function found to hook.')
return False

CALLTHIS = bytes.fromhex('E8 00 00 00 00') # call $+5
inline_hook_asm = [
'lea rdi, [rip]',
'and rdi, -0x1000',
'push rdi',
'xor rdx, rdx',
'mov dl, 7',
f'mov rsi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'xor rax, rax',
'mov al, 0xa',
'syscall', # mprotect(hook_addr & -0x1000, length, PROT_READ | PROT_WRITE | PROT_EXEC)
f'jmp {hook_addr:#x}'
] if is64 else [
CALLTHIS,
'pop ebx',
'and ebx, -0x1000',
'push ebx',
f'mov ecx, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'xor edx, edx',
'mov dl, 7',
'xor eax, eax',
'mov al, 0x7d',
'int 0x80', # mprotect(hook_addr & -0x1000, length, PROT_READ | PROT_WRITE | PROT_EXEC)
f'jmp {hook_addr:#x}'
]

filters_bytes = bytes([0x20, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x15, 0x0, 0x1, 0x0, 0x3E, 0x0, 0x0, 0xC0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x15, 0x0, 0x7, 0x0, 0x3B, 0x0, 0x0, 0x0, 0x15, 0x0, 0x6, 0x0, 0x42, 0x1, 0x0, 0x0, 0x15, 0x0, 0x5, 0x0, 0x2, 0x0, 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x1, 0x1, 0x0, 0x0, 0x15, 0x0, 0x3, 0x0, 0x29, 0x0, 0x0, 0x0, 0x15, 0x0, 0x2, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x15, 0x0, 0x1, 0x0, 0x65, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x7F, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80]) if is64 else bytes([0x20, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x15, 0x0, 0x1, 0x0, 0x3, 0x0, 0x0, 0x40, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x15, 0x0, 0x6, 0x0, 0xB, 0x0, 0x0, 0x0, 0x15, 0x0, 0x5, 0x0, 0x66, 0x1, 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x5, 0x0, 0x0, 0x0, 0x15, 0x0, 0x3, 0x0, 0x27, 0x1, 0x0, 0x0, 0x15, 0x0, 0x2, 0x0, 0x66, 0x0, 0x0, 0x0, 0x15, 0x0, 0x1, 0x0, 0x1A, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x7F, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80])
"""
amd64 sandbox:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x07 0x00 0x0000003b if (A == execve) goto 0012
0005: 0x15 0x06 0x00 0x00000142 if (A == execveat) goto 0012
0006: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0012
0007: 0x15 0x04 0x00 0x00000101 if (A == openat) goto 0012
0008: 0x15 0x03 0x00 0x00000029 if (A == socket) goto 0012
0009: 0x15 0x02 0x00 0x0000002a if (A == connect) goto 0012
0010: 0x15 0x01 0x00 0x00000065 if (A == ptrace) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
#################################################################
i386 sandbox:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0x40000003 if (A == ARCH_I386) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x06 0x00 0x0000000b if (A == execve) goto 0011
0005: 0x15 0x05 0x00 0x00000166 if (A == execveat) goto 0011
0006: 0x15 0x04 0x00 0x00000005 if (A == open) goto 0011
0007: 0x15 0x03 0x00 0x00000127 if (A == openat) goto 0011
0008: 0x15 0x02 0x00 0x00000066 if (A == socketcall) goto 0011
0009: 0x15 0x01 0x00 0x0000001a if (A == ptrace) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
"""
prog = (len(filters_bytes) // 8).to_bytes(8, 'little').ljust(16, b'\xCC') if is64 else (len(filters_bytes) // 8).to_bytes(4, 'little').ljust(8, b'\xCC')

inline_hook_bytes = b''
for asm in inline_hook_asm:
try:
inline_hook_bytes += asm if type(asm) is bytes else ks.asm(asm, addr=to_hook + len(inline_hook_bytes), as_bytes=True)[0]
except Exception as e:
print(f'[-] Error occurred while assembling instruction: {asm}')
print(f'[-] Error: {e}')
assert len(inline_hook_bytes) <= minimal_size, 'Inline hook code exceeds the minimal size limit.'
old_bytes = get_bytes(to_hook, len(inline_hook_bytes))

install_sandbox_asm = [
'xor r8d, r8d',
'xor r10d, r10d',
'xor edx, edx',
'mov esi, 1',
'mov edi, 0x26',
'mov eax, 0x9d',
'syscall', # prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
'lea rdx, [rip + 0x57]', # &filters
'mov qword ptr [rdx - 8], rdx',
'sub rdx, 0x10',
'mov esi, 2',
'mov edi, 0x16',
'mov eax, 0x9d',
'syscall', # prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)
f'lea rdi, [rip - {hook_addr - to_hook + 0x40:#x}]',
f'lea rsi, [rip + 0x98]',
f'mov ecx, {len(inline_hook_bytes):#x}',
'rep movsb', # recover the original bytes before the hook
'pop rdi',
f'mov rsi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'mov edx, 5',
'mov eax, 0xa',
'syscall', # mprotect(to_hook & -0x1000, length, PROT_READ | PROT_EXEC)
f'jmp {to_hook:#x}',
] if is64 else [
'xor ebx, ebx',
'xor ecx, ecx',
'xor edx, edx',
'xor esi, esi',
'xor edi, edi',
'mov bl, 0x26',
'mov cl, 1',
'mov eax, 0xac',
'int 0x80', # prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
CALLTHIS,
'pop edx',
'lea edx, [edx + 0x57]', # &filters
'mov dword ptr [edx - 4], edx',
'sub edx, 0x8',
'xor ebx, ebx',
'mov bl, 0x16',
'xor ecx, ecx',
'mov cl, 2',
'mov eax, 0xac',
'int 0x80', # prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)
CALLTHIS,
'pop edi',
f'lea edi, [edi - {hook_addr - to_hook + 0x38:#x}]',
CALLTHIS,
'pop esi',
f'lea esi, [esi + 0x8d]',
f'mov ecx, {len(inline_hook_bytes):#x}',
'rep movsb', # recover the original bytes before the hook
'pop ebx',
f'mov esi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'mov edx, 5',
'mov eax, 0x7d',
'int 0x80', # mprotect(to_hook & -0x1000, length, PROT_READ | PROT_EXEC)
f'jmp {to_hook:#x}'
]

install_sandbox_bytes = b''
for asm in install_sandbox_asm:
try:
install_sandbox_bytes += asm if type(asm) is bytes else ks.asm(asm, addr=hook_addr + len(install_sandbox_bytes), as_bytes=True)[0]
except Exception as e:
print(f'[-] Error occurred while assembling instruction: {asm}')
print(f'[-] Error: {e}')
install_sandbox_bytes += prog + filters_bytes
install_sandbox_bytes += old_bytes
assert len(install_sandbox_bytes) <= max_size, f'Sandbox code({len(install_sandbox_bytes)} bytes) exceeds the max_size({max_size} bytes).'
patch_bytes(to_hook, inline_hook_bytes)
patch_bytes(hook_addr, install_sandbox_bytes)
return True

实现变量尺寸修改

思路

在伪代码界面选择一个变量后调用该功能时获取该变量的信息, 如果是栈变量的话就能进行修改了, 修改的用户接口和IDA 自带的修改类型相同, 填写一个类型后计算表达式类型的字节长度, 如果大于原本的长度就会进行栈扩展, 通过遍历函数中每条指令对sp的修改来发现函数栈帧的初始化和释放代码, 申请指令通常是sub sp, xxx, 而释放有可能是add sp, xxxleave等, 直接将解析出的新变量长度加在当前栈帧的大小上, 之前的所占的栈帧范围全部废弃只用于padding. 然后就是找到选中变量范围内被函数的哪条指令引用过, 将这些指令访问的栈帧地址偏移到新栈帧多申请出来的那块范围内.

获取变量信息

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
26
27
28
29
30
31
32
33
34
35
def _get_selected_stack_lvar_info() -> tuple[int, ida_hexrays.lvar_t] | None:
"""Return (func_ea, lvar) for current pseudocode selection, or None."""
if not ida_hexrays.init_hexrays_plugin():
return None

w = ida_kernwin.get_current_widget()
if not w or ida_kernwin.get_widget_type(w) != ida_kernwin.BWN_PSEUDOCODE:
return None

vu: ida_hexrays.vdui_t = ida_hexrays.get_widget_vdui(w)
if not vu:
return None

if not vu.get_current_item(ida_hexrays.USE_KEYBOARD):
return None

it = vu.item
lvar: ida_hexrays.lvar_t = None
if it.citype == ida_hexrays.VDI_LVAR:
if hasattr(it, 'l'):
lvar = it.l
elif hasattr(it, 'get_lvar'):
lvar = it.get_lvar()
elif it.citype == ida_hexrays.VDI_EXPR and it.e.op == ida_hexrays.cot_var:
idx = it.e.v.idx
lvar = vu.cfunc.lvars[idx]

if not lvar:
return None

# Ensure it is a stack variable, not a register temporary.
if not lvar.is_stk_var() or lvar.is_reg_var():
return None

return vu.cfunc.entry_ea, lvar

首先要检测当前是否位于伪代码界面, 然后通过表达式(ida_hexrays.VDI_EXPR)中的变量(ida_hexrays.cot_var)或者本地变量的定义(ida_hexrays.VDI_LVAR)来返回本地变量的描述对象, 对于我们要实现的目标其中一个很重要的成员就是变量的栈偏移(lvar.get_stkoff())

获取新的变量类型信息

最大化复用IDA 自带的类型解析器, 不需要自己写解析器也能自动使用IDA 的Local Type类型库解析类型表达式:

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
26
27
28
29
30
info = _get_selected_stack_lvar_info()
if info is None:
ida_kernwin.warning('Please select a stack local variable in pseudocode view first.')
return False

func_ea, lvar = info
stkoff = lvar.get_stkoff()
old_size = lvar.type().get_size()
default_decl = f'{lvar.type()}'
decl = ida_kernwin.ask_str(default_decl, 0, 'Input target type declaration')
if not decl:
return False

decl = decl.strip()
if not decl:
ida_kernwin.warning('Empty type declaration.')
return False

# Reuse IDA built-in declaration parser/type engine.
lvar_tif = ida_typeinf.tinfo_t()
ida_typeinf.parse_decl(lvar_tif, ida_typeinf.get_idati(), f'{decl};', ida_typeinf.PT_TYP)

new_size = lvar_tif.get_size()
if new_size is None or new_size <= 0:
ida_kernwin.warning(f'IDA cannot determine a concrete size for type: {decl}')
return False

if new_size - old_size <= 0:
ida_kernwin.warning(f'New type size ({new_size} bytes) is not greater than the original type size ({old_size} bytes).\nUse "Change Variable Type" action if you just want to change the variable type without expanding the stack frame.')
return False

栈扩展

像上面获取类型信息一样, IDA9 之后很多获取信息的API都需要迁移, 基本上都要类似解析指令一样先创建一个类型对象ida_typeinf.tinfo_t() 然后用各个模块的信息获取函数来存入信息, 包括接下来的栈帧信息.

扩展之前先获取当前栈帧的大小, 减去当前框架下 2 个内存单元(存放栈基指针和返回地址)就是函数的本地变量空间大小, 后续需要用来计算变量实际相对栈基指针的偏移(stkoff是相对于栈顶而非栈底的)和新的栈帧大小.

栈扩展的核心思路是使用ida_frame.get_sp_delta(func_ea, ea)获取函数内某条指令对 sp 的影响, 寻找分配和回收指令替换, 考虑到i386框架调用约定, 有可能在调用其他函数时调整esp值, 所以需要在找到分配指令时记录分配大小在接下来寻找回收指令时匹配这个大小

实现代码:

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
26
27
28
29
30
31
32
def _expand_stack_frame(func_ea: int, size_delta: int) -> bool:
"""Expand the stack frame of the function at `func_ea` by `size_delta` bytes. This function should handle updating the function's stack frame size and adjusting all relevant instructions (e.g. those that reference the stack variable) accordingly."""
func_end = get_func(func_ea).end_ea
now_ea = func_ea
now_size = 0
allocated = None

while now_ea <= func_end:
sp_delta = ida_frame.get_sp_delta(func_ea, now_ea)
if sp_delta != 0:
sus_ea = now_ea - now_size
insn = ida_ua.insn_t()
ida_ua.decode_insn(insn, sus_ea)
mnem = insn.get_canon_mnem()
op0 = idc.print_operand(sus_ea, 0)
op1 = idc.print_operand(sus_ea, 1)
if ('sub' in mnem and 'sp' in op0) or \
(allocated and 'add' in mnem and 'sp' in op0 and insn.ops[1].value == allocated):
allocated = insn.ops[1].value
new_frame_size = allocated + size_delta
new_insn_asm = f'{mnem} {op0}, {new_frame_size:#x}'
new_insn_bytes = ks.asm(new_insn_asm, addr=sus_ea, as_bytes=True)[0]
old_insn_bytes = insn_bytes_at(sus_ea)
if len(new_insn_bytes) != len(old_insn_bytes):
print(f'[-] Failed to patch stack frame instruction at {sus_ea:#x}: new instruction size {len(new_insn_bytes)} does not match old instruction size {len(old_insn_bytes)}.')
return False
patch_bytes(sus_ea, new_insn_bytes)

now_size = idc.get_item_size(now_ea)
now_ea += now_size

return True

这个方法的一个痛点是原本申请栈空间的指令大概率是sub rsp, imm8, 长度为 4 , 而修改变量后的申请栈空间的指令可能会变成sub rsp, imm32, 长度为 7 , 目前没想到很好的解决方案, 只能想到通过inline hook来解决

变量访问调整

上面说过修改变大的变量会被放在新分配的栈顶, 这样可以防止修改其他变量的访问, 调整变量前要先获取汇编层面所有对该变量的引用, 这点可以通过前面获取过的stkoff来实现, 遍历栈中的变量, 找到stkoff为目标变量到下一个变量之间的栈帧空间, 然后通过ida_frame.build_stkvar_xrefs()来获取函数对该范围内内存的所有引用, 然后调整[bp + offset]中的偏移来达到变量迁移的目标:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def _get_lvar_range(func_ea: int, stkoff: int) -> tuple[int, int] | None:
"""Implementation for getting the range of a local variable in the stack frame."""
frame_tif = ida_typeinf.tinfo_t()
if not ida_frame.get_func_frame(frame_tif, func_ea):
return None

frame_udt = ida_typeinf.udt_type_data_t()
if not frame_tif.get_udt_details(frame_udt):
return None

var_start, var_end = stkoff, None
for udm in frame_udt:
udm: ida_typeinf.udm_t
if udm.begin() // 8 > stkoff:
var_end = udm.begin() // 8
break

if var_end is None:
return None
return var_start, var_end

def _get_selected_lvar_xrefs() -> ida_frame.xreflist_t | None:
info = _get_selected_stack_lvar_info()
if info is None:
return None

func_ea, lvar = info
stkoff = lvar.get_stkoff()
xreflist = ida_frame.xreflist_t()
var_start, var_end = _get_lvar_range(func_ea, stkoff)

if None in (var_start, var_end):
return None

ida_frame.build_stkvar_xrefs(xreflist, func_ea, var_start, var_end)
return xreflist

...
xreflist = _get_selected_lvar_xrefs()
if xreflist is None:
ida_kernwin.warning('Failed to get stack variable references. The variable type has been changed, but some references may not be updated.')
return False

for i, xref in enumerate(xreflist):
xref_ea = xref.ea
old_insn_bytes = insn_bytes_at(xref_ea)
old_insn_asm = next(cs.disasm(old_insn_bytes, xref_ea))
op = old_insn_asm.op_str
bp_bias_str = op.split('-')[-1].split(']')[0].strip()
bp_bias = int(bp_bias_str, 16)
head_bias = old_frame_size - stkoff
op = op.replace(bp_bias_str, f'{new_frame_size - (head_bias - bp_bias):#x}')
new_insn_asm = f'{old_insn_asm.mnemonic} {op}'
print(f'Patching instruction at {xref_ea:#x}: {old_insn_asm.mnemonic} {old_insn_asm.op_str} -> {new_insn_asm}')
new_insn_bytes = ks.asm(new_insn_asm, addr=xref_ea, as_bytes=True)[0]
patch_bytes(xref_ea, new_insn_bytes)

函数迁移(废弃功能)

本来是用来替换inline hook的方案, 后面发现直接将整个函数移动到其他地方占据了太大的空间, 遂将这个功能废弃转用inline hook

函数迁移的核心就是修正指令中的RIP相关内存访问时的偏移:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def build_fixed_asm(insn: ida_ua.insn_t, old_ea: int, new_ea: int, func_range: tuple[int, int]) -> bytes | None:
"""
Build the fixed instruction bytes for the instructions within the function being pivoted. This function handles control flow instructions and IP-relative data references.
"""
mnem = insn.get_canon_mnem()
old_insn_bytes = insn_bytes_at(old_ea)
if old_insn_bytes is None:
return None

# fix control flow instructions
if insn.ops[0].type in (ida_ua.o_near, ida_ua.o_far):
target_ea = insn.ops[0].addr
if func_range[0] <= target_ea <= func_range[1]:
# prevent intra-function control flow instructions from being modified
return old_insn_bytes
return ks.asm(f'{mnem} {target_ea:#x}', addr=new_ea, as_bytes=True)[0]

# fix IP related data references (e.g. mov reg, cs:...)
raw_op_str = next(cs.disasm(old_insn_bytes, old_ea)).op_str
if 'ip' in raw_op_str:
old_offset = int(raw_op_str.split('[')[1].split(']')[0].split('+')[-1].strip(), 16)
new_offset = old_offset + (old_ea - new_ea)
new_op_str = raw_op_str.replace(f'{old_offset:#x}', f'{new_offset:#x}')
# print(f'Fixing IP-relative operand:{old_ea:012x}: {raw_op_str} ->{new_ea:012x} {new_op_str}')
return ks.asm(f'{mnem} {new_op_str}', addr=new_ea, as_bytes=True)[0]

return old_insn_bytes

def func_pivot(func_ea : int, pivot_ea : int):
"""
Migrate the function at `func_ea` to `pivot_ea` by patching the instructions at `pivot_ea` with the fixed instruction bytes from the original function. The function handles control flow instructions and IP-relative data references to ensure the pivoted function works correctly at its new location.
"""
bias = pivot_ea - func_ea
func = get_func(func_ea)
funcrng = (func.start_ea, func.end_ea)
counter = 0
while counter <= func.size():
insn = ida_ua.insn_t()
length = ida_ua.decode_insn(insn, func_ea + counter)

if length == 0:
continue

fixed_asm = build_fixed_asm(insn, func_ea + counter, pivot_ea + counter, funcrng)
patch_bytes(pivot_ea + counter, fixed_asm)
counter += length

因为是废弃功能所以没做 32 位框架的功能, 只能对 64 位框架函数进行迁移

完整插件

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
from keystone import Ks, KS_ARCH_X86, KS_MODE_32, KS_MODE_64
from capstone import Cs, CS_ARCH_X86, CS_MODE_32, CS_MODE_64

from ida_segment import get_segm_by_name
from ida_ida import inf_is_64bit
from ida_funcs import get_func, add_func, del_func, reanalyze_function
from ida_bytes import get_bytes, patch_bytes
from ida_auto import plan_and_wait
import idc
import idautils
import ida_idaapi
import ida_ua
import ida_kernwin
import ida_typeinf
import ida_frame
import ida_hexrays

is64 = inf_is_64bit()
minimal_size = 0x40
eh_frame = get_segm_by_name('.eh_frame_hdr').start_ea
eh_frame_size = get_segm_by_name('.eh_frame').end_ea - eh_frame
ks = Ks(KS_ARCH_X86, KS_MODE_64 if is64 else KS_MODE_32)
cs = Cs(CS_ARCH_X86, CS_MODE_64 if is64 else CS_MODE_32)

def insn_bytes_at(ea: int) -> bytes | None:
insn = ida_ua.insn_t()
length = ida_ua.decode_insn(insn, ea)
if length == 0:
return None
return get_bytes(ea, length)

def get_first_good_func() -> int | None:
"""
Find the first function with size greater than `minimal_size` reachable from _start.
"""
start_ea = idc.get_name_ea_simple('_start')
if start_ea == idc.BADADDR or start_ea is None:
return None

start_func = get_func(start_ea)
if not start_func:
start_func = get_func(start_ea)
if not start_func:
return None

visited = set()
q = [start_func.start_ea]

while q:
f_ea = q.pop()
if f_ea in visited:
continue
visited.add(f_ea)

func = get_func(f_ea)
if not func:
continue

size = func.end_ea - func.start_ea
if size > minimal_size:
return func.start_ea

for insn_ea in idautils.FuncItems(func.start_ea):
for xr in idautils.XrefsFrom(insn_ea):
tgt = xr.to
tgt_func = get_func(tgt)
if tgt_func and tgt_func.start_ea not in visited:
q.append(tgt_func.start_ea)
return None

def build_fixed_asm(insn: ida_ua.insn_t, old_ea: int, new_ea: int, func_range: tuple[int, int]) -> bytes | None:
"""
Build the fixed instruction bytes for the instructions within the function being pivoted. This function handles control flow instructions and IP-relative data references.
"""
mnem = insn.get_canon_mnem()
old_insn_bytes = insn_bytes_at(old_ea)
if old_insn_bytes is None:
return None

# fix control flow instructions
if insn.ops[0].type in (ida_ua.o_near, ida_ua.o_far):
target_ea = insn.ops[0].addr
if func_range[0] <= target_ea <= func_range[1]:
# prevent intra-function control flow instructions from being modified
return old_insn_bytes
return ks.asm(f'{mnem} {target_ea:#x}', addr=new_ea, as_bytes=True)[0]

# fix IP related data references (e.g. mov reg, cs:...)
raw_op_str = next(cs.disasm(old_insn_bytes, old_ea)).op_str
if 'ip' in raw_op_str:
old_offset = int(raw_op_str.split('[')[1].split(']')[0].split('+')[-1].strip(), 16)
new_offset = old_offset + (old_ea - new_ea)
new_op_str = raw_op_str.replace(f'{old_offset:#x}', f'{new_offset:#x}')
# print(f'Fixing IP-relative operand:{old_ea:012x}: {raw_op_str} ->{new_ea:012x} {new_op_str}')
return ks.asm(f'{mnem} {new_op_str}', addr=new_ea, as_bytes=True)[0]

return old_insn_bytes

def func_pivot(func_ea : int, pivot_ea : int):
"""
Migrate the function at `func_ea` to `pivot_ea` by patching the instructions at `pivot_ea` with the fixed instruction bytes from the original function. The function handles control flow instructions and IP-relative data references to ensure the pivoted function works correctly at its new location.
"""
bias = pivot_ea - func_ea
func = get_func(func_ea)
funcrng = (func.start_ea, func.end_ea)
counter = 0
while counter <= func.size():
insn = ida_ua.insn_t()
length = ida_ua.decode_insn(insn, func_ea + counter)

if length == 0:
continue

fixed_asm = build_fixed_asm(insn, func_ea + counter, pivot_ea + counter, funcrng)
patch_bytes(pivot_ea + counter, fixed_asm)
counter += length

def install_sandbox(to_hook: int = None, hook_addr: int = eh_frame, max_size: int = eh_frame_size) -> bool:
"""
Install sandbox by inline hook the function at `to_hook` (or the first suitable function if `to_hook` is None) to the compact sandbox code at `hook_addr`.Temporily support only AMD64 architecture.
"""
if to_hook is None:
to_hook = get_first_good_func()
if to_hook is None:
print('[-] No suitable function found to hook.')
return False

CALLTHIS = bytes.fromhex('E8 00 00 00 00') # call $+5
inline_hook_asm = [
'lea rdi, [rip]',
'and rdi, -0x1000',
'push rdi',
'xor rdx, rdx',
'mov dl, 7',
f'mov rsi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'xor rax, rax',
'mov al, 0xa',
'syscall', # mprotect(hook_addr & -0x1000, length, PROT_READ | PROT_WRITE | PROT_EXEC)
f'jmp {hook_addr:#x}'
] if is64 else [
CALLTHIS,
'pop ebx',
'and ebx, -0x1000',
'push ebx',
f'mov ecx, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'xor edx, edx',
'mov dl, 7',
'xor eax, eax',
'mov al, 0x7d',
'int 0x80', # mprotect(hook_addr & -0x1000, length, PROT_READ | PROT_WRITE | PROT_EXEC)
f'jmp {hook_addr:#x}'
]

filters_bytes = bytes([0x20, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x15, 0x0, 0x1, 0x0, 0x3E, 0x0, 0x0, 0xC0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x15, 0x0, 0x7, 0x0, 0x3B, 0x0, 0x0, 0x0, 0x15, 0x0, 0x6, 0x0, 0x42, 0x1, 0x0, 0x0, 0x15, 0x0, 0x5, 0x0, 0x2, 0x0, 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x1, 0x1, 0x0, 0x0, 0x15, 0x0, 0x3, 0x0, 0x29, 0x0, 0x0, 0x0, 0x15, 0x0, 0x2, 0x0, 0x2A, 0x0, 0x0, 0x0, 0x15, 0x0, 0x1, 0x0, 0x65, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x7F, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80]) if is64 else bytes([0x20, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x15, 0x0, 0x1, 0x0, 0x3, 0x0, 0x0, 0x40, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x15, 0x0, 0x6, 0x0, 0xB, 0x0, 0x0, 0x0, 0x15, 0x0, 0x5, 0x0, 0x66, 0x1, 0x0, 0x0, 0x15, 0x0, 0x4, 0x0, 0x5, 0x0, 0x0, 0x0, 0x15, 0x0, 0x3, 0x0, 0x27, 0x1, 0x0, 0x0, 0x15, 0x0, 0x2, 0x0, 0x66, 0x0, 0x0, 0x0, 0x15, 0x0, 0x1, 0x0, 0x1A, 0x0, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0xFF, 0x7F, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80])
"""
amd64 sandbox:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x07 0x00 0x0000003b if (A == execve) goto 0012
0005: 0x15 0x06 0x00 0x00000142 if (A == execveat) goto 0012
0006: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0012
0007: 0x15 0x04 0x00 0x00000101 if (A == openat) goto 0012
0008: 0x15 0x03 0x00 0x00000029 if (A == socket) goto 0012
0009: 0x15 0x02 0x00 0x0000002a if (A == connect) goto 0012
0010: 0x15 0x01 0x00 0x00000065 if (A == ptrace) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
#################################################################
i386 sandbox:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0x40000003 if (A == ARCH_I386) goto 0003
0002: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x06 0x00 0x0000000b if (A == execve) goto 0011
0005: 0x15 0x05 0x00 0x00000166 if (A == execveat) goto 0011
0006: 0x15 0x04 0x00 0x00000005 if (A == open) goto 0011
0007: 0x15 0x03 0x00 0x00000127 if (A == openat) goto 0011
0008: 0x15 0x02 0x00 0x00000066 if (A == socketcall) goto 0011
0009: 0x15 0x01 0x00 0x0000001a if (A == ptrace) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
"""
prog = (len(filters_bytes) // 8).to_bytes(8, 'little').ljust(16, b'\xCC') if is64 else (len(filters_bytes) // 8).to_bytes(4, 'little').ljust(8, b'\xCC')

inline_hook_bytes = b''
for asm in inline_hook_asm:
try:
inline_hook_bytes += asm if type(asm) is bytes else ks.asm(asm, addr=to_hook + len(inline_hook_bytes), as_bytes=True)[0]
except Exception as e:
print(f'[-] Error occurred while assembling instruction: {asm}')
print(f'[-] Error: {e}')
assert len(inline_hook_bytes) <= minimal_size, 'Inline hook code exceeds the minimal size limit.'
old_bytes = get_bytes(to_hook, len(inline_hook_bytes))

install_sandbox_asm = [
'xor r8d, r8d',
'xor r10d, r10d',
'xor edx, edx',
'mov esi, 1',
'mov edi, 0x26',
'mov eax, 0x9d',
'syscall', # prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
'lea rdx, [rip + 0x57]', # &filters
'mov qword ptr [rdx - 8], rdx',
'sub rdx, 0x10',
'mov esi, 2',
'mov edi, 0x16',
'mov eax, 0x9d',
'syscall', # prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)
f'lea rdi, [rip - {hook_addr - to_hook + 0x40:#x}]',
f'lea rsi, [rip + 0x98]',
f'mov ecx, {len(inline_hook_bytes):#x}',
'rep movsb', # recover the original bytes before the hook
'pop rdi',
f'mov rsi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'mov edx, 5',
'mov eax, 0xa',
'syscall', # mprotect(to_hook & -0x1000, length, PROT_READ | PROT_EXEC)
f'jmp {to_hook:#x}',
] if is64 else [
'xor ebx, ebx',
'xor ecx, ecx',
'xor edx, edx',
'xor esi, esi',
'xor edi, edi',
'mov bl, 0x26',
'mov cl, 1',
'mov eax, 0xac',
'int 0x80', # prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
CALLTHIS,
'pop edx',
'lea edx, [edx + 0x57]', # &filters
'mov dword ptr [edx - 4], edx',
'sub edx, 0x8',
'xor ebx, ebx',
'mov bl, 0x16',
'xor ecx, ecx',
'mov cl, 2',
'mov eax, 0xac',
'int 0x80', # prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)
CALLTHIS,
'pop edi',
f'lea edi, [edi - {hook_addr - to_hook + 0x38:#x}]',
CALLTHIS,
'pop esi',
f'lea esi, [esi + 0x8d]',
f'mov ecx, {len(inline_hook_bytes):#x}',
'rep movsb', # recover the original bytes before the hook
'pop ebx',
f'mov esi, {((hook_addr - to_hook) & -0x1000) + 0x2000: #x}',
'mov edx, 5',
'mov eax, 0x7d',
'int 0x80', # mprotect(to_hook & -0x1000, length, PROT_READ | PROT_EXEC)
f'jmp {to_hook:#x}'
]

install_sandbox_bytes = b''
for asm in install_sandbox_asm:
try:
install_sandbox_bytes += asm if type(asm) is bytes else ks.asm(asm, addr=hook_addr + len(install_sandbox_bytes), as_bytes=True)[0]
except Exception as e:
print(f'[-] Error occurred while assembling instruction: {asm}')
print(f'[-] Error: {e}')
install_sandbox_bytes += prog + filters_bytes
install_sandbox_bytes += old_bytes
assert len(install_sandbox_bytes) <= max_size, f'Sandbox code({len(install_sandbox_bytes)} bytes) exceeds the max_size({max_size} bytes).'
patch_bytes(to_hook, inline_hook_bytes)
patch_bytes(hook_addr, install_sandbox_bytes)
return True

def _parse_ea(text: str) -> int | None:
text = text.strip()
if not text:
return None

try:
return int(text, 0)
except ValueError:
pass

ea = idc.get_name_ea_simple(text)
if ea is not None and ea != idc.BADADDR:
return ea

return None

def _parse_size(text: str) -> int | None:
text = text.strip()
if not text:
return None
try:
return int(text, 0)
except ValueError:
return None

def _ask_install_params() -> tuple[int | None, int, int] | None:
class InstallSandboxParamsForm(ida_kernwin.Form):
def __init__(self):
super().__init__(
r'''STARTITEM 0
BUTTON YES* Install
BUTTON CANCEL Cancel
Install Sandbox Parameters
Please input values below (supports 0x..., decimal, symbol for addresses)

<to_hook (empty = auto):{to_hook_input}>
<hook_addr (required): {hook_addr_input}>
<max_size (required): {max_size_input}>
''',
{
'to_hook_input': ida_kernwin.Form.StringInput(value=''),
'hook_addr_input': ida_kernwin.Form.StringInput(value=f'{eh_frame:#x}'),
'max_size_input': ida_kernwin.Form.StringInput(value=f'{eh_frame_size:#x}'),
},
)

form = InstallSandboxParamsForm()
form.Compile()
try:
ok = form.Execute()
if ok != 1:
return None

to_hook_text = form.to_hook_input.value.strip()
hook_addr_text = form.hook_addr_input.value.strip()
max_size_text = form.max_size_input.value.strip()
finally:
form.Free()

if not hook_addr_text or not max_size_text:
ida_kernwin.warning('hook_addr and max_size are required.')
return None

to_hook = _parse_ea(to_hook_text)
if to_hook_text.strip() and to_hook is None:
ida_kernwin.warning('Invalid to_hook value.')
return None

hook_addr = _parse_ea(hook_addr_text)
if hook_addr is None:
ida_kernwin.warning('Invalid hook_addr value.')
return None

max_size = _parse_size(max_size_text)
if max_size is None or max_size <= 0:
ida_kernwin.warning('Invalid max_size value.')
return None

return to_hook, hook_addr, max_size

def run_install_sandbox_action() -> bool:
params = _ask_install_params()
if params is None:
return False

to_hook, hook_addr, max_size = params
try:
ok = install_sandbox(to_hook=to_hook, hook_addr=hook_addr, max_size=max_size)
except Exception as e:
ida_kernwin.warning(f'install_sandbox failed: {e}')
return False

if ok:
ida_kernwin.info('Sandbox installed successfully.')
return True

ida_kernwin.warning('Sandbox installation did not complete.')
return False

ACTION_NAME = 'dunfkme:install_sandbox'
CHANGE_TYPE_ACTION_NAME = 'dunfkme:change_lvar_type_and_expand_stack'


def _get_selected_stack_lvar_info() -> tuple[int, ida_hexrays.lvar_t] | None:
"""Return (func_ea, lvar) for current pseudocode selection, or None."""
if not ida_hexrays.init_hexrays_plugin():
return None

w = ida_kernwin.get_current_widget()
if not w or ida_kernwin.get_widget_type(w) != ida_kernwin.BWN_PSEUDOCODE:
return None

vu: ida_hexrays.vdui_t = ida_hexrays.get_widget_vdui(w)
if not vu:
return None

if not vu.get_current_item(ida_hexrays.USE_KEYBOARD):
return None

it: ida_hexrays.ctree_item_t = vu.item
lvar: ida_hexrays.lvar_t = None
if it.citype == ida_hexrays.VDI_LVAR:
if hasattr(it, 'l'):
lvar = it.l
elif hasattr(it, 'get_lvar'):
lvar = it.get_lvar()
elif it.citype == ida_hexrays.VDI_EXPR and it.e.op == ida_hexrays.cot_var:
idx = it.e.v.idx
lvar = vu.cfunc.lvars[idx]

if not lvar:
return None

# Ensure it is a stack variable, not a register temporary.
if not lvar.is_stk_var() or lvar.is_reg_var():
return None

return vu.cfunc.entry_ea, lvar

def _expand_stack_frame(func_ea: int, size_delta: int) -> bool:
"""Expand the stack frame of the function at `func_ea` by `size_delta` bytes. This function should handle updating the function's stack frame size and adjusting all relevant instructions (e.g. those that reference the stack variable) accordingly."""
func_end = get_func(func_ea).end_ea
now_ea = func_ea
now_size = 0
allocated = None

while now_ea <= func_end:
sp_delta = ida_frame.get_sp_delta(func_ea, now_ea)
if sp_delta != 0:
sus_ea = now_ea - now_size
insn = ida_ua.insn_t()
ida_ua.decode_insn(insn, sus_ea)
mnem = insn.get_canon_mnem()
op0 = idc.print_operand(sus_ea, 0)
op1 = idc.print_operand(sus_ea, 1)
if ('sub' in mnem and 'sp' in op0) or \
(allocated and 'add' in mnem and 'sp' in op0 and insn.ops[1].value == allocated):
allocated = insn.ops[1].value
new_frame_size = allocated + size_delta
new_insn_asm = f'{mnem} {op0}, {new_frame_size:#x}'
new_insn_bytes = ks.asm(new_insn_asm, addr=sus_ea, as_bytes=True)[0]
old_insn_bytes = insn_bytes_at(sus_ea)
if len(new_insn_bytes) != len(old_insn_bytes):
print(f'[-] Failed to patch stack frame instruction at {sus_ea:#x}: new instruction size {len(new_insn_bytes)} does not match old instruction size {len(old_insn_bytes)}.')
return False
patch_bytes(sus_ea, new_insn_bytes)

now_size = idc.get_item_size(now_ea)
now_ea += now_size

return True

def _get_lvar_range(func_ea: int, stkoff: int) -> tuple[int, int] | None:
"""Implementation for getting the range of a local variable in the stack frame."""
frame_tif = ida_typeinf.tinfo_t()
if not ida_frame.get_func_frame(frame_tif, func_ea):
return None

frame_udt = ida_typeinf.udt_type_data_t()
if not frame_tif.get_udt_details(frame_udt):
return None

var_start, var_end = stkoff, None
for udm in frame_udt:
udm: ida_typeinf.udm_t
if udm.begin() // 8 > stkoff:
var_end = udm.begin() // 8
break

if var_end is None:
return None
return var_start, var_end

def _get_selected_lvar_xrefs() -> ida_frame.xreflist_t | None:
info = _get_selected_stack_lvar_info()
if info is None:
return None

func_ea, lvar = info
stkoff = lvar.get_stkoff()
xreflist = ida_frame.xreflist_t()
var_start, var_end = _get_lvar_range(func_ea, stkoff)

if None in (var_start, var_end):
return None

ida_frame.build_stkvar_xrefs(xreflist, func_ea, var_start, var_end)
return xreflist

def _refresh_function_after_stack_change(func_ea: int) -> bool:
"""Refresh function by re-creating it (similar to U then P on function head)."""
pfn = get_func(func_ea)
if not pfn:
return False

start_ea = pfn.start_ea
old_end_ea = pfn.end_ea

# 1) Delete current function definition.
if not del_func(start_ea):
return False

# 2) Ask IDA to recreate function at the same entry, letting analysis infer frame info.
ok = False
try:
ok = bool(add_func(start_ea))
except Exception:
ok = False

if not ok:
try:
ok = bool(idc.add_func(start_ea, idc.BADADDR))
except Exception:
ok = False

if not ok:
return False

new_pfn = get_func(start_ea)
if not new_pfn:
return False

# 3) Trigger and wait for analysis.
analysis_end = max(old_end_ea, new_pfn.end_ea)
reanalyze_function(new_pfn)
plan_and_wait(start_ea, analysis_end)

# 4) Refresh decompiler cache.
if ida_hexrays.init_hexrays_plugin():
ida_hexrays.mark_cfunc_dirty(start_ea)
try:
ida_hexrays.decompile(start_ea)
except Exception:
pass

# 5) Jump back to pseudocode view so workflow stays in decompiler window.
_restore_pseudocode_view(start_ea)

return True


def _restore_pseudocode_view(func_ea: int) -> bool:
"""Open/activate pseudocode view for function entry after re-analysis."""
if not ida_hexrays.init_hexrays_plugin():
return False

try:
pw = ida_hexrays.open_pseudocode(func_ea, 0)
if pw:
ida_kernwin.activate_widget(pw, True)
ida_kernwin.jumpto(func_ea)
return True
except Exception:
pass

# Fallback: invoke UI action to show pseudocode for current address.
try:
ida_kernwin.jumpto(func_ea)
ida_kernwin.process_ui_action('hx:GenPseudo')
return True
except Exception:
return False

def _change_lvar_type_and_expand_stack():
info = _get_selected_stack_lvar_info()
if info is None:
ida_kernwin.warning('Please select a stack local variable in pseudocode view first.')
return False

func_ea, lvar = info
stkoff = lvar.get_stkoff()
old_size = lvar.type().get_size()
default_decl = f'{lvar.type()}'
decl = ida_kernwin.ask_str(default_decl, 0, 'Input target type declaration')
if not decl:
return False

decl = decl.strip()
if not decl:
ida_kernwin.warning('Empty type declaration.')
return False

# Reuse IDA built-in declaration parser/type engine.
lvar_tif = ida_typeinf.tinfo_t()
ida_typeinf.parse_decl(lvar_tif, ida_typeinf.get_idati(), f'{decl};', ida_typeinf.PT_TYP)

new_size = lvar_tif.get_size()
if new_size is None or new_size <= 0:
ida_kernwin.warning(f'IDA cannot determine a concrete size for type: {decl}')
return False

if new_size - old_size <= 0:
ida_kernwin.warning(f'New type size ({new_size} bytes) is not greater than the original type size ({old_size} bytes).\nUse "Change Variable Type" action if you just want to change the variable type without expanding the stack frame.')
return False

frame_tif = ida_typeinf.tinfo_t()
if not ida_frame.get_func_frame(frame_tif, func_ea):
ida_kernwin.warning('Failed to get function frame type info. The variable type has been changed, but stack frame expansion may not work.')
return False

old_frame_size = frame_tif.get_size() - 0x10 if is64 else 8 # account for return address and old BP
new_frame_size = old_frame_size + new_size

if not _expand_stack_frame(func_ea, new_size):
ida_kernwin.warning('Failed to expand stack frame.')
return False

xreflist = _get_selected_lvar_xrefs()
if xreflist is None:
ida_kernwin.warning('Failed to get stack variable references. The variable type has been changed, but some references may not be updated.')
return False

for i, xref in enumerate(xreflist):
xref_ea = xref.ea
old_insn_bytes = insn_bytes_at(xref_ea)
old_insn_asm = next(cs.disasm(old_insn_bytes, xref_ea))
op = old_insn_asm.op_str
bp_bias_str = op.split('-')[-1].split(']')[0].strip()
bp_bias = int(bp_bias_str, 16)
head_bias = old_frame_size - stkoff
op = op.replace(bp_bias_str, f'{new_frame_size - (head_bias - bp_bias):#x}')
new_insn_asm = f'{old_insn_asm.mnemonic} {op}'
print(f'Patching instruction at {xref_ea:#x}: {old_insn_asm.mnemonic} {old_insn_asm.op_str} -> {new_insn_asm}')
new_insn_bytes = ks.asm(new_insn_asm, addr=xref_ea, as_bytes=True)[0]
patch_bytes(xref_ea, new_insn_bytes)

if not _refresh_function_after_stack_change(func_ea):
ida_kernwin.warning('Type/insn update done, but failed to refresh function frame/decompilation state.')
return False

return True


class InstallSandboxActionHandler(ida_kernwin.action_handler_t):
def activate(self, ctx):
run_install_sandbox_action()
return 1

def update(self, ctx):
# Availability is finally controlled in popup hook; keep action enabled.
return ida_kernwin.AST_ENABLE_ALWAYS

class ChangeLvarTypeAndExpandStackActionHandler(ida_kernwin.action_handler_t):
def activate(self, ctx):
_change_lvar_type_and_expand_stack()
return 1

def update(self, ctx):
# Availability is finally controlled in popup hook; keep action enabled.
return ida_kernwin.AST_ENABLE_ALWAYS

class DunfkmeUiHooks(ida_kernwin.UI_Hooks):
def finish_populating_widget_popup(self, widget, popup):
if ida_kernwin.get_widget_type(widget) != ida_kernwin.BWN_PSEUDOCODE:
return 0

if _get_selected_stack_lvar_info() is None:
return 0

ida_kernwin.attach_action_to_popup(widget, popup, CHANGE_TYPE_ACTION_NAME, None)
return 0


_ui_hooks = None

def register_install_sandbox_action() -> bool:
desc = ida_kernwin.action_desc_t(
ACTION_NAME,
'Install Sandbox...',
InstallSandboxActionHandler(),
None,
'Install seccomp sandbox inline hook',
-1,
)

if not ida_kernwin.register_action(desc):
return False

ida_kernwin.attach_action_to_menu('Edit/Plugins/', ACTION_NAME, ida_kernwin.SETMENU_APP)
return True

def register_change_lvar_type_and_expand_stack_action() -> bool:
desc = ida_kernwin.action_desc_t(
CHANGE_TYPE_ACTION_NAME,
'Change Variable Type And Expand Stack',
ChangeLvarTypeAndExpandStackActionHandler(),
None,
'Change the type of the selected pseudocode local variable and expand the stack frame accordingl',
-1,
)
return ida_kernwin.register_action(desc)

def unregister_install_sandbox_action() -> None:
ida_kernwin.detach_action_from_menu('Edit/Plugins/', ACTION_NAME)
ida_kernwin.unregister_action(ACTION_NAME)

def unregister_change_lvar_type_and_expand_stack_action() -> None:
ida_kernwin.unregister_action(CHANGE_TYPE_ACTION_NAME)

class DunfkmePlugin(ida_idaapi.plugin_t):
flags = ida_idaapi.PLUGIN_KEEP
comment = 'Install seccomp sandbox hook from IDA menu.'
help = 'Use Edit/Plugins/Install Sandbox... to configure and install sandbox.'
wanted_name = 'Dunfkme Sandbox Installer'
wanted_hotkey = ''

def init(self):
global _ui_hooks

if not register_install_sandbox_action():
return ida_idaapi.PLUGIN_SKIP
if not register_change_lvar_type_and_expand_stack_action():
unregister_install_sandbox_action()
return ida_idaapi.PLUGIN_SKIP
_ui_hooks = DunfkmeUiHooks()
_ui_hooks.hook()
return ida_idaapi.PLUGIN_KEEP

def run(self, arg):
run_install_sandbox_action()

def term(self):
global _ui_hooks
if _ui_hooks is not None:
_ui_hooks.unhook()
_ui_hooks = None
unregister_install_sandbox_action()
unregister_change_lvar_type_and_expand_stack_action()
def PLUGIN_ENTRY():
return DunfkmePlugin()