总结idapython在逆向中的应用
前言idapython是一种在ida工具中运行的脚本,它可以对ida中呈现的汇编指令和数据进行操作,它可以简化我们的逆向工作加快我们的分析。idapython是一种Python语言,它相比Python多了一些函数库,这些函数库帮助我们对ida中显示出来的内容进行操作,本文主要是从两个方面总结idapython,一个是:idapython的语法,第二个是:idapython的实战运用。
语法篇
脚本运行
File->Script file->选中文件
地址获取
列举几个常用的显示反汇编窗口中特殊地址的函数,在输出地址时,为了更形象得显示地址,常常使用hex()函数来把地址的返回值转成十六进制形式,获取的地址可以为汇编代码遍历提供了开始地址。
def hex(str) #把字符串转换成十六进制
def MinEA() #获取反汇编窗口中代码段的最小地址
def MaxEA() #获取反汇编窗口中代码段的最大地址
def ScreenEA() #获取光标所在位置
def SegEnd(str) #获取程序中某段的结束地址
实例1:
print hex(MinEA())
print hex(MaxEA())
print hex(ScreenEA())
for seg in Segments():
#如果为代码段,则调用GetStr
if SegName(seg) == '.text':
print hex(SegEnd(seg))
运行结果:
数值获取
在使用idapython脚本时,经常需要获取反汇编窗口中,某些地址处的数值,这些数值是以十六进制的形式存在的,得到这些数值后就可以查找某些字符串或者指令或者数据的十六进制形式(长整型),获取数值的值常用的有4个,它们传入的参数都为地址,返回的是数值(字符串形式)
def Byte(addr) #以字节为单位获取地址处的值
def Word(addr) #以字为单位获取地址处的值
def Dword(addr) #以双字为单位获取地址处的值
def Qword(addr) #以四字为单位获取地址处的值
def isLoaded(addr) #判断地址处的数值是否有效
实例2:
print isLoaded(0x401114)
print Byte(0x401114)
print Word(0x401114)
print Dword(0x401114)
print Qword(0x401114)
运行结果:
操作码获取
使用idapython脚本获取地址处的操作数及操作码,可以帮助我们对某些给定的指令加以注释,或者根据操作数来判断程序的关键操作并可以给出显眼的注释。
def GetDisasm(addr) #输出某地址处的反汇编字符串(包括注释)
def GetOpnd(addr,n) #获取某地址处的操作数(第一个参数是地址,第二个是操作数索引)
def GetFlags(addr) # 获取与地址对应的整数
def GetMnem(addr) #输出某地址处的指令
def GetOpType(addr,n) #输出指定操作数的类型
def GetOperandValue(addr,n) #输出跟指定操作数相关的数据值
实例3:
print GetDisasm(0x401114)
print GetOpnd(0x401114,1)
print hex(GetFlags(0x401114))
print GetMnem(0x401114)
print GetOpType(0x401114,1)
print GetOperandValue(0x401114,1)
运行结果:
搜索操作
idapython脚本跟其它的脚本一样具有搜索功能,它可以在指定的地址处进行搜索,搜索的方向可以是上,也可以是下,搜索的方向是由flag来确定的。搜索功能可以帮助我们找到特定的字符串或者指令,搜索失败时返回-1.
def FindBinary(ea,flag,str) #对某字符串进行搜索,找到后返回字符串地址
def FindCode(ea,flag)#从当前地址查找第一个指令并返回指令地址
def FindData(addr,flag) #从当前地址查找第一个数据项并返回数据地址
flag取值有:
SEARCH_DOWN 向下搜索
SEARCH_UP 向上搜索
SEARCH_NEXT 获取下一个找到的对象。
SEARCH_CASE 指定大小写敏感度
SEARCH_UNICODE 搜索 Unicode 字符串。
实例4:
print hex(FindBinary(MinEA(),SEARCH_DOWN,'41 42'))
print hex(FindCode(MinEA(),SEARCH_DOWN))
print hex(FindData(MinEA(),SEARCH_DOWN))
运行结果:
数据判断操作
我们获取指令或者操作码,有时需要判断获取的值是否符合相应的数据类型,如果符合则进一步进行操作,否则进行下一步操作。以下函数的返回值类型为bool型,传入的参赛类型为字符串类型。
def isCode(f)#判断是否为代码
def isData(f) #判断是否为数据
def isTail(f) #判断标记地址是否为尾部
def isUnknown(f) #判断标记地址是否为未知
def isHead(f) #判断标记地址是否为头部
实例5:
print isCode(GetFlags(0x401114))
print isData(GetFlags(0x401114))
print isTail(0x401134)
print isUnknown(0x401214)
print isHead(0x401114)
运行结果:
修改操作部分
该部分函数为修改ida数据库中数值的函数,数值的修改之后,当再次打开ida,数据不会复原,数据的修改函数以修改单位进行区分,以下为数据的修改函数。这些函数可以帮助我们修改获取去除某些指令或者字符串。
def PatchByte(addr,val) #以字节为单位修改
def PatchWord(addr,val) #以字为单位修改
def PatchDword(addr,val) ##以双字为单位修改
实例6:
PatchByte(0x401114,0x12)
PatchWord(0x401124,0x12456978)
PatchDword(0x401134,0x00000000)
运行结果:
交互部分
当运行脚本时,需要跟用户进行交互,让用户来选择接下来要进行的内容,这些交互可以让用户做选择,输入字符串或者跳转到想要查看的地址等,这些函数使得操作更人性化,甚至这些交互函数也可以作为程序的调式工具,输出中间结果等。
def AskYN(n,str) #弹出对话框,让用户来选择是或者否
def Jump(addr) #跳转到相应的地址
def AskStr(str,str) #显示一个输入框,让用户输入字符串
def Message(str) #在输出出口输出一句字符串
实例7:
AskYN(1,'is it?')
Jump(0x401114)
AskStr('hello!','please enter!')
Message('hello!')
运行结果:
函数操作部分
在代码段中,含有很多的函数,通过跟函数操作相关的函数可以获取代码段中的所有函数、函数中的参数、函数名及函数中调用了哪些函数。这些函数可以帮助我们分析重要的函数,从而加快对程序的分析。
def Functions(start,end)#获取某地址区间所有函数
def GetFunctionName(addr)#获取函数名字
def NextFunction(addr) #获取下一个函数地址
def XrefsTo(Addr, flags)#获取调用某地址处函数的函数
实例8:
for seg in Segments():
#如果为代码段
if SegName(seg) == '.text':
for function_ea in Functions(seg,SegEnd(seg)):
FunctionName=GetFunctionName(function_ea)
print FunctionName
nextFunc=NextFunction(function_ea)
print nextFunc
运行结果:
实战篇
1.xctf simple-unpack
1.查壳,发现是elf文件并且加了upx的壳,接下来使用脱壳工具脱壳。
2.用ida打开,看到flag,可以使用脚本提取出该flag
3.当使用脚本找到'movesi, offset flag'地址时,通过取值函数可以获取flag所在的地址,进而通过循环结构获取flag整个字符串.代码如下:
def GetStr(start,end):
flag=''
for addr in range(start,end):
#判断有没有找到'movesi, offset flag'
if GetOpnd(addr,0)=='esi' and 'flag' in GetOpnd(addr,1):
#获取flag所在的地址
address=hex(Dword(addr))
#把地址转换成十进制数
taddress=int(address,16)
#使用循环结构获取flag
while(1):
flag+=chr(Byte(taddress))
#判断flag读取是否结束
if chr(Byte(taddress))=='}':
break
taddress+=1
print flag
break
# 遍历所有的段
for seg in Segments():
#如果为代码段,则调用GetStr
if SegName(seg) == '.text':
GetStr(seg,SegEnd(seg))
运行结果:
2.xctf Shuffle
1.用ida加载,发现有一大串数字,经分析发现要对这些数字转换成字符后,就出现了flag。
2.那么用idapython脚本,转换这些数字,该脚本遍历代码段的指令,找到含有这些数字的指令,取出进行转换就可获取flag。代码如下:
def GetAns(start,end):
flag=''
for addr in range(start-4,end):
#当前地址的下一个地址
#判断下一个地址所在的指令是否为mov , al
if GetOpnd(addr,1)=='al' and 'esp' in GetOpnd(addr,0):
#判断当前地址所有的指令是否是'moveax, xxh'
if GetOpnd(addr-5,0)=='eax':
#获取十六进制数
hex=GetOpnd(addr-4,1)[:2]
#转换成10进制数
Int=int(hex,16)
flag+=chr(Int)
if chr(Int)=='}':
break
print flag
# 遍历所有的段
for seg in Segments():
#如果为代码段,则调用GetAns
if SegName(seg) == '.text':
GetAns(seg,SegEnd(seg))
结果如下:
3.xctf re2-cpp-is-awesome
1.先用以下脚本判断ida中的循环结构,当跳转指令后面接的地址高于跳转指令所在的地址时,该跳转指令为循环跳转指令并加注释,代码如下。使用该脚本后可以看到关键的循环函数,在该循环里有程序自定义的数组。
# -*- coding:utf-8 -*-
#把大写字母转换成小写
def SwapToXiao(c):
t=32
return chr(ord(c)+t)
def isJmp(addr):
SzOp=['JO','JNO','JB','JNB','JE','JNE','JBE','JA','JS','JNS','JP','JNP','JL','JNL','JNG','JG','JCXZ','JECXZ','JMP','JMPE']
llen=len(SzOp)
for i in range(0,llen):
SwapAns=''
#把SzOp数组中所有字符串转换成小写字符串
for c in SzOp:
SwapAns+=SwapToXiao(c)
#加到SzOp数组中
SzOp.append(SwapAns)
#获取操作指令
Op=GetMnem(addr)
#判断是否是操作指令
if isCode(GetFlags(addr)):
#判断是否是跳转指令
for Sin in SzOp:
if Sin==Op:
return 1
return 0
def isCir(start,end):
for ea in range(start,end):
if isJmp(ea)==1:
#获取跳转地址
new_addr=GetDisasm(ea)[-6:]
#判断是否为挑战地址
if new_addr[-1:]<='9' and new_addr[-1:]>='0':
if int(new_addr,16)<ea:
#添加注释
MakeComm(ea,"循环跳转指令")
# 遍历所有的段
for seg in Segments():
#如果为代码段
if SegName(seg) == '.text':
isCir(seg,SegEnd(seg))
运行结果:
2.按f5,看到关键代码
3.接着使用脚本获取这两个数组并获取flag,代码如下:
szint=[]
#该函数获取flag
def GetAns(start,end,tstr):
flag=''
for i in szint:
flag+=tstr
print flag
#该函数获取str字符串
def GetStr():
str_addr=0x400E58
tstr=''
while(1):
#判断循环是否结束
if hex(Byte(str_addr))=='0x0' and hex(Byte(str_addr+1))=='0x0':
break
#叠加字符串字符生成字符串
tstr+=chr(Byte(str_addr))
str_addr+=1
return tstr
#获取整数数组
def getSzInt(start,end):
for addr in range(start,end): #rax*4
#判断是否是mov eax, IntSz指令语句,在该语句中可以获取整数数组的地址
if 'rax*4' in GetOpnd(addr,1):
#获取整数数组的地址
address=hex(Dword(addr+3))
taddress=int(address,16)
#获取整数数组
while(1):
if Dword(taddress)>0x7f:
break
szint.append(Dword(taddress))
taddress+=4
break
for seg in Segments():
if SegName(seg) == '.text':
getSzInt(seg,SegEnd(seg))
str1=GetStr()
GetAns(seg,SegEnd(seg),str1)
显示结果如下:
4.2018 TSRC 团队赛 第二题 半加器
1.在x64dbg中运行,搜索字符串后发现'invalid argument'字符串,并且该字符串传入到函数的参数中,那么程序运行时处理了该字符串。
2.'invalid argument'字符串在只读数据段中,使用如下脚本可以找到该字符串,随后找到调用它的地方。该脚本从只读数据段开始遍历,找到字符串后跳转到相应的地址,使用XrefsTo函数找到调用它的位置并在右边加上注释,跳转到调用的位置。
#invalid argument的前12个字符序列
str1='69 6E 76 61 6C 69 64 20 61 72 67 75'
def GetStrPos(start,end):
#查找str2字符串所在的位置
BinaryAddr=FindBinary(start,SEARCH_DOWN,str1)
#判断查找是否失败
if hex(BinaryAddr)=='0xffffffffL':
print 'not bin'
else:
print 'Binary ',hex(BinaryAddr)
#跳转到字符串所在的位置
Jump(BinaryAddr)
#遍历调用该字符串的位置
for refhs in XrefsTo(BinaryAddr, flags=0):
print "x: %s x.frm 0x%x"%(refhs,refhs.frm)
Jump(refhs.frm)
#做注释
MakeComm(refhs.frm,"使用了invalid argument字符串")
#询问用户
AskYN(1,'看完'+hex(refhs.frm)+'地址处的代码吗?')
for seg in Segments():
#如果为只读数据段,则调用GetStrPos
if SegName(seg) == '.rdata':
#print 'seg ',hex(seg)
GetStrPos(seg,SegEnd(seg))
运行结果:
3.那么接下来寻找程序中的算术或者逻辑操作码,找到后,添加注释并输出地址。我使用GetDisasm()函数获取某地址处的字符串(包括指令和注释),随后清除字符串的空格和判断程序中是否含有注释。
import re
#算术逻辑操作码
CalOp=['mul','imul','or','not','div','xor']
def isCalOp(Op):
for i in CalOp:
if Op==i:
return 1
return 0
#清空字符串中的空格
def clearspace(str1):
str2=''
for i in str1:
if i==' ':
continue
else:
str2+=i
return str2
#判断整个指令是否有注释
def isComment(str):
for i in str:
if i==';':
return True
return False
#判断算术逻辑指令有没有使用十六进制数进行计算
def isHex(str1):
#使用正则表达式构造匹配十六进制数的字符串
pattern= re.compile(r'[-]*+')
sNum=''
try:
if isComment(str1):
xb=str1.rindex(';')
sNum=str1
else:
sNum=str1
#找到十六进制字符串,若没有找到,则抛出异常
ans=pattern.match(sNum)
#判断找到的十六进制数是否准确
if ans.group(0)==sNum and sNum!='0':
return 1
except:
return 0
else:
return 0
4.随后判断算术逻辑指令中是否含有十六进制操作数,如果含有则输出地址,判断的思路是:获取最后一个操作数,通过正则表达式的方式判断该操作数是否为十六进制操作数。
def GetOp(start,end):
for addr in range(start,end):
#获取操作指令
Op=GetMnem(addr)
#判断是否是操作指令
if isCode(GetFlags(addr)):
if isCalOp(Op)==1:
#获取某地址处的字符串,包括指令和注释
Comm=GetDisasm(addr)
Comm=clearspace(Comm)
if isHex(Comm)==1:
#往ida中添加注释
MakeComm(addr,"使用了操作码:"+Op)
print "Op_addr ",hex(addr)
for seg in Segments():
#是否为代码段
if SegName(seg) == '.text':
GetOp(seg,SegEnd(seg))
5.分析地址中的算术逻辑指令,双击输出的地址就能跳转到汇编窗口,发现有三个地方的指令起到关键作用,据此写下求flag的脚本。
def GetAns():
str="invalid argument"
flag=""
for i in range(0,len(str)):
if i==7:
flag+='A'
else:
flag+=chr(ord(str)^28^31)
print flag
GetAns()
运行结果:
5.暴力破解010editor
建议大家看下面的内容前先看下这篇文章:https://www.52pojie.cn/thread-1089329-1-1.html,不然有些内容会看不懂。
1.当输入错误的用户名和注册码时,会显示“Invalid name or password”信息。
2.既然显示“Invalid name or password”信息,那么在只读数据段中必然会有该字符串,而且某指令也会使用该字符串,那么找到哪里使用了该字符串,先是遍历所有的只读数据段中的数值找到字符串,随后通过引用函数来找到使用该字符串的地方。
import time
#Invalid name or password的前12个字符序列
str1='49 6E 76 61 6C 69 64 20 6E 61 6D 65'
def GetStrPos(start,end):
#查找str2字符串所在的位置
BinaryAddr=FindBinary(start,SEARCH_DOWN,str1)
#判断查找是否失败
if hex(BinaryAddr)=='0xffffffffL':
print 'not find'
else:
print 'BinaryAddr ',hex(BinaryAddr)
#跳转到字符串所在的位置
Jump(BinaryAddr)
#遍历调用该字符串的位置
for refhs in XrefsTo(BinaryAddr, flags=0):
print "refhs: %s refhs.frm 0x%x"%(refhs,refhs.frm)
#暂停2秒
time.sleep(2)
#跳转到引用该字符串的地方
Jump(refhs.frm)
#做注释
MakeComm(refhs.frm,"使用了Invalid name or password. Please enter your name and password exa字符串")
for seg in Segments():
#如果为只读数据段,则调用GetStrPos
if SegName(seg) == '.rdata':
GetStrPos(seg,SegEnd(seg))
运行结果如下:
从结果中可以看出使用‘Invalid name or password...’的地址,那么关键跳必然在其附近。
3.暴力破解的关键是找到关键跳,首先找到程序中的跳转指令,跳转指令中除去jmp指令和后接寄存器的指令,因为关键跳往往不是无条件跳转,需要进行判断后跳转,找到后仔细分析跳转指令找到关键跳。
跳转指令会非常多,为了减少工作量,在调用“Invalid name or password”的附近查找跳转指令。
def isJmp(addr):
SzOp=['jo', 'jno', 'jb', 'jnb', 'je', 'jne', 'jbe', 'ja', 'js', 'jns', 'jp', 'jnz', 'jl', 'jnl', 'jng', 'jg', 'jcxz', 'jecxz','jmpe']
#获取操作指令
Op=GetMnem(addr)
#判断是否是操作指令
if isCode(GetFlags(addr)):
#判断是否是跳转指令
for Sin in SzOp:
if Sin==Op:
return 1
return 0
def isRightJmp(start,end):
for ea in range(start,end):
if isJmp(ea)==1:
Disasm=GetDisasm(ea)
#判断指令后面的操作数是否为地址
if Disasm[-9:-4]=='14070':
#获取跳转地址
new_addr=Disasm[-9:]
#输出出现在‘Invalid name or password. Please ’附近的跳转指令
print 'new_addr',new_addr
MakeComm(ea,"跳转指令")
for seg in Segments():
if SegName(seg) == '.text':
isRightJmp(seg,SegEnd(seg))
运行结果如下:
4.分析发现关键跳后,进行NOP
关键跳位置:
Nop代码:
PatchDword(0x140707B6B,0x00000000)
nop后的结果:
参考链接:
https://blog.csdn.net/qq_33526144/article/details/103623807
https://blog.csdn.net/qq_33526144/article/details/104116643
https://blog.csdn.net/qq_33526144/article/details/103994062
https://blog.csdn.net/qq_33526144/article/details/103638199
我膨胀了,竟然看了好一会儿 ida打开,拖入exe,a thousand years later .f5 反编译失败,mmp,ida卸载:'(weeqw
没错,这就是我了,多谢大佬分享教程 老大,这个可以看下吗?https://www.52pojie.cn/thread-1080406-1-1.html
学习了,谢谢楼主 很详细的总结,谢谢 感谢分享,有你更精彩 新人过来学习一下 谢谢分享,学习一下。 干货满满,好长 就想看看不出