idapython入门

IDA Python入门(todo)

ida pro7.7已经基本普及了,所以本篇文章是基于ida pro77和python3环境编写。

不可否认的是ida pro在静态分析上极为强大,不过动调方面还是比起其他工具稍显不足,而idapython提供了大量的ida的api,能一定程度上缓解动调的缺陷,这里就给大家简单介绍一下(

PS:参考了Q神的 http://www.qfrost.com/posts/ctf/idapython ,并且修正为了新版ida python的api

image-20240223213616667

image-20240223221902702

指令相关:

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
from idaapi import *
from ida_dbg import *
from ida_bytes import *
# 返回目标地址指向的指令的前一条指令的地址
# 参数一是查找的开始地址,参数二是往前查找的最小地址(在范围内)
prev_1 = prev_head(0x00007FF6A1AA2577, 1)
prev_2 = prev_head(0x00007FF6A1AA257a, 0x00007FF6A1AA2578) # fail
print(hex(prev_1), hex(prev_2))
>> 0x7ff6a1aa2573 0xffffffffffffffff

# 返回目标地址指向的指令的后一条指令的地址
# 参数一是查找开始地址,参数二是往后查找的最大地址(不在范围内)
next_1 = next_head(0x00007FF6A1AA257a, 1) # fail
next_2 = next_head(0x00007FF6A1AA257a, 0x00007FF6A1AA2580)
>> 0xffffffffffffffff 0x7ff6a1aa257b

# 输出目标地址的反汇编语句(就是有一点点乱码x)
a = generate_disasm_line(0x00007FF6A1AA2585)
print(a)
>> b'\x01\x05movzx\x02\x05 \x01)\x01!eax\x02!\x02)\x01\t,\x02\t \x01*\x01 byte ptr\x02 \x01\t[\x02\t\x01!r10\x02!\x01\t]\x02\t\x02*'

# 获取目标地址指令
a = print_insn_mnem(0x00007FF6A1AA2585)
print(a)
>> movzx

# 获取目标地址的某个操作数(按索引取)
a = print_operand(0x00007FF6A1AA2585, 0)
print(a)
>> b'\x01)\x01!eax\x02!\x02)'

# 获取目标地址某个操作数的值(按索引取)
print(hex(get_operand_value(0x00007FF6A1AA2577, 1)))
print(type(get_operand_value(0x00007FF6A1AA2577, 1)))
>> 0x10
>> <class 'int'>

# 获取目标地址字符串,参数二是长度,参数三是string的类型
a = get_strlit_contents(0x00007FF6A1AC28A8, 4, 0)
b = get_strlit_contents(0x00007FF6A1AC28A8, 4, 1)
print(a)
print(b)
>> b'Erro'
>> b'\xe7\x89\x85\xe6\xbd\xb2'

# 在目标地址添加注释,参数三为True则不会替换comment
a = set_cmt(0x00007FF6A1AA2577, "this is a comment", True)
print(a)
>> True

# 给目标地址的变量改名
a = set_name(0x00007FF6A1AC28A8, "err")
print(a)
>> True

# 获取当前光标处地址
print(hex(get_screen_ea()))
>> 0x7ff6a1ac28a8

# 统计目标地址所在函数有多少个基本块
print(FlowChart(get_func(0x00007FF6A1AA21B0)).size)
>> 149

功能+调试相关:

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
# 设置断点
add_bpt(0x00007FF6A1AA2577)

# 删除断点
del_bpt(0x00007FF6A1AA2577)

# 设置断点是否开启
enable_bpt(0x00007FF6A1AA2577, False)

# 设置idapython为默认语言,在设置条件断点时不设置会默认使用idc
load_and_run_plugin("idapython", 3)

def condition():
print(">>> rip:", get_reg_val("rip"))
return True
add_bpt(0x00007FF6011D2581)
# 设置条件断点,每次运行到这个断点停下会触发condition
set_bpt_cond(0x00007FF6011D2581, "condition()")

# 查看交叉引用,返回一个迭代器,包含idautils._xref对象
a = XrefsTo(0x00007FF6011D2577, flags=0)
for addr in a:
print(hex(addr.frm)) # .frm 返回交叉引用地址

# 读/写 1/2/4/8字节, wide是宽字节
get_byte(addr)
get_word(addr)
get_dword(addr)
get_qword(addr)
get_wide_byte(addr)
get_wide_word(addr)
get_wide_dword(addr)
get_wide_qword(addr)
patch_byte(addr, val)
patch_word(addr, val)
patch_dword(addr, val)
patch_qword(addr, val)

# 获取寄存器rip的值
get_reg_val("rip")

# 设置寄存器rax的值为0x10
set_reg_val("rax", 0x10)

# 开始调试
start_process()
# continue
continue_process()
# 运行到目标地址
run_to(addr)
# 获取并清除调试器事件代码,普通代码返回0x20,断点、ret返回0x10,程序结束返回负数
# 必须在进程执行的代码后面调用该函数,以便检索调试器的事件代码,否则可能会阻止后续尝试
wait_for_next_event(EVENT_TYPE, flags)
# 单步
step_over()
wait_for_next_event(WFNE_SUSP, -1) # 每次step_over都要调用一次,continue这些也是
# 事件 WFNE_SUSP 将等待导致被调试进程挂起的事件,比如异常或断点
# 事件 WFNE_CONT 可以恢复被挂起的进程,继续执行
# example:
wait_for_next_event(WFNE_SUSP, -1)
wait_for_next_event(WFNE_ANY | WFNE_CONT, -1)

实战:使用IDApython编写爆破脚本

PS:使用我给SICTF Round3出的Re题Closeme作为例子

附件: 点击下载附件

这题说实话预期就是爆破(x

大概回顾一下题目,每次创建几个窗口后都会弹出一个messagebox,点击yes/no会被储存为1/0,然后长度为16时就check一次,所以先把CreateWindowsEx和MessageBoxW那里的call xxx给nop掉(你也不想开几千上万个窗口把cpu干烧了吧x),然后就需要手动添加各种值了。

因为每16次循环才能使长度达到16进而check,太影响效率了,所以直接在原来call messagebox那里patch成jmp到cmp rax, 10h这里。

rax是长度,直接jmp过来就没有调用获取长度的函数,我们需要手动设置0x10

1
2
3
4
5
6
7
breakpoint_addr = 0x00007FF6A1AA2577
add_bpt(breakpoint_addr)
# ...
rip = get_reg_val("RIP")
#print("rip: ", hex(rip))
if rip == breakpoint_addr:
set_reg_val("rax", 0x10)

image-20240224010303964

但是再执行两步就会把一个局部变量取出来解一层引用给r10,后续就是16个比较,所以r10应该是一个指向我们储存16个1或0的地址。

那么我们要伪造2层,一是先把那个局部变量的值修改为一个可写地址(同时也要保证运行时不会有其他的写者),然后二是在这个可写地址写入我们爆破的数据。

我直接找了.data段最末尾的0x10的空间

1
2
3
# .data section
buf_addr = 0x7FF6A1ACDFF0
patch_qword(get_reg_val("rbp") + 0x38, buf_addr) # 写入局部变量

然后写入要爆破的数据需要注意端序问题(

1
2
3
4
5
6
7
send = # 你要爆破的数据, 比如 0101010101010101
left = '0' + '0'.join(send[:8][::-1])
left = int(left, 16)
right = '0' + '0'.join(send[8:][::-1])
right = int(right, 16)
patch_qword(buf_addr, left)
patch_qword(buf_addr + 8, right)

然后就是爆破2^16次就行了!

完整爆破脚本:

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
from idaapi import *
from ida_dbg import *
from ida_bytes import *

this_input = 0

start = 0x00007FF6530D220F
#add_bpt(start)
breakpoint_addr = 0x00007FF6A1AA2577
add_bpt(breakpoint_addr)
win_addr = 0x00007FF6A1AA2993
add_bpt(win_addr)

print("Start!")
start_process()

print("Waiting for next event...")
wait_for_next_event(WFNE_SUSP, -1)
# .data section
buf_addr = 0x7FF6A1ACDFF0
patch_qword(get_reg_val("rbp") + 0x38, buf_addr)

while True:
if this_input >= 0x10000:
print("Burst Finished.")
break
send = bin(this_input)[2:].zfill(16)
print("send: ", send)

try:
rip = get_reg_val("RIP")
#print("rip: ", hex(rip))
if rip == breakpoint_addr:
set_reg_val("rax", 0x10)
#print("set rax to 0x10")

left = '0' + '0'.join(send[:8][::-1])
left = int(left, 16)
right = '0' + '0'.join(send[8:][::-1])
right = int(right, 16)
patch_qword(buf_addr, left)
patch_qword(buf_addr + 8, right)
elif rip == win_addr:
print("win")
print("flag: SICTF{" + bin(this_input - 1)[2:].zfill(16) + "}")
exit(0)
continue_process()
wait_for_next_event(WFNE_SUSP, -1)
except Exception as e:
print("err: ", e)
break
this_input += 1

print("out")

Welcome to my other publishing channels