0x00 前言

游戏题,主要学学游戏题应该怎么做,以前碰到游戏题都在misc或者压根没做出来(然后没WP)

拿到这题是懵的,没有符号就算了,压根不知道该干什么

兜兜转转搜到了源码PacManX,本来想着有源码自行编译之后拿去BinDiff一下,但是自行编译很多环境是不一样的,所以不行

通过搜索字符串,查找一局游戏结束时的操作,发现也没有特别的,这时候就卡住了

后面查看WP发现原来是放在了读文件的操作上,看到游戏确实有给多四个文件,看来以后做游戏题还得注意文件读写这块(毕竟有存档之类的,这些东西也是可以藏数据的)

0x01 解

无壳,64位,直接上IDA

想直接动调,但是有反调试

通过在import表搜索exit可以找到不少相关调用,一个个看,最后发现在高亮处这个地方很怪

F5一下可以看到对两个字符串进行了判断

这边这个反调试是这样的:

1.用函数CreateToolhelp32Snapshot,参数dwFlags=2th32ProcessID=0,返回值给到变量hSnapshot

2.创建一个PROCESSENTRY32W结构的变量pe,其中dwSize=568

3.调用函数Process32First,传入hSnapshotpe

4.一个循环不断判断pe.th32ProcessID是否是CurrentProcessId,不是则调用Process32NextW,获取下一个进程ID

当判断成立,则令变量th32ProcessID = pe.th32ParentProcessID再次调用CreateToolhelp32Snapshot获取快照,不过这一次参数dwFlags=2th32ProcessID=pe.th32ParentProcessID(挺怪的,dwFlags=2的情况下后面的参数实际上没有用的)

5.又是一个循环,与第4步流程差不多,不过循环要找th32ProcessID的变成了创建当前程序的父进程的ID,返回结构体PROCESSENTRY32W

6.找到后关闭Handler,并且取结构体PROCESSENTRY32W中的szExeFile,之后经过一系列转换(函数wcslenWideCharToMultiByte之类的字符串转换),最终得到当前程序父进程的名字,如果父进程名应为explorer.exe或者powershell.exe则正常,否则退出(这边我觉得挺离谱的,看不起我CMD是吗)

所以过反调试挺简单,最后的一个判断patch掉就行(tab看汇编,jz改为jnz)

(反反调试应该有要研究这个的,继续挖坑)

反调试看完继续看imports表

从读写文件下手,即针对函数freadfwrite下手

第一个位置就很可疑,有一些读注册表的操作

需要注意的是函数sub_7FF6F0EF419A跟进去是一个解密函数,参数是加密的东西,解密过程比较简单

1
2
3
4
5
6
7
8
9
const char *__fastcall sub_7FF6F0EFF3E0(const char *a1)
{
int i; // [rsp+24h] [rbp+4h]

sub_7FF6F0EF4FFF((__int64)&unk_7FF6F0F3A102);
for ( i = 0; i < j_strlen(a1); ++i )
a1[i] = (a1[i] ^ 0x11) - 34;
return a1;
}

根据这个可以找到各字符串的含义

1
2
3
4
5
6
7
8
9
unk_7FF6F0F2E538 = 'Software\PacManX'
unk_7FF6F0F2E530 = 'Error'
unk_7FF6F0F2E54C = 'MYFLAG'
unk_7FF6F0F2E590 = 'game.data'
OldFilename = 'game.tmp'
unk_7FF6F0F2E580 = 'powerpowerpo'
unk_7FF6F0F2E558 = 'powerpowerpowerpowerpowerpowerpo'
FileName = 'game.ps1'
unk_7FF6F0F2E5C0 = 'powershell.exe -File game.ps1'

根据这个可以大致知道会检查一个路径是HKEY_CURRENT_USER\Software\PacManX\MYFLAG的注册表值(长为36Bytes),然后会有一个.ps1文件的出现(从game.tmp改过来的)用于检查flag,这个文件是退出时才会出现,而且用完马上remove

那就新建个注册表值,然后下个断点让它出来

运行到这里能在同目录下找到一个.ps1文件,不过混淆很严重

可以手动去,也可以用工具,这次试试用工具PowerDecode

用法很简单,会点英语就行,人家甚至提供GUI,我哭死

不过最后不会生成plainscript出来,要自己去log里面复制

