Flare-on Challenge 2017 部分题解
//已经在内部论坛发过一次了,再发一次,凑凑博客更新量。
Flareon是主要题目为逆向的一场CTF比赛,已经是第四届了,比赛时间一个月左右。知道今年的比赛开始的时候就是九月底了,真正做题基本就是在10月1日开始,然而比赛结束是在北京时间10月13日中午,而且我还要上班……一共十二道题,绝大多数题是在十一假期完成的,最后差一块题拿牌子,实在是时间不够。
虽然Flareon官方也发了题解,但好歹也是做了很久的题……总之还是发上来吧。
看了看他们的WP,最后一题的页数竟有四十多页,就算一天半天不吃不喝也没法做完,何况还要上班……所以还是明年再战吧。
日志和压缩包还在整理,有时间传。
使用的软件版本:
- Windows version: 10.0.15063
- x64dbg version: Sep 12 2017
- IDA version: IDA Pro 7.0
感想:
- 不要为了做题而做题。企业环境挖洞是一回事,你现在是在学习,是另一回事。想好自己是谁。
- 目前网络安全更多的是适量的数学+少量的EE+足量的CS。没有扎实的CS基础就不要谈安全。
- 相比于国内CTF比赛的套路题,国外的比赛更多的是通过CTF题目为渠道,让你了解更多的东西。这一点国内比赛这两年多多少少学到了一点,但真的还差很多很多。
- 学习能力和信息收集能力很重要。我也不知道自己是怎么一小时内敢在.Net CLR里下断点的,还没有符号……
另外近日听说x部下了指示,漏洞细节不能公开。希望大家不要受影响,不要被一些无谓的、傻逼的、操蛋的行政命令束缚住自己进步的脚步。
ROT13
JS里将字符串ROT13之后做的比较。直接用bash就可以了:
alias rot13="tr '[A-Za-z]' '[N-ZA-Mn-za-m]'"
echo [email protected] | rot13
# [email protected]
Igniteme
程序启动后输入字符串XOR然后做对比。那么就用python解开:
vp = [0x0D, 0x26, 0x49, 0x45, 0x2A, 0x17, 0x78, 0x44, 0x2B, 0x6C, 0x5D, 0x5E, 0x45, 0x12, 0x2F, 0x17, 0x2B, 0x44, 0x6F, 0x6E, 0x56, 0x09, 0x5F, 0x45, 0x47, 0x73, 0x26, 0x0A, 0x0D, 0x13, 0x17, 0x48, 0x42, 0x01, 0x40, 0x4D, 0x0C, 0x02, 0x69, 0x04]
for i in range(len(vp)-2, -1, -1):
vp[i] = vp[i] ^ vp[i+1]
vp = ''.join([ chr(i) for i in vp[:-1]])
# [email protected]
Greektome
输入被分成多组,每组20个字符,密钥验证算法如下:
# # Key judgement
# for &bytes in 0x40107c -> 0x40107c+0x79:
# bytes ^= key
# bytes += 0x22
# assert( 0xFB5E == sub_4011E6(0x40107C, 0x79) )
# # sub_4011E6(x,y)
# assert( y != 0)
# ebx = x
# eax = 0x14
# {
# di = (int16)var_4
# esi = min(eax, edx)
# edx -= esi
# {
# eax =
# }
# }
# // CMOVx: Conditional move according to X
# v2: 0x79 -> 0x65 -> 0x51 -> 0x3D -> 0x29 -> 0x15 -> 0x01 -> 0
最简单的方法还是用python爆破:
def generator(a1,a2):
v2 = a2
v3 = 0xff
v8 = 0xff
if not a2:
raise ValueError
v4 = a1
scanned = 0
while v2:
v5 = v8
v6 = 0x14 if v2>0x14 else v2
v2 = v2 - v6
for i in range(v6):
v5 = v5 + a1[scanned]
v5 = v5 & 0xFFFF
v3 = v3 + v5
v3 = v3 & 0xFFFF
scanned = scanned + 1
#print("% 3d - % 3d"%(scanned, v6))
v8 = (v5 >> 8) + (0xff&v5)
v8 = v8 & 0xFFFF
v3 = (v3 >> 8) + (0xff&v3)
v3 = v3 & 0xFFFF
print("=== % 4x = % 4x ==="%(v8,v3))
return ( (v8 >> 8) + ((0xFF) & v8) ) | ( 0xFFFF & ( (v3 << 8) + ((0xFF00) & v3) ) )
def check(xk):
borg = """
33 E1 C4 99
11 06 81 16 F0 32 9F C4 91 17 06 81 14 F0 06 81
15 F1 C4 91 1A 06 81 1B E2 06 81 18 F2 06 81 19
F1 06 81 1E F0 C4 99 1F C4 91 1C 06 81 1D E6 06
81 62 EF 06 81 63 F2 06 81 60 E3 C4 99 61 06 81
66 BC 06 81 67 E6 06 81 64 E8 06 81 65 9D 06 81
6A F2 C4 99 6B 06 81 68 A9 06 81 69 EF 06 81 6E
EE 06 81 6F AE 06 81 6C E3 06 81 6D EF 06 81 72
E9 06 81 73 7C """
bbs = [(0xff&((i^xk)+0x22)) for i in list(bytes.fromhex(borg.replace("\n", "")))]
return generator(bbs, 0x79)
j = 0
for i in range(0xff):
if ( check(i) == 0xFB5E ):
j=i
break
# j = 0xa2
# Now it is time to check bytes
for i in range(0x79):
ida_bytes.patch_byte(0x40107c+i, ida_bytes.get_byte(0x40107c+i)^j)
# [email protected]
看来和出题人是一个想法。提交于October 2nd, 8:33:33 PM CST
。
Notepad
一道侧重于样本分析的程序。源程序是Windows的记事本,被在另一个段注入了新代码,并修改了OEP。新代码会使用ROR13获取所有加载模块的哈希,并按照如下算法比较并存储之:
for module in PEB->LDRDATA->InMemoryOrderModuleList
if HashROR13(module.name)==HashROR13(TargetModuleName):
return module # for futher usage ;)
注意的是,在我的Windows 10中,这段程序会出问题,因为由于一些未知的原因,这段代码在我机器上获取的模块名称为大写,而ROR13的实现是区分大小写的。我们在0x10153F6
下断点,断点日志为Parsing {s:edx}
,查看日志:
Parsing L"4_notepad.exe"
Parsing L"ntdll.DLL"
Parsing L"KERNEL32.DLL"
Parsing L"KERNELBASE.dll"
Parsing L"comdlg32.dll"
...
可以发现KERNEL32
被跳过了。patch掉此处,便于动态调试。
对于被调用的函数,我们可以用Flare-IDA中的Shellcode-hashes脚本来辅助分析。静态分析后发现,程序在遍历%USERPROFILE%/flareon2016challenge
目录下的文件,检查其时间是否符合特征值。如果符合,就提取出其中的八个字节到key.bin
中,以此类推。Swings师傅提醒后,发现正确的文件是从去年的题目包中下载的(P.S 我觉得这个题设计的一般)。释放文件后再次执行程序,会弹出对话框:
---------------------------
---------------------------
[email protected]
---------------------------
OK
---------------------------
提交于October 3rd, 3:05:00 PM CST
。
Pewpewboat
这次是个小游戏,有100关,每一关的初始化结构体都是用同一个函数解密不同内存地址得到的,因此flag应该在这里。简单分析了这个结构体,如下:
typedef struct {
QWORD GoodStatus; // being fliped at y-axis. just the correct answer
QWORD DiskStatus; // contains current status
char Checksum[8]; // 恶魔妹妹摸咪咪……
DWORD ammo_amount;
char LastInputUpperCase[2];
char LevelName[15];
something[17];
char HelloMessage[69];
}
这里本来是想通过patch的方法,跳过验证过程,直接打出提示消息,以为这样就会在最后得到flag。等全部patch完才发现,还是要上调试器脚本的。考虑到你可能也想试一下,我附上了自己patch后的文件。
回到游戏流程。每轮游戏中,你的输入值除了会影响DiskStatus
外,还会被原样放到上面结构体的LastInputUpperCase
中。如果对应的点在GoodStatus
中为True
,那么就算击中。此外,每轮游戏中,checksum
值都会变,用来解密下一轮游戏。
总之,你有两个绕过验证的方案:
- 手动写程序计算所有正确的输入向量
- 像我一样打patch然后从内存里提出来
我在patch后这样设了下断点:
$ gef pewpewboat.patched
gef > b *0x403ec6
gef > commands
> append binary memory /tmp/5_final_full.bin `$rax+0x00 $`rax+0x240
> c
> end
gef > r
然后在dump文件中用strings
简单看了看,发现了这么一行:
PEWYouPEWfoundPEWsomePEWlettersPEWdidPEWya?PEWToPEWfindPEWwhatPEWyou'rePEWlookingPEWfor,PEWyou'llPEWwantPEWtoPEWre-orderPEWthem:PEW9,PEW1,PEW2,PEW7,PEW3,PEW5,PEW6,PEW5,PEW8,PEW0,PEW2,PEW3,PEW5,PEW6,PEW1,PEW4.PEWNextPEWyouPEWletPEW13PEWROTPEWinPEWthePEWsea!PEWTHEPEWFINALPEWSECRETPEWCANPEWBEPEWFOUNDPEWWITHPEWONLYPEWTHEPEWUPPERPEWCASE.
把所有的PEW换成空格,会发现他想让你按照他给的数字,将对应局游戏答案对应的字符做ROT13。做完之后我得到了:
butwhereistherum
还是一头雾水。后来在师傅启发下,又看了看源码,发现输入的时候有一段代码忘了看。索性直接输进去:
[email protected]
提交于October 4th, 1:40:36 AM CST
。
Payload
因为很久没碰过Windows了,所以在读MSDN和逆向rundll32
以及ntdll
上花的时间比解题时间还长。
从几个字符串中可以看出,这个DLL正确的调用姿势是%WINDOWS%/system32/rundll32.exe payload.dll #1
。DLL在rundll32.exe!InitCommandInfo+78
处被加载,然后在rundll32.exe!wWinMain+2A2
处查找并调用了导出函数。此外,DLL也可以有一些初始化函数,这些函数由 payload.dll!_initterm
调用,地址存放在payload.dll!0x10250
,在读取导出表前执行。显然是MSVC的作风。
总之,我目前知道第一个函数是sub_180004400
和sub_180005A50
。他们会在DLL加载后即刻执行,并用这样的一组key来初始化导出表:
(ctime.year + ctime.monty) % 26
解密后,你可以看到这个DLL原始文件名为lustrated.dll
,导出函数是orphanedirreproducibleconfidences
。分析这个导出函数,发现他会检查输入参数是否符合导出函数名,如果符合就弹出一个这样的对话框:
---------------------------
key part
---------------------------
key[25] = 0x6d;
---------------------------
OK
---------------------------
emmmmmm……于是我写了个编译器脚本,key从0到25循环,然后得到了这么一批日志:
ENT >>> "filingmeteorsgeminately" <<< ENT
ENT >>> "leggykickedflutters" <<< ENT
ENT >>> "incalculabilitycombustionsolvency" <<< ENT
ENT >>> "crappingrewardsanctity" <<< ENT
ENT >>> "evolvablepollutantgavial" <<< ENT
ENT >>> "ammoniatesignifiesshampoo" <<< ENT
ENT >>> "majesticallyunmarredcoagulate" <<< ENT
ENT >>> "roommatedecapitateavoider" <<< ENT
ENT >>> "fiendishlylicentiouslycolouristic" <<< ENT
ENT >>> "sororityfoxyboatbill" <<< ENT
ENT >>> "dissimilitudeaggregativewracks" <<< ENT
ENT >>> "allophoneobservesbashfulness" <<< ENT
ENT >>> "incuriousfatherlinessmisanthropically" <<< ENT
ENT >>> "screensassonantprofessionalisms" <<< ENT
ENT >>> "religionistmightplaythings" <<< ENT
ENT >>> "airglowexactlyviscount" <<< ENT
ENT >>> "thonggeotropicermines" <<< ENT
ENT >>> "gladdingcocottekilotons" <<< ENT
ENT >>> "diagrammaticallyhotfootsid" <<< ENT
ENT >>> "corkerlettermenheraldically" <<< ENT
ENT >>> "ulnacontemptuouscaps" <<< ENT
ENT >>> "impureinternationalisedlaureates" <<< ENT
ENT >>> "anarchisticbuttonedexhibitionistic" <<< ENT
ENT >>> "tantalitemimicryslatted" <<< ENT
ENT >>> "basophileslapsscrapping" <<< ENT
ENT >>> "orphanedirreproducibleconfidences" <<< ENT
再用另一个脚本读取这些参数并执行,最后拼成一组flag:[email protected]
。
提交于October 5th, 10:49:44 PM CST
zsud
这个程序很麻烦,花了差不多两天。
挂上调试器后,就注意到了一个操蛋的特征:
.text:004036A0 sub_4036A0 proc near ; CODE XREF: _WinMain@16_0+2↓p
.text:004036A0 jmp sub_403630
.text:004036A0 sub_4036A0 endp
...
.text:00403630 jmp sub_4035C0
...
.text:004035C0 jmp sub_403550
...
每个函数调用前部都是连续的128个jmp。调试器还比较好办,用trace功能,追踪到第一个不是jmp的指令即可:
Break Condition: byte(eip) != e9
Log Text: Jumping over 0x{p:eip}
Log Condition: byte(eip) == e9
IDA也还好。因为提供了很完整的函数信息,我们用下面的脚本可以做更精确的判断。如果这串脚本你看不懂,可以先去了解下IDA的ctree结构,这个在二次开发和复杂程序处理中比较重要。
def log(msg):
print("[LOG-FixChain]%s"%msg)
def get_call_chain(ea=None, chainmax=100000):
if not ea: # use current cursor address
ea = ida_kernwin.get_screen_ea()
#ea = ida_func.get_func(ea)
jmp_chain = [ea]
while chainmax: # Search until found end or meet chianmax
current = jmp_chain[-1]
log("Processing %08x"%current)
# Get ctree
cfunc = ida_hexrays.decompile(current)
# Will not work if we do not output a string
s = str(cfunc)
if not cfunc: # We have encountered something unusual. Get back and jump to normal end
jmp_chain.pop()
log("Unusual call at %08x. Quit for safe."%(jmp_chain[-1]))
jmp_chain.append(0)
break
if cfunc.entry_ea != current:
#reprocess immidately if this is not an entry
log("Redirected to %08x"%(cfunc.entry_ea))
jmp_chain[-1] = cfunc.entry_ea
continue
targets = 0
target_ea = 0
# Check all items
# Is a jump slot if only one call found
# Or we should stop here
for citem in cfunc.treeitems:
citem = citem.to_specific_type
if citem.opname == "call":
# Found a call.
targets += 1
target_ea = citem.x.obj_ea
if targets > 1:
break
if targets != 1:
jmp_chain.append(0)
break
else:
# Is a slot function. Add target to our list
jmp_chain.append(target_ea)
chainmax -= 1
return jmp_chain
def fix_call_chain(ea=None,chainmax=100000):
chain = get_call_chain(ea,chainmax)
if not chain[-1]==0:
raise ValueError("Unexcepted chain end!")
chain_target = chain[-2]
log("Targeting to %08x"%(chain_target))
# Get last function's def
for i in range(len(chain)-2):
ida_name.set_name(chain[i],"j_%s_id_%d"%(ida_name.get_name(chain_target), i), SN_CHECK)
# Also try to modify its arguments
p = ida_typeinf.tinfo_t()
if ida_nalt.get_tinfo(p, chain_target) and ida_nalt.set_tinfo(chain[i], p):
pass
工具准备好后,分析也简单了一些。程序启动了两个线程,一个是HTTP服务器,另一个是主线程。
主线程会解密402490
的一长串字符串,你可以在解密后到eax
找到这串大小为[esp+44h]
的解密结果。这个结果是一些base64字符串,解码之后的东西同样是一团糟。
因此就看看接下来对这串字符串做了什么。这个程序用的一个一个技术叫.Net CLR Hosting
,也就是说你可以调用一个垫片库mscoree.dll
,在你自己的程序中启动一个.Net CLR环境,并执行CSharp代码。跟进去发现,启动这个环境时使用的DLL是一个不存在于磁盘上的文件。有经验的应该了解了,这里首先用MapViewOfFileSub
, CreateFileMappingA/W
之类的东西,把文件映射到内存中,然后去访问他。然而看到这块的时候并不知道怎么将一个Map好的文件Unmap回来,所以就很惨。
最后在漫长的逆向后,发现了402780
的一个函数,这个函数应该是实现了一个PE Loader,会把一块内存当作一个DLL文件,映射到内存中。找到这里就好办了,在第一次映射之前下断点,直接从Memory Dump中拿到文件,也就是压缩包中的zsud_3c000_striped.dll
。
这个文件就是刚才提到的在CLR Hosting中跑的.Net程序。用dnSpy可以看到,程序用soooooo_sorry_zis_is_not_ze_flag
作为密钥,用全0的IV,使用AES256解密了一组字符串,并将其加载到PowerShell中执行。因为不知道MS在加密这里有没有用什么骚操作,我就用了最原始的办法:直接把dnSpy的输出修改后重新编译一下。解密结果保存为一个ps1文件。
这个powershell文件就是整个MUD的核心。源码细节就不说了,总之你要拿到key的话,涉及到这么几个隐藏环节:
- 抽屉里有把钥匙,可以拿
- 拿钥匙后的所有步骤会被发给服务器,钥匙的内容也会发过去
- 钥匙内容满足一定条件,就会被传送到前门
- 在游戏(而不是Twitter)中找Kevin Mandia,带上他的头盔,扔下key,他会吐一串神器的Hex String
估计按这几步走,就能拿到flag。拿钥匙后步骤被发送到服务器的条件是directions_enum[step] == msvcrt.rand()
,但是这个rand
不知道是被重定义了还是怎样,我自己写程序得到的结果对不上。因此索性在CLR虚拟机中直接下断点,取rand的返回值并打印,然后一顿骚操作,得到了这么一组日志:
EAX ======== w
EAX ======== n
EAX ======== n
EAX ======== e
EAX ======== e
EAX ======== s
EAX ======== s
...
太长,就不都列出来了。然后重启程序,按照这些步走一遍,被发配到大门口,然后看看key:
> look key
You can start to make out some words but you need to follow the RIGHT_PATH!@66696e646b6576696e6d616e6469610d0a
这个就符合第3步的要求:钥匙文本中有个at符号。看看at后面是什么:
In [4]: b=bytes.fromhex("66696e646b6576696e6d616e6469610d0a")
In [6]: b
Out[6]: b'findkevinmandia\r\n'
去右上角的房间找他,并按照第四步操作:
> say Kevin Mandia
Kevin says, with a nod and a wink: '6D 75 64 64 31 6E 67 5F 62 79 5F 79 30 75 72 35 33 6C 70 68 40 66 6C 61 72 65 2D 6F 6E 2E 63 6F 6D'.
Bet you didn't know he could speak hexadecimal! :-)
emmmm,我不知道他会说十六进制,但我知道python会:
In [7]: c=bytes.fromhex("6D 75 64 64 31 6E 67 5F 62 79 5F 79 30 75 72 35 33 6C 70 68 40 66 6C 61 72 65 2D 6F 6E 2E 63 6F 6D")
In [8]: c
Out[8]: b'[email protected]'
提交于October 7th, 2:53:36 PM CST
Flair
我一点Android也不懂,Java也不是太懂,但现代魔法的好处就在于,可以快速用JEB掌握 ;)
程序有四步,先来看第一步Michael
。没有任何混淆,直接按照要求解出字符串MYPRSHE__FTW
。注意,Java中hashCode
算法描述里的^
符号是数学中的意义,而不是C中的意义。
第二步Brain
会读一些资源。用apktool
解包后按照要求查找并填入格式化字符串,就得到了hashtag_covfefe_Fajitas!
。注意中间的fefe
是小写。
第三步Milton
用了Stapler
类中的一些方法,我写了组Python脚本,可以方便工作:
import base64
# from https://code.activestate.com/recipes/576736-rc4-arc4-arcfour-algorithm/
def rc4crypt(data, key):
x = 0
box = list(range(256))
for i in range(256):
x = (x + box[i] + (key[i % len(key)])) % 256
box[i], box[x] = box[x], box[i]
x = 0
y = 0
out = []
for char in data:
x = (x + 1) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
out.append(((char) ^ box[(box[x] + box[y]) % 256]))
return bytes(out)
# neapucx is just hexstring to bytes, we will not use it yet
# poserw is SHA1 digest. Just use python lib
drdfg = lambda x,y:bytes(i^y for i in base64.b64decode(x))
vutfs = lambda x,y,z:rc4crypt(drdfg(x,y),z)
v0 = " !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
v3 = vutfs("Zcj30d9jroqAN2mtzayVK0awaVTLnoXVcsjl9ujUAd22JiI9xfhqEW1BbkG3LsgQRoStjh+Eb6wTD4BwD9Kypa5ggXfHLWmFjFgERViV+5IRU4RbUPDUwYTivhsdZnA=", 17, b"for").decode()
iemm = lambda x:''.join([v0[v3.index(x[i])] for i in range(len(x))])
现在来看这一步。首先要给APP打四星,然后才能点按钮。你输入的值应该是下面这个字符串的SHA-1:
j = [
("JP+98sTB4Zt6q8g=", 56, "State"),
("rh6HkuflHmw5Rw==", 96, "Chile"),
("+BNtTP/6", 118, "eagle"),
("oLLoI7X/jIp2+w==", 33, "wind"),
("w/MCnPD68xfjSCE=", 148, "river")]
print(''.join([vutfs(e[0],e[1],bytes(e[2], "ascii")).decode() for e in j]))
字符串是 A rich man is nothing but a poor man with money.
,SHA1是10aea594831e0b42b956c578ef9a6d44ee39938d
。
第四步就麻烦一点,这个程序会从资源文件tvps
中按照这个格式读数据:
struct {
short Index;
byte Value;
} tvps[112];
数据会被加载到Hashmap中并按照Index排序,然后拼接Value。时间仓促,写了段脚本:
fpr = open("tspe", "rb").read()
q = int(len(fpr)/3)
a = [0 for i in range(0xFFFF)]
for i in range(q):
try:
key = (fpr[ i*3 + 0 ]<<8) + (fpr[ i*3+1 ] << 0)
a[key]=fpr[i*3+2]
except IndexError:
print("Overflow with key = %d and i = %d"%(key,i))
break
print(''.join([chr(i) for i in a[:112]]))
字符串是Give a man a fire and he'll be warm for a day. Set a man on fire and he'll be warm for the rest of his life.
,SHA1是5f1be3c9b081c40ddfc4a0238156008ee71e24a4
。
四步结束后,得到flag[email protected]
提交于October 7th, 10:14:55 PM, CST
remorse
文件名后缀是ino.hex
,还有字符串Arduino UNO
,应该要在板子上跑的。可惜我没有钱,所以就手动分析吧。
对于新的CPU,首先要掌握的有:
- 架构、内存布局、SFR和指令集。官网有
- 调用约定。估计是GCC的,那就是这里
- 用脑子思考而不是思维定势思考
写了一些代码用于辅助IDA和GDB调试(是的,有模拟器,不过我不会操控IO),此外IDA自带的Auto Commends功能也帮了不少忙。之后的事情绝大多数时间就是逆向,逆向和逆向,还会通过avr-gdb和avr模拟器认识一些常见的avr执行特征。最后在0536
发现了一组函数,好像在加载什么东西,并用一个密钥异或,而这个密钥的来源和某个计时器的输出结果有关。索性爆破:
p=[0xb5, 0xb5, 0x86, 0xb4, 0xf4, 0xb3, 0xf1, 0xb0, 0xb0, 0xf1, 0xed, 0x80, 0xbb, 0x8f, 0xbf, 0x8d, 0xc6, 0x85, 0x87, 0xc0, 0x94, 0x81, 0x8c]
q=0
for q in range(0, 0x100):
print('%x: %s'%(q, ''.join([chr((p[i]^q)+i) for i in range(0x17)])))
然后搜索flare-on
,发现:
...
da: op^q2n1qr4Aembsf,po-bpl
db: [email protected]
dc: ij\k,t3st6;gs`q`*jm/\rf
...
提交于October 10th, 3:15:50 AM CST
shell
第一组:设密文c是大小为c[count][keylen]
的数组,密钥为w,明文为p[count][keylen]
。那么:
p[0] = c[0] ^ w
p[1] = c[1] ^ p[0] = c[1] ^ c[0] ^ w
p[2] = c[2] ^ p[1] = c[2] ^ c[1] ^ c[0] ^ w
...
不妨令k[i] = c[0] ^ c[1] ^ ... ^ c[n-1] ^ c[n]
,则对任意i<count
,有:
p[i] = k[i] ^ w
且
p[i]全部为可显示字符
写脚本求解,可得到后面的,思路基本一致,都是爆破。
然而自己太胖,不能穿女装,码力不够,因此从Swings师傅那里偷了个flag,[email protected]
。
covfefe
这个程序是用虚拟机保护的。虚拟机设计思路和subfuscator
有相似之初,不了解这个的可以去看看movfuscator
,就是用减法替代一切操作。
首先分析一下401070
的主函数,我们可以知道这块vCPU可以这样描述:
typedef enum {
SYSCALL_GET_CHAR,
SYSCALL_PUT_CHAR
} SYSCALL_ID;
typedef struct {
uint32 r0; //gpr r0
uint32 syscall_args[SYSCALL_ID];
uint32 syscall_flag[SYSCALL_ID]; // 1 means being called, 0 means call finished
char ROM_PART_A[0x134];
char RAM_PART_A[0x150];
uint32 sp; // point at first stack item and grow from lower to higher
char ROM_PART_B[0xF04]; // an index number at 00403448 will be changed upon start
char RAM_PART_B[OTHER_RAM_SIZE]; // all other contents
};
void sub_401070(
VM* vm_addr,
uint32 vm_mem_size,
uint32 vm_start_point
);
vCPU的指令长度是3字节(24bit,不是21bit,真不是clemency架构)。VMM也很好描述:
while True:
mem[inst[1]] -= mem[inst[0]]
if inst[2]: # conditional jump
if mem[inst[1]] <= 0:
jmp(inst[2])
continue
process_sys_call()
cursor+=3
注意,这里我们还是在用32-bit的视角去看。然而vCPU是在24bit的世界的。所以下面我会用1420
代表32bit内存中的偏移量,基地址是0x403008
,而同样的地址在24bit下表示为@508
。换算方法是:
0x508 * 4 + 0x403008 == 0x403008+0x1420 == 0x404428
vCPU只提供了sub和jmp,这样的话追踪和逆向麻烦很多。函数调用约定类似于__cdecl
,对指针的解引用用的方法差不多是SMC。
现在我们知道了参数传递、指令集、寄存器、SFR、内存布局和取地址/数据方式,那么剩下的事情多多少少好办一点。
为了更详细的了解这个程序的运行过程,做几个断点:
# Code break points to monitor jumps
bp 0x4010af
bpcnd 0x4010af, "dword(esp+c) && (dword(dword(esp+8)+dword(esp+0)) - dword(dword(esp+4)+dword(esp+0))) <= 0 "
bplog 0x4010af, "jmp from {p:dword(ebp-8)} => {p:dword(ebp-8)*4+dword(esp+0)} to {p:dword(esp+c)} => {p:dword(esp+C)*4+dword(esp+0)}"
SetBreakpointCommand 0x4010af, "g;"
# Memory access br. Reading hint offsets and hits. This helps to find where is `puts`
Address=00403458
Summary=access(dword), log("READ HINT OFFSET"), cmd("g")
Address=00403710
Summary=access(byte), log("READ HINT"), cmd("g")
trace记录在压缩包中可以看到。分析后发现,这个程序涉及flag验证的虚指令长度大概为1k条左右。考虑到这种虚拟方式对编码的影响,实际映射到x86上顶多300条汇编(取地址膨胀率5:1,数值操作膨胀率3:1,跳转膨胀率2:1),折算到C语言没几行,因此加密算法应该极其简单。
多次尝试后(看我的trace记录就能看到这个多次尝试的过程),可以知道密钥算法是:
for a,b in input: # Two bytes per time
assert( (a*0xF)<<7+b^0x7F == arr[cnt++] ) # cnt from 0, and arr is at @e9c with 16 long ints
直接写脚本:
In [104]: '%s@flare-on.com'%(''.join(["%c%c"%(((i>>7)//0xF),(i&0x7F)^0x7F) for i in [220810, 188179, 193934, 182430, 211227, 182413, 193947, 224668, 222742, 213152, 186267, 182430, 188172, 224653, 192010, 209407]])[:-1])
Out[104]: '[email protected]'
提交于October 11th, 5:34:07 PM CST