2025SPC电子取证竞赛决赛

一、服务器部分

我发现学校的比赛几乎就是没有关联性的,计算机和服务器是割裂的,没什么联动内容

1、分析服务器磁盘镜像,其源盘SHA256值是?(答案格式:大写)

源盘的SHA256直接扔到火眼取证分析里边计算哈希就好了

这边直接选择SHA256就好了

得到答案:EDE7DBDDA6392FFDE1F3F83D707FC694760E52774AF80950EC329335F5DF1CDC

2、分析服务器磁盘镜像,所使用的操作系统版本是?(答案格式:0.0.0000)

在基本信息的系统信息即可看见

这边有操作系统的版本

可以看到是7.9.2009

3、分析服务器磁盘镜像,频繁登录服务器的IP地址是?(答案格式:127.0.0.1)

查看登录日志,这边看看登录ip

会发现最频繁的就是这个192.168.51.1了

答案就是这个

4、分析服务器磁盘镜像,宝塔面板中Nginx提交安装的时间是?(答案格式:2025-01-01 00:00:00)

如果在文件界面看的话在这个路径下:/www/server/panel/data/db

所以是2025-10-23 23:27:20

另外其实在分析阶段也有

也可以直接看,其实是一模一样的,毕竟源文件是在那边

5、分析服务器镜像,虚拟镜像文件挂载的路径是?(答案格式:/abc/abc)

问挂载的路径,那挂载肯定是需要mount命令的,我们去历史命令看看有没有

发现的确存在mount命令,挂载的镜像文件

1
mount -o loop disk.img /mnt/mysql/

是在把disk.img这个磁盘映射文件当作块设备来挂载到了/mnt/mysql路径下

所以答案是/mnt/mysql

(或许做别的也可以看看mount的情况,但是这边是看不见的,是只有在历史记录才有)

6、分析服务器磁盘镜像,Docker配置了阿里云专属加速地址,其ID是?(答案格式:90abcdef)

想知道加速器配置,当然需要看docker的服务配置文件了

这个文件一般都是/etc/docker/daemon.json

所以我们过去看看

就看见了这题的答案,也就是阿里云专属加速地址的ID

36oqbgbk

7、分析服务器磁盘镜像,服务器中MySQL容器的数据持久化目录是?(答案格式:/xxx)

这一题同样藏在了历史命令里,这一块还是很重要的,以后应该着重加以分析

在这边可以看到这样子一行命令

1
docker run -d --name mysql8 -p 3060:3306 -e MYSQL_ROOT_PASSWORD=Jsai8amKK23 -v /data:/var/lib/mysql --restart=unless-stopped mysql:8.0

意思就是docker运行了一个mysql容器,然后呢是把主机/data目录给挂载到了容器的/var/lib/mysql来确保数据持久化,容器删了之后数据也不会丢失

所以在服务器里是存储在主机的/data目录下的,所以答案是/data

同时在这边我们还能看到有趣的东西,比如说mysql的密码,明文存储在了历史命令里:Jsai8amKK23

SSH连接

为了方便做题,SSH连接一下还是很有必要的

我们ifconfig看看IP先

是192.168.100.138,因为我NAT设置的就是这一段

直接连上就好了,就能直接用了

但是这一种直接连上就能用是因为你在火眼仿真更改了连接方式,这边同时有两个好处,一个是你在火眼改完NAT后自动配置了网关,另一个就是重启了网络

如果你没选NAT连接,那么就会像下图一样发现连接不上,因为网关没配置

但是有的人也发现了自己手动调成了NAT后仍然不能ssh连接

这其实是因为这个ens33接口是启动时不激活的导致的,在火眼下图也可以看见,是分析的出来的

面对这种情况就需要我们手动启动一下ens33接口了

1
sudo nmcli connection up ens33

在启动之后就会发现能登了

当然了,如果不会,在这边随便加一张ens37的网卡,也会发现能用ens33接口ssh连接了,这主要也是因为加入网卡的时候触发了网络重置,ens33接口重新获取DHCP时得到了正确的配置

大概是这样子

总之我们是成功连接上ssh了

8、分析服务器磁盘镜像,服务器中使用的MySQL镜像ID是(回答前五位)?(答案格式:xxxxx)

直接看发现压根没加载,然后ls之后发现了在/root目录下就存在着一个mysql8.tar的文件

