2021 天翼杯 bbgo Writeup
阅读量343001
|
发布时间 : 2021-10-25 16:30:42
x译文声明
本文是翻译文章
译文仅供参考,具体内容表达以及含义原文为准。
![]()
- 关键算法: 位运算
- 出处: 2021 天翼杯
- 工具: IDAPython
- 考点: Golang 恢复符号, 文件解密
- 解题技巧: 爆破
分析
恢复符号
- 这题恶心在 IDA 7.6 没法恢复符号,估计是出题人魔改了 Golang 编译器的源码或是 patch 了文件
- (我更倾向于魔改了源码,因为 Golang 初始化 runtime 的时候会对自身的 moduledata 等进行校验,patch 不太容易做到)
- 所以只能根据 Golang 恢复符号的原理手动进行恢复了,推荐这篇 Blog
- 还原本题符号的脚本如下
import idc
import ida_bytes
functable_addr = 0x4EFA20
funcstruct_addr = 0x4F4BE8
funcname_addr = 0x4BC920
ea = functable_addr
while ea < funcstruct_addr:
f_addr = idc.get_qword(ea)
fs_addr = idc.get_qword(ea+8)+functable_addr
print(hex(f_addr), hex(fs_addr))
name_addr = idc.get_wide_dword(fs_addr+8)+funcname_addr
idc.create_strlit(name_addr, idc.BADADDR)
size = ida_bytes.get_max_strlit_length(name_addr, idc.AF_ASCII)
name = idc.get_bytes(name_addr, size).decode()
idc.set_name(f_addr, name, idc.SN_NOCHECK)
ea += 16
- 然后发现绝大多数函数都能恢复了,但是
main 函数还是恢复不了,不过交叉引用一下 os_ReadFile() 就能定位到了
梳理主要逻辑
- 上来就是一个
if 判断,大胆猜测这是在比较 argc ,所以这题逻辑就是通过 argv[] 给出待加密的文件和加密文件名
- 这时候使用 findcrypt 搜一下,
main 函数里面识别出了一个 CRC_table ,推测是上来对要加密的文件生成了一个校验和
- 之后这里就要进行大量的动调了,基本就是在每次调用函数之前下个断点,函数返回之后下一个断点,用于确定函数调用的传参和返回值(参数一般通过各种寄存器传递,返回值一般就是在
rax, rcx 这些寄存器里面
- 这么做是因为 Golang 的调用约定实在是混乱,只靠 IDA 的静态分析根本没法正确识别参数和返回值
- 通过动调,可以判断出
sha1.Write() 传入的参数是栈里面的常量和 CRC 拼接起来的 bytes,然后运行 sha1.Sum() 对其进行 Hash
- 接着动调。(注意这里无需在每个函数处都下断点,因为那些
runtime 开头的函数都是 Golang 底层的一些函数,不太重要,后面需要重点关注的两个函数就是 sub_4830E0 和 sub_483580 )
- 动调到
sub_4830E0 发现传入了刚刚 Hash 操作产生的 0x14 个 bytes,调用后在 r8 寄存器里可以发现新生成了 0xb0 个 bytes,感觉这地方是个密钥生成的函数,重命名为 genKey()
- 接着动调
sub_483580 ,发现这里头就是传入了 16 bytes 的明文和上面新生成的 bytes,因此感觉这就是加密函数了,重命名为 enc()
分析 genKey 函数
- 函数乍一看非常复杂,所以一定要动调,静态调试谁也看不懂
- 这部分感觉是值拷贝的过程,动调可以确定是把传入的
digest 数组 copy 了一份
- 然后调用
runtime.makeslice 初始化了一个数组
- 这里又是一个循环赋值操作,对比左值右值,推测应该是进行了
v47[n] = v47[n-1] + const 这样的操作,v47 就是刚刚初始化的数组,所以感觉它可能就是 keys
- 这部分就是主要的密钥生成逻辑了,不好意思,只能硬看了
- 得到密钥生成的算法如下(包括
hash 部分)
func genKey(guess1, guess2 byte) []uint32 {
var length, i, a, b, c, d, lll, index uint32
sha := sha1.New()
seed := []byte{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, guess1, guess2}
sha.Write(seed)
rawDigest := sha.Sum(make([]byte, 0))
length = uint32(len(rawDigest))
digest := make([]uint32, 0x14)
for i = 0; i < uint32(len(rawDigest)/4); i++ {
digest[i] = binary.LittleEndian.Uint32(rawDigest[4*i : 4*(i+1)])
}
keysLen := 2*length + 4
keys := make([]uint32, keysLen)
keys[0] = 0x0BE38A40C
for i = 1; i < 2*length+4; i++ {
keys[i] = keys[i-1] + 0x48C20B1A
}
lll = uint32(3 * keysLen)
a = 0
b = 0
c = 0
d = 0
for index = 1; index <= lll; index++ {
keys[a] = bits.RotateLeft32(keys[a]+b+c, 3)
tmp1 := keys[a]
tmp2 := c + tmp1 + digest[d]
tmp3 := tmp1 + c
tmp4 := bits.RotateLeft32(tmp2, int(tmp3))
digest[d] = tmp4
c = digest[d]
tmp5 := (a + 1) % (2*uint32(length) + 4)
a = tmp5
d = (d + 1) % 4
b = tmp1
}
return keys
}
分析 enc 函数
-
enc 函数相对 genKey 来说要简单一些,如果你能分析出 genKey 的逻辑,那么分析 enc 肯定就没问题了,不断动调即可
- 得到
enc 函数的逻辑如下
func enc(crc1, crc2 byte) {
rawPlain := []byte{0x1e, 0x26, 0x2e, 0x20, 0x2b, 0x2d, 0x2f, 0x1c, 0x29, 0x22, 0x1d, 0x1f, 0x21, 0x28, 0x23, 0x27}
plain := make([]uint32, len(rawPlain)/4)
keys := genKey(crc1, crc2)
cipher := make([]uint32, 4)
length := 0x14
for i := 0; i < len(rawPlain)/4; i++ {
plain[i] = binary.LittleEndian.Uint32(rawPlain[4*i : 4*(i+1)])
}
var p, q, m, n uint32
p = plain[0]
q = keys[0] + plain[1]
m = plain[2]
n = keys[1] + plain[3]
for index := 1; index <= length; index++ {
tmp1 := bits.RotateLeft32(q*(2*q+1), 7)
tmp2 := bits.RotateLeft32(n*(2*n+1), 7)
tmp3 := bits.RotateLeft32(tmp1^p, int(tmp2))
c1 := bits.RotateLeft32(tmp2^m, int(tmp1))
p = q
q = keys[2*index+1] + c1
m = n
n = keys[2*index] + tmp3
} // b->a, d->c
cipher[0] = keys[2*length+2] + p
cipher[1] = q
cipher[2] = keys[2*length+3] + m
cipher[3] = n
}
- 然后就是对其进行逆向操作,这里尝试使用
z3 ,但是由于本题需要爆破,因为效率原因遂放弃,后来意识到因为 rol 是循环左移,不会丢失原来 uint32 中的 bits ,所以可以倒推回去
func dec(crc1, crc2 byte, realCipher []uint32) []byte {
keys := genKey(crc1, crc2)
cipher := realCipher
length := 0x14
var a, b, c, d, c1, tmp1, tmp2, tmp3 uint32
plain := make([]uint32, 4)
d = cipher[3]
c = cipher[2] - keys[2*length+3]
b = cipher[1]
a = cipher[0] - keys[2*length+2]
for index := length; index >= 1; index-- {
tmp3 = d - keys[2*index]
d = c
c1 = b - keys[2*index+1]
b = a
tmp1 = bits.RotateLeft32(b*(2*b+1), 7)
tmp2 = bits.RotateLeft32(d*(2*d+1), 7)
a = bits.RotateLeft32(tmp3, -int(tmp2)) ^ tmp1
c = bits.RotateLeft32(c1, -int(tmp1)) ^ tmp2
}
plain[0] = a
plain[1] = b - keys[0]
plain[2] = c
plain[3] = d - keys[1]
bytesPlain := []byte{}
for i := 0; i < 4; i++ {
tmp := make([]byte, 4)
binary.LittleEndian.PutUint32(tmp, plain[i])
bytesPlain = append(bytesPlain, tmp...)
}
return bytesPlain
}
解题
爆破未知的 CRC
- 现在所有部分都逆明白了,这时候需要爆破两个 byte 的 CRC
- 由于被加密的文件是 PNG 图像,所以它的头部是有特征的,这里就看解密的结果是不是存在
PNG 三个字符就行了
- 写出爆破函数
func bruteCRC() (uint8, uint8) {
var a, b int
var crc1, crc2 uint8
file, _ := os.ReadFile("flag.png.enc")
realCipher := make([]uint32, 4)
for i := 0; i < 4; i++ {
realCipher[i] = binary.LittleEndian.Uint32(file[4*i : 4*(i+1)])
}
wg := sync.WaitGroup{}
runtime.GOMAXPROCS(runtime.NumCPU())
for a = 0; a <= 0xff; a++ {
for b = 0; b < 0xff; b++ {
t1 := a
t2 := b
wg.Add(1)
go func() {
plain := dec(byte(t1), byte(t2), realCipher)
if strings.Contains(string(plain), "PNG") {
crc1 = uint8(t1)
crc2 = uint8(t2)
}
wg.Done()
}()
}
}
wg.Wait()
return crc1, crc2
}
- 爆破得到两个 bytes 是
179, 136
解密文件
- 解密文件就是很常规了,不再赘述
func fileDec(crc1, crc2 uint8) {
file, _ := os.ReadFile("flag.png.enc")
realCipher := make([]uint32, 4)
plainBytes := []byte{}
for i := 0; i < len(file); i += 16 {
for j := 0; j < 4; j++ {
realCipher[j] = binary.LittleEndian.Uint32(file[i+4*j : i+4*(j+1)])
}
tmp := dec(crc1, crc2, realCipher)
plainBytes = append(plainBytes, tmp...)
}
os.WriteFile("flag.png", plainBytes, 0777)
}
- Flag 如图所示
总结
- 文件加密题里面经常有爆破若干字节的套路
- Golang 的动调真的很重要很重要
商务合作,文章发布请联系 anquanke@360.cn
本文转载自:
如若转载,请注明出处:
安全KER - 有思想的安全新媒体
分享到:
r4ve1分享到:
发表评论
您还未登录,请先登录。
登录