Reversing.kr Writeup(1-10)

折腾了几个月的SCI总算暂时告一段落了,接下来就该祈祷审稿人能不能选择性眼瞎给我通过了。下一步时间多一点了,还是打算搞搞有意思的东西。不过想了一下还是得再巩固巩固逆向基础才行,所以正好最近抽空刷一刷reversing.kr,记录一下题解,之后的话看看能不能整理一下把看过的一些有趣的论文提到的东西或者工具写点东西。

Easy Crack

win32 的一个比较基础的 crackme,需要找出正确的 password。

od 中直接通过寻找字符串的方式找到判断函数的位置,即是0x401080.
在地址0x4010aa的地方看到了GetDlgItemTextA的调用就大概能够猜测到一些了,这里把我们的输入存到了ESP+0x8的地方

接下来就是对输入进行判断,总共四部分
第一部分:

第二个字母 ascii 为 0x61,即a

第二部分:

第三、四个字母要为5y
第三部分:

第四位之后要为R3versing
第四部分:

第一位要为E
组合起来就是答案Ea5yR3versing

Easy Keygen

程序功能是输入一个用户名和一个序列号它会进行 check。

目的是求出给定序列号的用户名。
也就是逆向其中的算法。
通过查找参考字符串可以成功定位到加密部分的位置,如下:

分析之后就是对输入进行逐位异或,第一位异或 0x10,第二位异或 0x20,第三位异或 0x30,第四位异或 0x10,依次类推。
所以写个简单程序即可得到答案

tmp='5B134977135E7D13'
data=bytes.fromhex(tmp).decode('utf-8')
ans=""
for i in range(1,len(data)+1):
    if i%3==1:
        ans+=chr(ord(data[i-1])^0x10)
    if i%3==2:
        ans+=chr(ord(data[i-1])^0x20)
    if i%3==0:
        ans+=chr(ord(data[i-1])^0x30)
print(ans)

Easy Unpack

直接利用 esp 定律即可找到

所以 flag 就是00401150

Music Player

研究一下这个文件,发现是每到 1:00 钟的时候就会弹窗警告,所以我们可以对vbamsgbox下断点,

然后断下来之后看看当前函数块的代码,往上看能看到这么一部分,

这里0xea60就是 60000,所以找到了这里的比较,当然这里可以结合VBDecompiler看,比如这一段的反编译代码为:

Private Sub TMR_POS_Timer() '4044C0
  Dim Me As Variant
  Dim global_4011D0 As Me
  Dim var_20 As Variant
  loc_00404545: ClsMCI = var_A4
  loc_00404568: var_18 = var_A4
  loc_0040456B: If var_A4 < 60000 Then GoTo loc_004045FE
  loc_00404574: var_eax = FrmMain.CMD_STOP_Click
  loc_004045D8: MsgBox(&H402BAC, 64, var_40, var_50, var_60)
  loc_004045F9: GoTo loc_00404795
  loc_004045FE: 'Referenced from: 0040456B
  loc_00404601: If var_30 = -1 Then GoTo loc_004046CA
  loc_0040460B: var_eax = FrmMain.Proc_0_10_403370(Me, &H402BAC)
  loc_00404614: var_BC = var_18
  loc_00404627: If global_407000 <> 0 Then GoTo loc_00404631
  loc_0040462F: GoTo loc_00404642
  loc_00404631: 'Referenced from: 00404627
  loc_00404642: 'Referenced from: 0040462F
  loc_0040465A: If CLng((var_BC / global_4011D0)) <= 0 Then GoTo loc_0040466A
  loc_00404664: ecx = "LI"
  loc_0040466A: 'Referenced from: 0040465A
  loc_0040466C: If CLng((var_BC / global_4011D0)) <> 0 Then GoTo loc_00404673
  loc_00404673: 'Referenced from: 0040466C
  loc_0040468D: var_C0 = var_20
  loc_00404693: var_ret_1 = global_00000001
  loc_004046A1: HS_POS.Value = var_ret_1
  loc_004046CA: 'Referenced from: 00404601
  loc_004046EC: ClsMCI = var_A4
  loc_0040470E: If var_A4 <= 60010 Then GoTo loc_00404795
  loc_0040476F: ecx = &H402BDC & Chr(114) & &H402BE4
  loc_00404795: 'Referenced from: 004045F9
  loc_0040479E: GoTo loc_004047CE
  loc_004047CD: Exit Sub
  loc_004047CE: 'Referenced from: 0040479E
  loc_004047CE: Exit Sub