我们只需要加载一下这个镜像到docker就能看到其镜像id了

1
docker load -i mysql8.tar

Docker镜像的ID默认就是这个镜像内容所有层和数据通过SHA256算出来的一个值

所以镜像其实就是Loaded image ID: sha256:34178dbaefd067c5997133cbdef31f164aa899689394f70b065725afb7aa322a

下边只有一小串主要还是便于展示而已

题目为了避免在这边搞混,就只问了前五位,所以就是34178,还是很具体的

9、分析服务器磁盘镜像,店铺网站的备份数据库文件名是?(答案格式:xxx.sql)

在第五题我们就知道了挂载的镜像文件在/mnt/mysql

那很明显挂载的是sql文件啊,也就是数据库文件嘛

那我们自己也挂载一次然后就能看见这个备份数据库文件了

命令直接仿照原来的就好,直接照抄历史命令

1
2
3
mount -o loop /home/disk.img /mnt/mysql/
cd /mnt/mysql
ls

成功得到备份数据库文件名——agfk.sql

当然你如果什么都不知道,只能去暴搜了

在宝塔里直接包含子目录在根目录暴搜,能搜到这个sql文件

10、分析服务器磁盘镜像,店铺网站使用的数据库名称是?(答案格式:abc)

上一题拿到了备份数据库了已经,打开来看看

发现这数据库就叫这个——agfk

重构网站

我们来重构一下网站再往下做

首先是要创建mysql容器,把数据库先搭起来

1
2
3
docker load -i mysql8.tar  #导入docker镜像/root/mysql8.tar
docker tag 34178dbaefd0 mysql:8.0 #修改TAG标签
docker run -d --name mysql8 -p 3060:3306 -e MYSQL_ROOT_PASSWORD=Jsai8amKK23 -v /data:/var/lib/mysql --restart=unless-stopped mysql:8.0 #创建mysql容器,历史命令中一致

其实是和历史命令一样

这边有个有意思的点,就是在修改tag的时候我们取了前12位就替代了整个64位哈希ID

这是Docker为了方便用户操作设计的短ID设计(当然了,短ID不一定要12位,能识别就行)

只要能唯一标识就行,如果正正好好出事儿了,会报错的,重新写长点就好,问题不大

成功要运行完之后我们就可以在navicat看了,密码暴露在了命令里Jsai8amKK23

注意一下这边命令里有3060:3306,对端口做了映射

其实是少了个数据库的,我们的agfk数据库不在里边,我们导入一下

这边不能直接运行sql文件,因为agfk是单独的一个数据库,不属于任何别的数据库,如果你写到别的数据库里就会发现出现一系列报错,说你外键约束名称重复什么什么的,本来就不属于那边主要是

我们应该在这边先直接新建数据库

能在sql文件里看到具体的设置

然后确定新建

一定要在新建好之后再去运行喔

这样子导入的话就能成功了

这边的数据库重建是很关键的,否则agfk店铺网站的配置文件指向agfk数据库的时候会显示找不到这玩意,然后 就进不去了,直接404了

接下来是宝塔,火眼已经分析出来了这服务器有个宝塔面板了,所以我们可以直接先看看宝塔情况然后启动宝塔

跟着他说的做就好了,然后bt default启动宝塔

我们bt 5改一下密码,改成123456即可

然后上宝塔

这次的宝塔比初赛的正常多了

接下来就是一系列的恢复,首先是恢复网站

我们查看网站文件夹,发现干净的很,被删了

所以我们需要找一下有没有备份文件

最后在回收站里找到了

恢复一下,然后直接解压就好了

内容都回来了

我们只需要去hosts加一下静态映射就能重构网站成功了

加好静态映射了,接下来打开网址即可上去(这边必须关闭系统代理,否则会进入这个域名出售界面,他们接管了DNS解析使得我的本地hosts文件都没能生效)

最后即可进入两个网站,这是店铺网站

这是收银网站

重构完毕

