2023 巅峰极客 m1_read 详细复现

定位逻辑

本题给出了bin文件,即out.bin,故可以猜测其内部包含了加密结果或者密钥等
m1_read文件打开后,函数数量不多,并且静态分析WinMain不可行
于是翻找函数,可以找到形如AES的函数(sub_4BF0)


利用Findcrypt也出现了AES的特征码,于是假定是AES,并且没有魔改
函数接近结尾部分可以看出这是AES-128,一共有16B参与

观察调用AES函数的附近,还有一个异或就没了
寻找AES密钥,未能找到。。。
通过搜索查找,我们发现AES的密钥可以利用DFA进行攻击从而得到

AES加密一共十轮,其中第十轮不进行列混合
DFA需要的数据是,第九轮列混合之前,通过修改一位中间结果,导致加密结果与正确结果有4B不同

获得DFA数据

利用Frida框架,我们可以对AES函数进行hook,从而得到DFS攻击所需的数据

var baseAddr = Module.findBaseAddress("m1_read.exe")
// AES函数偏移:0x4BF0
var whiteAES = new NativeFunction(baseAddr.add(0x4BF0), 'pointer', ['pointer','pointer'])
// count = 9,这样第一轮数据恰好对应正确的数据
var count = 9

Interceptor.attach(baseAddr.add(0x4C2C),{
  // 列混合处的偏移:0x4C2C
  // 若不确定,可以通过测试来看:
  // 		1. 若正确数据与异常数据全部不同,则修改太早
  // 		2. 若正确数据与异常数据仅一个不同,则修改太晚
  onEnter: function(args){
    count++;
    // 第九轮修改中间结果
    if(count == 9){
      // 参考汇编代码可以发现rdi记录第九轮中间数据的地址
      // "add(Math.floor(Math.random()*16))":
      // 		表示对操作的地址进行偏移,值属于{1,2,3}
      // 		(0,4的可能性基本为0)
      //
      // "writeU8(Math.floor(Math.random()*256))":
      // 		表示对写入的值进行修改,值属于{1,2,3,...,E,F}
      // 		(0,16的可能性基本为0)
      // 综上,这实现了对中间数据的单个Byte修改,取消注释下述代码即可看出
      var p = ptr(this.context.rdi)
      // console.log("ori: ", p.readByteArray(16), "\n")
      p.add(Math.floor(Math.random()*16)).writeU8(Math.floor(Math.random()*256))
      // console.log("out: ", p.readByteArray(16), "\n")
    }
  },
  onLeave:(retval) =>{

  }
})

for (let index = 0; index < 33; index++){
  // 取消注释下述代码即可得到每一轮的数据
  var l = Memory.allocAnsiString("0123456789abcdef");
  var b = Memory.alloc(16);
  whiteAES(l, b);
  // console.log(b.readByteArray(16));
  count = 0;
}
import frida
import sys
import subprocess

# Things needed to be changed
currentDir = R"m1_read"
proName = R"m1_read.exe"
dir = R"************CENSORED************\m1_read"

def getJsScript(fileName="t1.ts", fileDir=currentDir) -> str:
  with open(fileDir + "/" +fileName, encoding='utf-8') as jsScript:
    return jsScript.read().rstrip()
def on_message(message, data):
  print(message)

jsScript = getJsScript()
program = dir + "/" + proName
subprocess.Popen(program)

session = frida.attach(proName)
script = session.create_script(jsScript)
script.on('message', on_message)
script.load()
sys.stdin.read()

于是可以得到33轮的数据:首轮是原始结果,之后各轮是经过替换的结果
程序实现还有一步异或加密,异或值为0x66,还原后可以得到

14f5fe746966f292651c2288bbff4609
14f5fe746966f292651c2288bbff4609
14f5fe176966209265e32288c3ff4609
14f5c27469e9f292de1c2288bbff4662
14cafe741066f292651c22d6bbff8409
14f5fe746966f292651c2288bbff4609
14f5fe746966f292651c2288bbff4609
14f5fe746966f292651c2288bbff4609
14f5fe746966f292651c2288bbff4609
14ecfe74fc66f292651c228cbbffae09
144cfe74a566f292651c228fbbff0709
14f58674692cf292391c2288bbff46b0
14c1fe744666f292651c2292bbffbc09
14f5087469bcf292211c2288bbff4661
14f5fe4e69663d92653f2288c6ff4609
14f5fe2669664792657e2288c1ff4609
14f58e746965f292c91c2288bbff4672
cef5fe746966f2f5651ce988bb6a4609
14f5e57469d8f292f41c2288bbff4660
14f5fe746966f292651c2288bbff4609
14f5fe606966a69265af228891ff4609
4af5fe746966f21f651c5188bb9b4609
144bfe740d66f292651c2270bbff3609
14f5687469a4f292181c2288bbff4688
1483fe740066f292651c2241bbff2409
1af5fe746966f27d651c6288bb354609
16f5fe746966f21e651c1488bbd24609
14f5fe4569667d92650422887aff4609
14f542746936f2929e1c2288bbff4623
14f5fe746966f292651c2288bbff4609
14f5ec746950f292cf1c2288bbff469f
140bfe749166f292651c224fbbff4909
14f5197469dff292041c2288bbff46ee
(上述脚本下,每个人的结果只能保证第一个相同)

得到原始密钥

利用phoenixAES可以实现DFA

# s1是前文中的33轮数据,以字符串形式存储(包含换行,否则无法识别)
import phoenixAES
with open("testfile", "wb") as t:
    t.write(s1.encode())

phoenixAES.crack_file("testfile")
# 自动打印出来第10轮的密钥
# B4EF5BCB3E92E21123E951CF6F8F188E

接着,得到首轮密钥,参考https://github.com/SideChannelMarvels/Stark
自行编译项目,可以得到一系列文件,其中我们需要aes_keyschedule.exe

./aes_keyschedule.exe B4EF5BCB3E92E21123E951CF6F8F188E 10

通过指定密钥和它对应的轮数,程序得到了其他每一轮的密钥,我们需要首轮密钥

取得加密数据

知道程序密钥过后,本题只需要先解决最终的异或,然后把AES解密即可
程序的加密数据在out.bin,010editor可以看到一大串数据,然而文件的后面部分都是重复的
于是猜测加密数据在前面,并且由于offset在0x10到0x1F的数据是0x0,我们猜测第一行或者第三行是加密数据

解密脚本

from Crypto.Cipher import AES

// 经过测试,第一行数据得到的不符合题意
# s = "ADB6A0F04B08040001FC08773BB0DB16"
s = "0B987EF5D94DD679592C4D2FADD4EB89"
# key长度注意要写为16B的长度,否则AES识别不正确
key = bytes.fromhex("00000000000000000000000000000000")
enc = bytearray(bytes.fromhex(s))
cipher = AES.new(key, AES.MODE_ECB)

for i in range(16):
  enc[i] ^= 0x66

flag = cipher.decrypt(enc)
print(flag)
# flag => cddc8d28dabb4ea9
posted @ 2023-10-14 21:04  Carykd  阅读(47)  评论(0编辑  收藏  举报