解出来这个,好像有三层混淆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
function enenenenene {
param(
$plaintextBytes,
$keyBytes
)
# Initialize S and KSA
$S = 0..255
$j = 0
for ($i = 0; $i -lt 256; $i++) {
$j = ($j + $S[$i] + $keyBytes[$i % $keyBytes.Length]) % 256
$temp = $S[$i]
$S[$i] = $S[$j]
$S[$j] = $temp
}

# PRGA and encryption
$i = 0
$j = 0
$ciphertextBytes = @()
for ($k = 0; $k -lt $plaintextBytes.Length; $k++) {
$i = ($i + 1) % 256
$j = ($j + $S[$i]) % 256
$temp = $S[$i]
$S[$i] = $S[$j]
$S[$j] = $temp
$t = ($S[$i] + $S[$j]) % 256
$ciphertextBytes += ($plaintextBytes[$k] -bxor $S[$t])
}

# Return ciphertext as a string
return $ciphertextBytes
}
function enenenenene1 {
param(
$inputbyte
)
$key = @(0x70, 0x6f, 0x77, 0x65, 0x72)
$encryptedText = @();
for ($k = 0; $k -lt $inputbyte.Length; $k++) {
$encryptedText = enenenenene -plaintextBytes $inputbyte -keyBytes $key;
$key = enenenenene -plaintextBytes $key -keyBytes $encryptedText;
}
return $encryptedText + $key;
}
function enenenenene2 {
param(
$inputbyte
)
$key = @(0x70, 0x30, 0x77, 0x65, 0x72)
for ($k = 0; $k -lt $inputbyte.Length; $k++) {
$inputbyte[$k] = $inputbyte[$k] + $key[$k % $key.Length]
}
return $inputbyte;
}
function enenenenene3 {
param(
$inputbyte
)
$key = @(0x70, 0x30, 0x77, 0x33, 0x72)
for ($k = 0; $k -lt $inputbyte.Length; $k++) {
$inputbyte[$k] = $inputbyte[$k] * $key[$k % $key.Length]
}
return $inputbyte;
}
$registryPath = 'HKCU:\Software\PacManX'

$valueName = 'MYFLAG'
$value = Get-ItemPropertyValue $registryPath $valueName
$plaintext = @($value) | ForEach-Object {
$input = $_
$plaintext = @()
for ($i = 0; $i -lt $input.Length; $i++) {
$plaintext += [int][char]$input[$i]
}
$plaintext
}
if ($plaintext.Length -ne 36) {
Set-Content -Path "log.txt" -Value "ERROR"
exit
}
$encrypted1Text = enENenenene2 -inputbyte (enenenENene2 -inputbyte (enenenenene3 -inputbyte (Enenenenene2 -inputbyte (enenenenene2 -inputbyte (enenenenene2 -inputbyte (enenenenene1 -input $plaintext))))))
$result = @(38304, 8928, 43673, 25957 , 67260, 47152, 16656, 62832 , 19480 , 66690, 40432, 15072 , 63427 , 28558 , 54606, 47712 , 18240 , 68187 , 18256, 63954 , 48384, 14784, 60690 , 21724 , 53238 , 64176 , 9888 , 54859 , 23050 , 58368 , 46032 , 15648 , 64260 , 17899 , 52782 , 51968 , 12336 , 69377 , 27844 , 43206 , 63616)
for ($k = 0; $k -lt $result.Length; $k++) {
if ($encrypted1Text[$k] -ne $result[$k]) {
Set-Content -Path "log.txt" -Value "ERROR"
exit

}

}
Set-Content -Path "log.txt" -Value "RIGHT"

enenenenene一眼RC4

其他的都就一些简单运算,直接解就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Cipher import ARC4
enc1 = [38304, 8928, 43673, 25957 , 67260, 47152, 16656, 62832 , 19480 , 66690, 40432, 15072 , 63427 , 28558 , 54606, 47712 , 18240 , 68187 , 18256, 63954 , 48384, 14784, 60690 , 21724 , 53238 , 64176 , 9888 , 54859 , 23050 , 58368 , 46032 , 15648 , 64260 , 17899 , 52782 , 51968 , 12336 , 69377 , 27844 , 43206 , 63616]
def de1(enc):
key = enc[-5:]
enc = enc[:-5]
rc4 = ARC4.new(enc)
key = rc4.decrypt(key)
rc4 = ARC4.new(key)
enc = rc4.decrypt(enc)
print(enc)

def de2(enc):
key = [0x70, 0x30, 0x77, 0x65, 0x72]
for i in range(len(enc)):
enc[i] -= key[i%5]
return enc

def de3(enc):
key = [0x70, 0x30, 0x77, 0x33, 0x72]
for i in range(len(enc)):
enc[i] //= key[i%5]
return enc

de1(bytes(de2(de2(de2(de3(de2(de2(enc1))))))))
# b'73412036-7d8c-437b-9026-0c2ca1b7f79d'

这边有需要注意的是RC4的解密次数,虽然在原函数里面是循环加密了36次,但解密的时候只需要解密一次就行了(应该是有什么数学原因吧)