End Sub

很容易看到var_A4 < 60000 Then GoTo loc_004045FE这样一个比较,那么我们把代码中的jl,即小于 60000 才跳转改成直接 jmp 即可。
修改保存之后尝试第二次运行,发现产生了异常。

但是音乐仍然在正常播放,我们需要处理一下这个异常。
那就直接对kernel32RaiseException下断点

然后断下来之后查看堆栈

跟到0x4046b9

看到这个 jge,直接改 jmp 就可以了。
然后拿到 flag:LIstenCare

Replace

随便输入一个数字程序出现异常并退出。
查看一下参考字符串,定位到了'correct'的位置。

可以看到要想出现correct必须执行0x401073开始的代码,也就是我们需要解决前面两个 jmp 的问题。
0x40105a的地方有GetDlgItemInt函数的调用,用于获取我们的输入并存至0x4084d0,从这儿跟进0x40466f函数。

这里存在的代码的复用,应该是手写的汇编,继续跟进0x40467a

可以看到0x40467a使我们的输入值(存储在0x4084d0)+2,
然后再回到0x40466f,它又将输入值+0x601605c7,之后再+2,
也就是经过0x40466f的函数之后我们的输入值增加了0x601605c7+4
然后 jmp 到了0x404690,这里动态的修改了0x40466f处执行的代码:

而 eax 则是之前修改过的我们的输入值即input+0x601605c7+4
也就是说,这里会把这个地址修改为0x90(汇编指令:NOP)。
再看该代码块的最后有jmp 0x401071,如果我们能够把0x401071覆盖掉,就可以成功显示 correct。
也就是要满足input+0x601605c7+4=0x401071,溢出一下满足input+0x601605c7+4=0x100401071即可。
最后 input 也就是 flag 为2687109798

ImagePrc

一个画图程序,画出来的图和另一个答案。
同样可以通过搜索参考字符串Wrong定位到关键代码:

这里看到选中的部分就是循环进行对比,一旦我们画的和答案不对就弹出wrong
所以我们直接把答案数据抠出来画出来就好了,大小0x15f90,也就是 90000,存储的是 RGB 值,直接用 python 画就行了,至于长和宽,可以从 ida 中一眼就看到

最终代码如下:

# -*- coding: utf-8 -*-

from PIL import Image
x=150
y=200
f = open("a.txt", 'r')
data = f.read().split(" ")
rgbi=[]
print(len(data))
for i in range(0,len(data),3):
    tmp=[data[i],data[i+1],data[i+2]]
    rgbi.append(tmp)
f.close()

c = Image.new("RGB", (x, y))
for i in range(0, x):
    for j in range(0, y):
        rgb = rgbi[i * y + j]
        c.putpixel([i, j],(int(rgb[0],16), int(rgb[1],16), int(rgb[2],16)))
c.show()

得到答案GOT

Position

搜索关键字符串就能得到关键加密函数,偏移为1740,仔细分析这个函数。
第一部分规则:获取 name 输入,并判断输入的 name 长度等于 4

第二部分规则:4 个字母都要求不小于0x61,不大于0x7a

第三条规则:name 的 4 个字母每两个字母不能相等

第四条规则:获取序列号输入,序列号长度为0xb

第五条规则:序列号的第五位要为0x2d,即对应 ascii 为-,即决定了序列号形如xxxxx-xxxxx

接下来为一大段计算,以便后面进行 check,先不说了,后面用到再说。

第七条规则:name[0]&1+5 + (name[1]>>2)&1+1 = pass[0]

第八条规则:(name[0]>>3)&1+5 + (name[1]>>3)&1+1 = pass[1]

第九条规则:(name[0]>>1)&1+5 + (name[1]>>4)&1+1 = pass[2]

第十条规则:(name[0]>>2)&1+5 + name[1]&1+1 = pass[3]

第十一条规则:(name[0]>>4)&1+5 + (name[1]>>1)&1+1 = pass[4]

也就是说,name的前两个字母经过一通计算得到了序列号的前五位,同理后续就是name的后两个字母经过计算得到序列号的后五位。这里不截图了,列出公式如下:

name[0]&1+5 + (name[1]>>2)&1+1 = pass[0]
(name[0]>>3)&1+5 + (name[1]>>3)&1+1 = pass[1]
(name[0]>>1)&1+5 + (name[1]>>4)&1+1 = pass[2]
(name[0]>>2)&1+5 + name[1]&1+1 = pass[3]
(name[0]>>4)&1+5 + (name[1]>>1)&1+1 = pass[4]