11、分析服务器磁盘镜像,店铺网站分配的白班营业账号是?(答案格式:abc@abc.com

看看白班营业账号,店铺网站首先确定就是agfk了(说起来这全文都没怎么要我们的收银网站啊)

这一题很明显是需要我们去数据库翻的

agfk网站,去agfk网站翻很合理吧

点入管理的表acg_manage

然后在type一类单击,可以看到注释,写了白班是type=2

所以账号就是邮箱,也就是lisa的这个lisa@outlook.com

12、分析服务器磁盘镜像,店铺网站数据库中控制管理账号状态的字段是?(答案格式:xxx)

继续遍历看看注释

可以看到是status这一列,所以字段为status

13、分析服务器磁盘镜像,店铺网站多次进行清理无效订单操作,最近一次删除的订单,其订单号是?(答案格式:123456789012345678)

清理无效订单操作,那基本上就是在更改数据库了

如果启用了二进制日志,那我们是可以通过分析binlog来找到这个删除订单的操作的

搜索一下binlog在哪里

我们可以全部提取出来然后利用mysqlbinlog文件进行转化,转成sql文件,虽然456看起来文件看起来就很小,不太可能在那里边(我们需要MySQL8.0的工具才能转化)

1
2
3
mysqlbinlog --no-defaults -vv --base64-output=DECODE-ROWS binlog.000001 > 1.sql
mysqlbinlog --no-defaults -vv --base64-output=DECODE-ROWS binlog.000002 > 2.sql
mysqlbinlog --no-defaults -vv --base64-output=DECODE-ROWS binlog.000003 > 3.sql

转换完成后进行分析,我们定位Delete_rows事件,发现最后一次出现是在binlog.000003 > 3.sql里

这边可以看到订单号,所以答案就是这个

569251024012225515

14、分析服务器磁盘镜像,店铺网站的后台访问路径是?(答案格式:/xxx)

在README.md写明了默认的后台就是/admin

而且在/www/wwwlogs/www.agfk.xyz.log文件里都能看到就是这个admin文件,比较常见,很明确了

实在不行你上去看一眼其实也好

进入了后台界面,所以就是/admin

15、分析服务器磁盘镜像,假设密码为123456ABCD,Salt为abcd,根据店铺网站的后台管理员密码加密算法计算,其密文为?(答案格式:7cadd4dce72ae04edfe582de6620980ce5b881ef)

就是找后台管理员密码的加密算法

我们在/admin界面看到了大概路径

所以大概在一个authentication文件里

这边可以发现似乎没什么内容,我们在同路径下看看manage.php有没有什么信息

看到了加密逻辑,定位一下这个generatePassword在干什么

也可以直接文件内容搜索定位过去

霍,先各自算md5,然后拼接起来算md5,再算sha1

写个python代码,差不多就这样子

1
2
3
4
5
6
7
8
9
10
11
12
import hashlib

def custom_hash(password: str, salt: str = "") -> str:
md5_pass = hashlib.md5(password.encode('utf-8')).hexdigest()
md5_salt = hashlib.md5(salt.encode('utf-8')).hexdigest()
combined = md5_pass + md5_salt
second_md5 = hashlib.md5(combined.encode('utf-8')).hexdigest()
final_hash = hashlib.sha1(second_md5.encode('utf-8')).hexdigest()

return final_hash

print(custom_hash("123456ABCD",'abcd'))

将题目说的要求填入

得到答案4cd2209c3377622f3dc35e56842f750d2e53cba5

16、分析服务器磁盘镜像,店铺网站曾使用过域名是?(答案格式:baidu.com)

曾经用过的域名当然是要看日志了

我们是192.168.100.1,所以前边那些192.168.51.1的就是原来的历史日志

这边可以看到曾经的域名是yyfaka.com

17、分析服务器磁盘镜像,店铺网站控制台显示的交易金额是多少?【所有数据】(答案格式:1000)

都问控制台了,明显要登录后台了

先登录后台吧

登录后台

管理账号是第一个,然后在这边可以看到它的盐值

利用上边的密码生成算法

生成123456对应的内容

把ba2cb05caa25f3798840a156e2794ff43485f39d替换掉原来数据库的密码一栏

显示停用,修改status

从0改成1即可

成功进入后台

交易金额切换到所有数据后显示为13454

18、分析服务器磁盘镜像,店铺网站上架的商品数量是?(答案格式:1)

后台一眼19

当然了,在前台把这三类的上架内容都数一下也会发现是19,也能做(因为问的是上架商品,要是问你总商品就不难这样子做了)

二、计算机部分

1、分析计算机镜像,确认源盘MD5值的后6位为?(标准格式:abc123)

和服务器的题目是一样的,就是直接计算哈希就好了,这边显示MD5

3CABA907CAF446CE053A88D5F30FC165

2、分析计算机镜像,确认涉案计算机磁盘的分区形式(标准格式:ABC)

在X-Ways打开看看技术细节报告

可以看到分区形式就是MBR

3、涉案计算机曾被远程控制,请确认对方外网IP为?(标准格式:192.168.1.1)

先看看有没有远控软件,todesk向日葵什么的

发现有向日葵

这边可以直接看发起计算机的外网IP

得到答案为39.171.228.53

4、分析计算机镜像,计算机曾使用xshell连接一服务器,请确认服务器地址为(标准格式:192.168.1.1)

直接看这边的Xshell分析似乎并没有服务器地址

但我们可以在回收站看见Xshell的.xsh文件

打开来看看

看到了CONNECTION的host

所以答案为16.154.233.65

5、分析计算机镜像,查询计划任务【User_Feed_Synchronization】的每天触发时间?(答案格式(北京时间):12:12)

问这个,那咱直接搜这个就是了

这边有点坑,这个时区不对

我们打开来看看源码,会发现末尾有个Z,也就是Zulu time,即UTC时区

那北京时间还得+8,所以是14:28

6、分析计算机镜像,涉案计算机中宝塔面板的远程桌面端口号为?(标准格式:123)

这边其实直接就有哇(),就是3389

当然上宝塔也能看了

其实是一样的启动方式,直接在本地127.0.0.1:8888打开就好啦

其实是一样的,火眼秒了

7、分析计算机镜像,【微信】应用的版本号为?(标准格式1.1.0.0)

又是个诈骗,这边写的是3.3.0.93,但是这是错的

你仿真看就会发现真正的版本号是3.3.0.0

8、分析计算机镜像,请确认计算机曾登录过微信id为?(标准格式:abc_123)

终于火眼出的是对的了

wxid_otbos7bx3rz322

9、分析计算机镜像,当事人使用的微信绑定的手机号为?(标准格式:15012341234)

火眼这边直接就有

手机号为17826837915

但是比赛的时候0人解出来,我认为应该是赛事平台的答案设置有问题(

10、分析计算机镜像,请确认涉案同伙【王一恒】所在的地址为(标准格式:杭州)

这边比赛的时候没找到,到处翻没找到

其实藏在手机备份里边

在360压缩里是可以看见最近记录的

这边能看到备份密码.docx

所以备份密码是DGbw2022

现在提出来文件合集分析即可

填入密码

我们就可以在微信好友列表找到这个人,省份为浙江(索引搜索定位更快)

11、分析计算机镜像,确认涉案人员【傅楠青】的手机号码的后四位(标准格式:1234)

在回收站中存在一个contact.db的数据库文件,十分可疑(我比赛怎么就没看见)

利用DB browser打开

即可发现傅楠青此人

手机号为13995455792

12、分析计算机镜像,确认涉案过期商品【兰蔻青春优氧精华】专柜价(标准格式:123)

我们搜索兰蔻能找到专柜报价的xls表,但是打不开,需要密码

这个密码藏的地方有点太阴了

在广告海报上边。。。

就直接在图上,我都没想到过在图上,没打开来看

所以密码是102431#(专门加的#防爆破是吧)

打开来就能看到价格是720

13、分析计算机镜像,确认涉案网站后台admin11账户登录的密码为(标准格式:adc123)

既然说是网站自然要去浏览器里瞄一眼了

密码管理器是必看的地带

这边可以发现密码是admin123

14、分析计算机镜像,确认计算机Bitlocker加密磁盘恢复密钥的后6位为?(标准格式:111111)

火眼太厉害了直接发现了

所以后六位为333927

正常做的话就是打开隐藏文件

勾选这个,然后就能看见多了一个bl文件

得到答案,顺便可以把加密的F盘恢复了

15、分析计算机镜像,统计涉案表格中“已结清”的“累计收款总金额”为?(标准格式:123)

F盘在上一题恢复了,接下来看看表格

就是这一题的内容

我们过滤已结清然后算累计收款金额即可

1
2
3
4
5
6
7
8
9
import pandas as pd
df = pd.read_excel('销售数据报表.xlsx', sheet_name='主营业务收入')

status_col = '状态' # 状态字段
amount_col = '累计收款金额' # 收款金额字段

total_amount = df.loc[df[status_col] == '已结清', amount_col].sum()

print(f"累计收款总金额(状态为'已结清'的记录):{total_amount}")

(Excel里直接算也一样,不麻烦)

所以最后得到累计收款总金额为996009

16、分析计算机镜像,确认应用【VeraCrypt】的来源,确认对方MAC地址(标准格式:00-16-EA-AE-3C-40)

在飞鸽传书的讨论群里我们可以找到这个VeraCrypt的来源

是大飞发的

然后在好友列表就能定位他的MAC地址了

4C-1D-96-9C-FB-2B

17、分析计算机镜像,其中隐藏了一个VeraCrypt加密容器文件,该加密容器中存放的图片文件名为(标准格式:abc.txt)

我们在火眼选择文件全显

这边大小倒序,我们很容易定位到这一个VC容器

600MB的,特征还是比较明显

接下来就找密码了

在WPS历史这边发现了一些文件,内容不多我们一个个看

最后发现了VC密码藏在排毒养颜宣传语这边

虽然直接搜也能搜到有个VC密码。。藏在两个地方也不知道何意味,降低难度吧

打开来就一个jpg文件,也就是这一题的答案

b7e1d0556148fe0403bc4e357666494.jpg

18、分析计算机镜像,确认涉案公司的成立时间(标准格式:2022年01年01日)

刚刚那个jpg在营业信息里,还打不开,明显有猫腻啊

我们打开看看

明显是文件头缺失,我们修一下

然后就能打开来了

所以成立时间是2019年10月11日

三、EXE二进制

1、该木马启动程序是一个exe,请给出这个exe程序的md5值?(答案格式:5254d73376cd8e2319e300b56d9118a3)

这个就很简单了,直接查看哈希值即可

所以答案就是6f05c7139f30b31ee958e6c094d0a937

2、分析该木马,给出该exe程序的动态链接库数量(答案格式:1)(没原题不记得名儿了)

有点尴尬,给的wp上边没这题,我也不记得原题是啥了,但记得大概是问了这样子一个问题

放Dependencies就能看见了

我记得我做对了,能看见这边是10个dll文件

所以答案是10

3、分析该木马,解密hack.edata的代码是在哪个文件中?(答案格式:xxx.so)

问解密hack.edata的代码在哪个文件,我们就先定位一下edata

在010 editor使用多文件查找

以unicode的编码搜索edata后发现了这玩意在libcef.dll里

所以答案就是libcef.dll

4、 接上题,解密hack.edata的代码所在的函数名称是什么?(答案格式:xxxxx)

在die打开上一题的dll文件,发现是32位的

利用32位ida打开查看

根据上一题得到的地址搜索过去0x2F314A

在这边,我们利用交叉引用索引回去(按x)

过去之后反编译拖到最上边即可看到函数名字

得到函数为cef_enable_highdpi_support

5、分析该木马,成功解密hack.edata文件后,该文件内有一个函数功能是从服务端接收指令,这个函数的名称是什么?(答案格式:xxxxx)

这一题根据题目,成功解密hack.edata文件后,该文件内有一个函数功能是从服务端接收指令

所以我们应该要先去解密这个文件,并且解密出来的还是个有函数的,很可能是dll动态链接库

根据上一题我们已经知道了解密函数是cef_enable_highdpi_support这个函数

我们先确认一下,hack.exe里有没有真的调用这玩意

确认函数调用

在IDA打开一下,然后在/View/Open subviews/Imports看一眼导入的内容

发现确实是调用这个了

我们明白,解密的入口并不在hack.exe,而是在第三题确定的libcef.dll中

所以接下来我们前去libcef.dll进行分析(全程IDA32)

确认libcef为伪装后的木马

来看看导出表

发现这个dll的确导出了cef_enable_highdpi_support这个函数,但是同时也导出了很多奇奇怪怪的随机字符串,这说明了这个dll不是正常的CEF的libcef.dll,因为不是那般规整

明显是假装的啊,是个假装的木马DLL(其实刚刚解压就被火绒摁住杀掉了)

接下来我们继续往下,打开这个函数查看

发现对VyVA6n的判断

首先是在开头看见了一个判断,我们步进sub_40ACB0看看

这是一个判断的函数

1
2
3
4
5
a1:要搜索的字符串指针(指向字符串内容,长度存储在 a1-4的位置)

a2:目标字符串指针(同样有长度前缀)

a3:搜索起始偏移量(以字符为单位)

但是反编译的内容似乎跟这个是对不上的,少了参数啊,反编译似乎发病了,我们只能看汇编

这下正常了

真正的调用应该是sub_40ACB0(“VyVA6n”,v16[1],1)这才对啊

也就是说我们要搜索的子串是VyVA6n,判断完这玩意再往后走

(如果我们能在字符串搜到肯定就更方便了,但是这题把这玩意首先是用的UTF-16LE宽字符存的,然后还放在代码段里,IDA不把这玩意当字符串收集,要不仔细根本是看不见这玩意的)

发现keystr构造方式

那判断这玩意干啥呢?继续往后看,我们就发现了关键函数sub_503CB8

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
int __usercall sub_503CB8@<eax>(int a1@<eax>, int a2@<edx>)
{
int v2; // ecx
int v3; // eax
int v4; // ebx
int v5; // eax
int v6; // edi
int v7; // esi
unsigned int v9[6]; // [esp-Ch] [ebp-2Ch] BYREF
int v10; // [esp+Ch] [ebp-14h]
int v11; // [esp+10h] [ebp-10h]
int v12; // [esp+14h] [ebp-Ch]
int v13; // [esp+18h] [ebp-8h] BYREF
int v14; // [esp+1Ch] [ebp-4h]
int savedregs; // [esp+20h] [ebp+0h] BYREF

v10 = 0;
v13 = 0;
v12 = a2;
v14 = a1;
sub_409918();
v9[2] = (unsigned int)&savedregs;
v9[1] = (unsigned int)&loc_503DA3;
v9[0] = (unsigned int)NtCurrentTeb()->NtTib.ExceptionList;
__writefsdword(0, (unsigned int)v9);
sub_409834(v9[0]);
v3 = v14;
if ( v14 )
v3 = *(_DWORD *)(v14 - 4);
v4 = v3 % 256;
v5 = v14;
if ( v14 )
v5 = *(_DWORD *)(v14 - 4);
v6 = v5;
if ( v5 > 0 )
{
v7 = 1;
do
{
v11 = (v4 ^ *(unsigned __int16 *)(v14 + 2 * v7 - 2)) % 62;
sub_40A704(v11, *((unsigned __int16 *)&off_503DC0 + v11));
sub_40A8D8(&v13, v10);
v4 = (*(unsigned __int16 *)(v14 + 2 * v7++ - 2) + v4) % 256;
--v6;
}
while ( v6 );
}
sub_409C14(v2, v13);
__writefsdword(0, v9[0]);
sub_409834(&loc_503DAA);
return sub_409894();
}

这玩意在循环处理每个宽字符(16位):

  1. 计算索引(当前状态v4 XOR 字符值) % 62
  2. 调用加密函数sub_40A704使用off_503DC0表中的值
  3. 更新状态sub_40A8D8更新v13
  4. 更新v4(字符值 + v4) % 256

算法特点:

  • 处理的是宽字符字符串(偏移量2*v7)
  • 使用62个元素的查找表(off_503DC0
  • 包含反馈机制:每个字符处理结果影响下一个字符
  • 使用了模运算(% 62, % 256)

也就是说,这个503CB8的函数从hack.VyVA6n里读出了字符串,输出了suffix

1
2
3
4
5
6
7
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"  # 62
seed = len(src) & 0xFF
suffix = ""
for ch in src:
suffix += alphabet[(ord(ch) ^ seed) % 62]
seed = (seed + ord(ch)) & 0xFF
return suffix
  • 用源文件内容:
  • src = "HackHackHaHaHaHa"
  • 会得到:
  • suffix = "a5g5VyVA6n1SYB1g"

我们在汇编里能看到这样子一串:

1
2
3
mov edx, 0x006F3D64 (UTF-16 的 "EIP")
mov ecx, [suffix]
call 0x40A930

所以key是这样子来的,用sub_40A930拼出来了’EIP’+suffix

再由sub_40A2C4转字节

所以最终 key 构造是:

  • **key = ****"EIP" + s18**
  • 即:**EIPa5g5VyVA6n1SYB1g**

总结文件作用

这不巧了,那我们是终于看明白这给的几个文件都是干啥的了

分析RC4流解密

接下来继续往下一个个看函数

能看到大概的解密流程,但是由于没有符号表,因此看的比较难受,全是sub函数

全程大概是个RC4的流密码,就是在解密.edata文件

sub_504084正是RC4流解密的核心函数

先进行参数的检查,传入的参数有a1,a2和a3,这边的a3没用到

a1是输入/输出数据缓冲区、a2是密钥缓冲区

这俩地方都获取了缓冲区,是C++动态数组的典型布局

接下来就是经典的RC4流解密三步了

1,初始化S盒

1
2
3
4
5
6
// 初始化 0-255
v9 = 0;
v10 = v30; // v30 是256字节的状态数组
do
*v10++ = v9++;
while ( v9 != 256 );

然后是第二步

2,密钥调度算法KSA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
v11 = 0;  // 这是 j
v12 = 0; // 这是 i
v13 = v30;
do
{
v14 = v36; // 获取密钥长度
if ( v36 )
v14 = *(_DWORD *)(v36 - 4); // 如果是vector,-4位置是长度

// 核心KSA算法:j = (j + S[i] + key[i % key_length]) % 256
v11 = (*v13 + v11 + *(unsigned __int8 *)(v36 + v12 % v14)) % 256;

// 交换 S[i] 和 S[j]
v33 = *(_BYTE *)v13; // 临时保存 S[i]
*v13 = v30[v11]; // S[i] = S[j]
v30[v11] = v33; // S[j] = 原来的S[i]

++v12; // i++
++v13;
}
while ( v12 != 256 ); // 循环256次

分布在这块,用密钥打乱状态数组

最后是伪随机的生成算法PRGA

3,伪随机生成算法PRGA

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
v17 = 0;  // 这是 i
v18 = 0; // 这是 j
v19 = v35; // 获取数据长度
if ( v35 )
v19 = *(_DWORD *)(v35 - 4);
v20 = v19 - 1;
if ( v20 >= 0 )
{
v31 = v20 + 1; // 数据长度
v21 = 0; // 数据索引
do
{
// PRGA步骤1: i = (i + 1) % 256
v17 = (v17 + 1) % 256;

// PRGA步骤2: j = (j + S[i]) % 256
v18 = (v30[v17] + v18) % 256;

// 交换 S[i] 和 S[j]
v33 = v30[v17];
v30[v17] = v30[v18];
v22 = v33;
v30[v18] = v33;

// PRGA步骤3: 生成密钥流字节
v32 = v30[(v22 + v30[v17]) % 256];

// 加密/解密: 数据 ^= 密钥流
*(_BYTE *)(v35 + v21++) ^= v32;

--v31;
}
while ( v31 );
}

搞明白了,就是基本的RC4流程,key也拿到了,我们可以开始解密了

编写解密代码

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
from pathlib import Path
import struct

ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

def trans(s: str) -> str:
k = len(s) & 0xFF
out = []
for ch in s:
idx = (ord(ch) ^ k) % 62
out.append(ALPHABET[idx])
k = (k + ord(ch)) & 0xFF
return "".join(out)

def rc4(data: bytes, key: bytes) -> bytes:
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xFF
S[i], S[j] = S[j], S[i]
i = j = 0
out = bytearray()
for b in data:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) & 0xFF]
out.append(b ^ K)
return bytes(out)

def is_pe(buf: bytes) -> bool:
if buf[:2] != b"MZ":
return False
e_lfanew = struct.unpack_from("<I", buf, 0x3C)[0]
return buf[e_lfanew:e_lfanew+4] == b"PE\x00\x00"

vy = Path("hack.VyVA6n").read_bytes().decode("ascii", errors="ignore")
s18 = trans(vy)
key = ("EIP" + s18).encode("ascii")

ct = Path("hack.edata").read_bytes()
pt = rc4(ct, key)

print("VyVA6n content:", vy)
print("s18 =", s18)
print("key =", key.decode())

print("head =", pt[:4])
print("is_pe =", is_pe(pt))

Path("stage2.dll").write_bytes(pt)
print("[+] dumped -> stage2.dll")

其实直接去cyberchef跑也一样

寻找服务端接受指令函数

解密得到了一个dll文件

打开来就能发现一个与众不同的函数名,很明显就是这玩意了

反编译看看内容也会发现就是这玩意在接受服务端信息

所以本题的答案就是NICEa5g5VyVA6n1SYB1gHaHa


2025SPC电子取证竞赛决赛
https://mei-you-qian.github.io/2025/12/22/2025SPC电子取证竞赛决赛/
作者
Meiyouqian
发布于
2025年12月22日
许可协议