Ostrich Algorithm
还可以在汇编里面把call exit nop掉,直接动调到最后可以拿到md5值
Safe Word
逻辑上可能有点不好理解,我们先理一下逻辑。
下面是main函数和一个sub_11e9()和sub_11fc(arg1),使用了BN反编译。
main函数开头使用了一个mmap函数分配了一块可以用于执行的内存,也就是说data_16020中的内容可以被作为代码执行。我们的输入被放入了rax_2中。并且还初始化了一个值var_20=0x5b。然后以我们的输入和var_30计算出索引获取rax_5中的值,作为参数传入sub_11fc()中,在上面分析sub_11fc()函数其实就是把rax_5的值作为了函数(代码)执行。既然是作为指令执行。那么这肯定就是是汇编的字节码,这四字节数据是一段汇编代码,被sub_11fc()执行,并把返回值赋给var_20用作下一次索引的计算。依次遍历完我们的输入。
for i in range(32):
var_20 = f( rax5[ (input[i] + var_20 << 8) << 3 ] )
我们的输入在一次的循环中是可以不同的,范围是(32–>127),而var_20在一次循环中是不变的,有没有想过var_20的本质是什么?var_20在每次计算时都被左移了8位,假设var_20是0xff那么 0xff<<8 == 0xff00;此时与input[i]相加刚好构成了一个范围(0xff20–>0xff7f)。那么外面的”<< 3”是干嘛的?观察一下rax_5的索引间隔是8,”<<3”其实就是乘8,所以我们计算出的值要*8才是rax_5的索引。每次执行完代码后都会更新var_20的值,于是范围发生了改变,假设改变后的var_20 = 0x11于是范围改变,(0xff20–>0xff7f) –> (0x1120–>0x117f)。有没有一种感觉,rax_5被分成了许多的块,而var_20其实就是某一个块的索引,每一个块中又会指定下一个块的索引?再想一下,这和函数的跳转是不是有异曲同工之处?看一下rax_5的值发现确实是分块的。我们知道flag的开头是”utflag{“不妨动调看看执行的指令到底是什么。在call处下断点进去查看。再继续运行几次看看,发现指令形式也是一样的。
函数直接返回了0x0B,也就是下一个块的索引,这下就真相大白了。
其实主要的逻辑就是函数跳转,var_20是函数的基地址,我们要用输入的字符控制函数执行指令的地址(基地址+字符),使函数直接跳转到下一个函数,只要遍历完所有的函数,我们的输入就是flag。
我们可以计算出每一段函数的基地址,然后遍历所有可见字符,如果匹配到”0x6a, l[i + 1], 0x58, 0xc3(push xx pop rax retn)”这条指令,说明这个字符就是正确的(这里的执行顺序就是函数的排布顺序)。
那么这题目的意义是什么呢?我们的输入可以看作是一个漏洞,我们可以通过构造一个输入来控制函数的执行流程(控制流入侵),达到自己想要的目的,那么这就完成了一次入侵。我们逆向自己的pwn题
def parse_data(data):
entries = data.strip().split(";")
values = dict()
for entry in entries:
if "=" in entry:
str = entry.split("=")
index = str[0].strip()[3::].replace(']','')
value = str[1].strip()
values[int(index)]=list(int(value.replace("LL",'')).to_bytes(4,"little"))
return values
data = """
v7[23296] = 1933468268LL;
v7[23297] = 729031286LL;
v7[23298] = 2575079303LL;
v7[23299] = 1475628389LL;
v7[23300] = 243385641LL;
v7[23301] = 1799261851LL;
......
"""
code = parse_data(data)
l = []
op = 91
path = []
path.append(91)
i =1
for j in code:
dex = j // 256
if dex == path[i-1] : continue
path.append(dex)
i+=1
print(path)
flag =''
for l in range(len(path)):
for char in range(32,128):
try:
if code[(char + op * 256)]==[0x6a,path[l+1],0x58,0xc3]:
flag += chr(char)
op = path[l+1]
break
except:
continue
print(flag,len(flag),len(path))
#utflag{1_w4nna_pl4y_hypix3l_in_c}
map
就是把字符用五个数字表示,输入所有可见字符然后导出表就行。
text =""
for i in range(32,127):
text += chr(i)
print(text)
str1 = text[:35:]
str2 = text[35:35+35:]
str3 = text[35+35::]
print(str1,str2,str3)
table = dict()
output1 ="4916849171491704917349172491754917449161491604916349162491654916449167491664915349152491554915449157491564915949158492094920849211492104921349212492154921449201492004920349265"
output2 = "4920249205492044920749206491934919249195491944919749196491994919849185491844918749186491894918849191491904936949368493714937049373493724937549374493614936049363493624936549364"
output3 ="4936749366493534935249355493544935749356493594935849345493444934749346493494934849351493504940149400494034940249405494044940749265492654926549265492654926549265492654926549265"
a = output1 + output2 + output3
value1 = [a[i:i+5] for i in range(0,len(a),5)]
for i,j in zip(text,value1):
table[j] =i
print(table)
m = "4934849349493674935749360493664940249346493534935849348493574936549351493644937449348493464936449365493744935349360493464935449364493574935749374493494935349358493594935449404"
mm =[m[i:i+5] for i in range(0,len(m),5)]
for i in mm:
print(table[i],end='')
# utflag{shouldve_used_haskell_thonk}
gameboy游戏用gbg模拟器运行。通过不断的改变值搜索内存中的值来找到关键内存位置。
这里我们搜索豆子的值,当豆子为3时发现只有CB94处的值为3。这类游戏的变量存储一般就在一块内存内,不会相隔太远,运行游戏改变donze的值,发现CB96处的值改变这里就是donze的值,因为要获取最大值直接输入ff,发现它往CB97的位置进了1位,于是把两边都改成FF FF发现值变成了负数,说明是有符号的,所以最大值应该是0111 1111 1111 1111,转成小端序就是FF 7F修改后就可以获得flag。
flag