name[3]&1+5 + (name[4]>>2)&1+1 = pass[6]
(name[3]>>3)&1+5 + (name[4]>>3)&1+1 = pass[7]
(name[3]>>1)&1+5 + (name[4]>>4)&1+1 = pass[8]
(name[3]>>2)&1+5 + name[4]&1+1 = pass[9]
(name[3]>>4)&1+5 + (name[4]>>1)&1+1 = pass[10]

那么根据 name 的范围0x61-0x7a我们可以直接进行爆破就好了。代码如下:


first=[]
second=[]
password="76876-77776"

for a in range(0x61,0x7b):
    for b in range(0x61,0x7b):
        if a==b: continue
        if ((a)&1)+5+((b>>2)&1)+1 != int(password[0]): continue
        if ((a>>3)&1)+5+((b>>3)&1)+1 != int(password[1]): continue
        if ((a>>1)&1)+5+((b>>4)&1)+1 != int(password[2]): continue
        if ((a>>2)&1)+5+((b)&1)+1 != int(password[3]): continue
        if ((a>>4)&1)+5+((b>>1)&1)+1 != int(password[4]): continue

        first.append(chr(a)+chr(b))

for a in range(0x61,0x7b):
    for b in range(0x61,0x7b):
        if a==b: continue
        if ((a)&1)+5+((b>>2)&1)+1 != int(password[6]): continue
        if ((a>>3)&1)+5+((b>>3)&1)+1 != int(password[7]): continue
        if ((a>>1)&1)+5+((b>>4)&1)+1 != int(password[8]): continue
        if ((a>>2)&1)+5+((b)&1)+1 != int(password[9]): continue
        if ((a>>4)&1)+5+((b>>1)&1)+1 != int(password[10]): continue

        second.append(chr(a)+chr(b))

print(first)
print(second)

截图如下:

根据提示简单组合一下,尝试之后得到 flag 是bump

Direct3D_FPS

一个类 CS 的射击小游戏,还是通过搜索参考字符串找到一个game clear

看到这个messageboxa,猜测byte_FA7028肯定就是最后 flag,跟过去一看发现是乱码,那就是中途对其进行计算才能得到 flag。看看调用,发现这个函数调用了:

这里进行了一个异或操作,然后得到最后的 flag。这里需要动态跟一下了。在偏移3400之后下断点。

经过一波调试大概得知了这个游戏的目的

游戏中出现的这些黄色小人,每个小人有一个编号,然后每个小人打32下之后消失,然后出发 flag 字串的异或操作,就是用已知的byte_FA7028字符串逐位异或i*4,其中i是小人的编号,从 0 开始,那么我们直接爆破就行了,把byte_FA7028开始的字符串 dump 出来直接逐位和4*i异或就可以了。

data=[0x43,0x6B,0x66,0x6B,0x62,0x75,0x6C,0x69,0x4C,0x45,0x5C,0x45,0x5F,0x5A,0x46,0x1C,0x07,0x25,0x25,0x29,0x70,0x17,0x34,0x39,0x01,0x16,0x49,0x4C,0x20,0x15,0x0B,0x0F,0xF7,0xEB,0xFA,0xE8,0xB0,0xFD,0xEB,0xBC,0xF4,0xCC,0xDA,0x9F,0xF5,0xF0,0xE8,0xCE,0xF0,0xA9,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x68,0x08,0x07,0xA8,0x13,0x8F,0x07,0xD8,0x74,0x8F,0x02,0x60,0x82,0x8F,0x02,0x01,0x00,0x00,0x00,0x57,0xB7,0x98,0xC3,0x00,0x00,0x00,0x00,0x57,0xB7,0x98,0xC3,0x57,0xB7,0x98,0x43,0x00,0x00,0x00,0x00,0x57,0xB7,0x98,0x43,0x57,0xB7,0x98,0xC3,0x00,0x00,0x00,0x00,0x57,0xB7,0x98,0xC3,0x57,0xB7,0x98,0xC3,0x00,0x00,0x00,0x00,0x57,0xB7,0x98,0x43,0x57,0xB7,0x98,0x43]
ans=""
for i in range(len(data)):
    ans+=chr(data[i]^(i*4))
print(ans)

ransomware

打开一看 pushad,猜测加壳了,esp 定律没用上,直接单步一边跳一边跟进就行了,最终找到入口:

但是进去之后一大堆无用的指令:

我们先用lordpe转储出来,importrec修复一下 IAT 表,然后把这些没用的指令替换掉

data = open('./Unpack_.exe','rb').read()
data = data.replace(b'\x60\x61\x90\x50\x58\x53\x5b',b'\x90\x90\x90\x90\x90\x90\x90')
open('./unpack_fuck.exe','wb').write(data)

放进 ida 后观察,主要加密如下:

就是个异或取反。至于sub_401000函数则没啥用,垃圾函数,混淆用的。
做到这儿就有点迷了。程序里面也没有 key 啊。后来才看到 readme,file 文件是从 exe 加密之后的,那么加密方式我们知道,再结合一些固定并已知的 pe 头比如This program cannot be run in DOS mode就能解出 key 了。

f=open("file",'rb')
data=f.read()
f2=open("run.exe",'rb')
data2=f2.read()
a=data[78:120]
b=data2[78:120]
ans=""
for i in range(len(a)):
    ans+=chr(a[i]^b[i]^0xff)
print(ans)

然后成功获得 key 为letsplaychess
那运行run.exe,输入这个 key,之后得到解密后的 file,运行即可得到 flag

Twist1

做了蛮久一个题。需要 32 位环境。
放进 ida 发现没啥东西那也就意味多半是加了壳,工具查不出来,那就手动跟一下把。
很快一通跟之后看到这样一个跳转

便进入了我们正常的程序逻辑。为了更好的后续分析,还是先脱壳。用lordPE转储之后,importREC修复一下 IAT 表,便可以正常运行了。这个题用了很多反调试手段(不过 od 的插件好像帮我绕过了,有好几个地方都没操作就过了)
然后开始跟进。核心分析函数:

跟进之后发现如下:

这里存在利用了setunhandledexceptionfilter进行反调试。
机制是当有调试器附加时,程序中产生异常,则错误会先传递给调试器从而报错。看到上图最后一条adc dl,byte ptr ds:[edx]的指令,这个就是异常的来源。nop 掉之后绕过反调试。
然后在函数0x401220中获取用户输入

获取输入之后来到了关键判断函数0x401240

然后获取ZwQueryInformationProcess函数,那后面自然又是ZwQueryInformationProcess的反调试手段了。

地址0x40727D-0x4072A1ProcessDebugPort(0x7),地址0x4072A8-0x4072C9ProcessDebugObjectHandle(0x1E)

ProcessDebugPort(0x7):进程处于调试状态,系统会为它分配一个调试端口,ProcessInformationClass参数的值为ProcessDebugPort(0x7)。若程序处于非调试状态,则变量dwDebugPort的值设置为 0,若进程处于调试状态,则值设置为0xFFFFFFF.
ProcessDebugObjectHandle(0x1E):调试进程时会生成调试对象,函数的第二个参数值为ProcessDebugObjectHandle,调用参数后就能获取调试对象句柄,进程处于调试状态时,调试对象句柄的值就存在,若进程处于非调试状态,则调试对象句柄值为NULL.

nop 掉这一整段或者在两个jnz的时候拒绝跳转就可以,然后执行jmp Unpack_.00407430

跟进 jmp 指令之后,又是ProcessDebugFlags(0x1F),同样的绕过方式

ProcessDebugFlags(0x1F):检测debug flag调试标志的值也可以判断是否处于被调试状态,函数 的第二个参数设置为processdebugflag(0x1)时,调用函数后通过第三个参数即可获的调试标志的值,若为 0,则进程处于被调试状态,若为 1,则进程处于非调试状态。

这个函数拷贝了输入要留意一下后续用到了

然后进去之后又是GetThreadContext的反调试,阻止一下下图这个jnz即可,比如改成jz或改一下ZF的值之类的都可以。

接下来进行一大堆比较,实际上实在对硬件断点进行反调试,逐个对比寄存器之类的。想办法阻止所有的 jnz 即可。

到此所有的反调试等手段都结束了,接下来就是程序的处理逻辑。不过逻辑可谓是相当凌乱,但是理清之后操作其实很简单,主要是程序跳过去跳过来的,最好是要边跟边记录,而且最好F7跟,不要错过了。这里就不再赘述了,最后跟进之后得到如下结果:

input[0]循环右移6位 = 0x49
input[1]^0x20=0x69
input[2]^0x77=0x35
input[3]^0x21=0x64
input[4]^0x46=0x8
input[5]循环左移4位 = 0x14

最中得到答案RIBENA