文件结构与数据分析专项-解析

news/2025/9/18 23:59:06/文章来源:https://www.cnblogs.com/WXjzc/p/19099940

https://exam.didctf.com/practice/questions可以找到题目

出这套题主要是想鼓励大家在遇到陌生的文件时,可以主动地去对这类文件进行分析(尤其是将多个文件打包在一起),希望能通过专项练习得到这方面的提升。

源码

这边先给出源码,先是main.go

package mainimport ("crypto/rand""fmt"
)func main() {key := make([]byte, 256)if _, err := rand.Read(key); err != nil {fmt.Println("Error generating key:", err)return}if err := PackDir("files", "output.pak", key); err != nil {panic(err)}if err := UnpackFile("output.pak", "output"); err != nil {panic(err)}
}

然后是packer.go,这里就是主要逻辑

package mainimport ("bytes""compress/gzip""crypto/rc4""encoding/binary""fmt""io""os""path/filepath""sync""syscall""time""unsafe"
)type Packer struct {Header    [4]byteFileCount uint32RC4Key    [256]byte
}type FileInfo struct {CreateTime  uint64ModifyTime  uint64FileSize    uint64GzipSize    uint64FileNameLen uint16FileName    stringFileData    []byte
}var pool = &sync.Pool{New: func() interface{} {return &syscall.Filetime{}},
}func uint64FromFiletime(filetime *syscall.Filetime) uint64 {result := *(*uint64)(unsafe.Pointer(filetime))return result
}func Timestamp(t time.Time) uint64 {filetime := pool.Get().(*syscall.Filetime)defer pool.Put(filetime)*filetime = syscall.NsecToFiletime(t.UnixNano())return uint64FromFiletime(filetime)
}func FileTime(t syscall.Filetime) uint64 {return uint64FromFiletime(&t)
}func GzipCompress(data []byte) ([]byte, error) {var buf bytes.Bufferw := gzip.NewWriter(&buf)_, err := w.Write(data)if err != nil {return nil, err}w.Close()return buf.Bytes(), nil
}func GzipDecompress(data []byte) ([]byte, error) {r, err := gzip.NewReader(bytes.NewReader(data))if err != nil {return nil, err}defer r.Close()return io.ReadAll(r)
}func Rc4Encrypt(key, data []byte) []byte {dst := make([]byte, len(data))c, _ := rc4.NewCipher(key)c.XORKeyStream(dst, data)return dst
}func DeriveNextKey(prevData, baseKey []byte) []byte {if len(prevData) >= 256 {return prevData[len(prevData)-256:]}need := 256 - len(prevData)newKey := append([]byte{}, prevData...)newKey = append(newKey, baseKey[:need]...)return newKey
}func PackDir(dir, outFile string, baseKey []byte) error {entries, err := os.ReadDir(dir)if err != nil {return err}var packer Packercopy(packer.Header[:], []byte("PACK"))copy(packer.RC4Key[:], baseKey[:256])buf := new(bytes.Buffer)if err := binary.Write(buf, binary.LittleEndian, &packer); err != nil {return err}curKey := packer.RC4Key[:]fileCount := uint32(0)for _, entry := range entries {if entry.IsDir() {continue}path := filepath.Join(dir, entry.Name())info, err := os.Stat(path)if err != nil {return err}data, err := os.ReadFile(path)if err != nil {return err}gz, err := GzipCompress(data)if err != nil {return err}enc := Rc4Encrypt(curKey, gz)var ctime, mtime time.Timeif stat, ok := info.Sys().(*syscall.Win32FileAttributeData); ok {ctime = time.Unix(0, stat.CreationTime.Nanoseconds())mtime = time.Unix(0, stat.LastWriteTime.Nanoseconds())} else {ctime = info.ModTime()mtime = info.ModTime()}fi := FileInfo{CreateTime:  Timestamp(ctime),ModifyTime:  Timestamp(mtime),FileSize:    uint64(info.Size()),GzipSize:    uint64(len(enc)),FileNameLen: uint16(len(entry.Name())),FileName:    entry.Name(),FileData:    enc,}if err := binary.Write(buf, binary.LittleEndian, fi.CreateTime); err != nil {return err}if err := binary.Write(buf, binary.LittleEndian, fi.ModifyTime); err != nil {return err}if err := binary.Write(buf, binary.LittleEndian, fi.FileSize); err != nil {return err}if err := binary.Write(buf, binary.LittleEndian, fi.GzipSize); err != nil {return err}if err := binary.Write(buf, binary.LittleEndian, fi.FileNameLen); err != nil {return err}if _, err := buf.Write([]byte(fi.FileName)); err != nil {return err}if _, err := buf.Write(fi.FileData); err != nil {return err}fileCount++curKey = DeriveNextKey(gz, packer.RC4Key[:])}packer.FileCount = fileCountout := buf.Bytes()binary.LittleEndian.PutUint32(out[4:8], fileCount)return os.WriteFile(outFile, out, 0644)
}func UnpackFile(packFile, outDir string) error {data, err := os.ReadFile(packFile)if err != nil {return err}buf := bytes.NewReader(data)var p Packerif err := binary.Read(buf, binary.LittleEndian, &p); err != nil {return err}curKey := p.RC4Key[:]for i := uint32(0); i < p.FileCount; i++ {var fi FileInfoif err := binary.Read(buf, binary.LittleEndian, &fi.CreateTime); err != nil {return err}if err := binary.Read(buf, binary.LittleEndian, &fi.ModifyTime); err != nil {return err}if err := binary.Read(buf, binary.LittleEndian, &fi.FileSize); err != nil {return err}if err := binary.Read(buf, binary.LittleEndian, &fi.GzipSize); err != nil {return err}if err := binary.Read(buf, binary.LittleEndian, &fi.FileNameLen); err != nil {return err}name := make([]byte, fi.FileNameLen)if _, err := io.ReadFull(buf, name); err != nil {return err}fi.FileName = string(name)enc := make([]byte, fi.GzipSize)if _, err := io.ReadFull(buf, enc); err != nil {return err}dec := Rc4Encrypt(curKey, enc)raw, err := GzipDecompress(dec)if err != nil {return err}outPath := filepath.Join(outDir, fi.FileName)if err := os.WriteFile(outPath, raw, 0644); err != nil {return err}fmt.Println("Unpacked:", outPath)curKey = DeriveNextKey(dec, p.RC4Key[:])}return nil
}

出题思路

先准备若干文件,这里模拟的是一个传销平台场景,数据用python生成,插入到数据库(纯粹是为了解答案方便)当中,然后导出作为待打包文件,包含数据文件和表结构文件。

设定的打包文件逻辑是,在文件中存储一些元数据,包括文件数量、创建时间、修改时间、原始大小、压缩后大小、文件名称。然后对原始数据进行压缩,并使用rc4加密,初始密钥随机,后续文件密钥使用上一个文件压缩结果的后256字节。

这样下来如果想要解包,则需要先找到初始密钥,然后解密第一个文件,得到压缩后的数据,再用最后256字节继续向下解密,如此往复。

为了简便,只实现了打包1个目录下的文件,没有做递归这些,编译时注释掉解包代码。

import random
import pymysql
from faker import Faker
from datetime import datetime, timedelta# 数据库连接配置
DB_CONFIG = {"host": "192.168.31.5","user": "root","password": "123456","database": "test","charset": "utf8mb4"
}faker = Faker("zh_CN")  # 生成中文数据# 插入会员数据
def insert_members(cursor, total_members=100):members = []start_time = datetime(2008, 1, 1, 0, 0, 0)for i in range(1, total_members + 1):nickname = faker.user_name()gender = random.choice([0, 1, 2])real_name = faker.name()mobile = faker.phone_number()id_card = faker.ssn()address = faker.address()bank_card = faker.credit_card_number()# 创建时间随机递增 1~5 小时if i == 1:create_time = start_timeelse:delta_hours = random.randint(1, 5)create_time = members[-1]["create_time"] + timedelta(hours=delta_hours)# 上级会员逻辑if i == 1:superior_id = Noneelse:candidate_size = max(5, int((i - 1) * 0.5))start_id = max(1, i - candidate_size)superior_id = random.randint(start_id, i - 1)member = {"member_id": i,"nickname": nickname,"gender": gender,"real_name": real_name,"mobile": mobile,"id_card": id_card,"address": address,"bank_card": bank_card,"superior_id": superior_id,"wallet_balance": 0.00,"create_time": create_time}members.append(member)cursor.execute("""INSERT INTO member (member_id, nickname, gender, real_name, mobile, id_card, address, bank_card, superior_id, wallet_balance, create_time)VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""", (member["member_id"],member["nickname"],member["gender"],member["real_name"],member["mobile"],member["id_card"],member["address"],member["bank_card"],member["superior_id"],member["wallet_balance"],member["create_time"]))return members# 插入流水数据
def insert_transaction_flows(cursor, members, total_flows=300):flows = []flow_id = 1members_with_flows = random.sample(members, k=len(members) // 2)for _ in range(total_flows):member = random.choice(members)if member not in members_with_flows and random.random() < 0.5:member = random.choice(members_with_flows)member_id = member["member_id"]wallet_balance = member["wallet_balance"]# 流水时间递增 1~200 分钟if flows:last_time = flows[-1]["create_time"]else:last_time = member["create_time"]delta_minutes = random.randint(1, 10)create_time = last_time + timedelta(seconds=delta_minutes)amount = random.randint(100, 1000)if wallet_balance <= 0:flow_type = 1else:flow_type = random.choice([1, 2])if flow_type == 1:new_balance = wallet_balance + amountelse:new_balance = wallet_balance - amountif new_balance < 0:flow_type = 1new_balance = wallet_balance + amountmember["wallet_balance"] = new_balanceflow = {"flow_id": flow_id,"member_id": member_id,"flow_type": flow_type,"amount": amount,"create_time": create_time}flows.append(flow)cursor.execute("""INSERT INTO transaction_flow (flow_id, member_id, flow_type, amount, create_time)VALUES (%s,%s,%s,%s,%s)""", (flow["flow_id"],flow["member_id"],flow["flow_type"],flow["amount"],flow["create_time"]))flow_id += 1def main():conn = pymysql.connect(**DB_CONFIG)cursor = conn.cursor()try:print("插入会员数据中...")members = insert_members(cursor, total_members=3500000)conn.commit()# print("插入流水数据中...")# insert_transaction_flows(cursor, members, total_flows=456789)# conn.commit()print("数据插入完成!")except Exception as e:conn.rollback()print("发生错误:", e)finally:cursor.close()conn.close()if __name__ == "__main__":main()

解题过程

首先的2题,让查看packer.exe的信息,此时一般就会使用DIE、EXEInfoPe这类工具,再看后面的问题,应当养成习惯,先用IDA加载了再说。

IDA9.2之前,对go的字符串支持几乎为0,可以用BinaryNinja替代,9.2开始可以正常反编译出字符串(如果能看到字符串,则会有比较大的突破)

1、2跳过,来到第3题

文件output.pak是由packer.exe生成的文件,该文件中包含了几个文件?(答案格式:0)

先来看反编译结果,可以清晰地看到程序可能使用了rc4、gzip、timestamp这几样东西,并且打包函数是main_PackDir

这里可以看到v61,他声明的长度是264,为什么是这个数?并且还有字符串PACK在,这占了4个字节,后面的从第9个字节开始的256个字节从a5中复制过来,这是之前main中的v3,通过crypto_rand_Read生成了256字节的随机密钥

也就是说,目前文件头部的结构是PACK+4字节+256字节随机密钥,那中间的4字节是什么呢?结合提问其实就可以猜测,它就是记录的文件数量,我们带着猜测看一下他的赋值

这里可以看到v15由v60+1得来,从宏观看这部分的代码,其实就是在循环中记录+1,这里就可以确定结果了

此时查看文件内容,就可以知道打包了4个文件

此外,在ida解析出的LocalTypes中可以看到这个结构体(不知道为啥另一个结构体看不到),可以很轻松得到结果

到这里,3、4、5、6、7都可以解出来了,密钥是

文件output.pak是由packer.exe生成的文件,该文件中包含了几个文件?(答案格式:0)
文件output.pak中存在一个密钥,该密钥的长度是多少字节?(答案格式:0)
给出密钥的MD5值。(答案格式:e10adc3949ba59abbe56e057f20f883e)
output.pak中包含的文件使用的加密算法是什么?(答案格式:des)
output.pak中包含的文件使用的压缩算法是什么?(答案格式:zip)4F7C6DA92323508E512FC92952F0D4552B164894107FC252621391F9978F61EB1A3D1915BFFE70931D326F9972721ACEEB813B7C6D3E4CA29699DCEF2171CAC043675C7B5F61A98F5CDE6439435CFF60EC76915C7E0DFFDAE9EB89596DC6A5B8B2E0DF61E415E78AC1C1BBF6F056EC4E74C15891DFC942EB732832022651ADC60EA139C993733C17C19D60137375C363E9693B7E0E04BCBAAEF89D14D70D752A8DF0525A6D3C9A78E583774DEA272B57038401BA9C27F54DBB8585FEDA71DF4A46D036AB1795ED75BA866189F57D130B8A9891515F2EC7659E956258F3FCCED8D6A741D6F80109A140B69550DF7650FC51DA590E96EAA2F82222B90F4E16AC09

接下来是8、9、10

member.txt的原始大小是多少字节?(答案格式:10086)
member.txt被压缩后的大小是多少字节?(答案格式:10086)
创建时间为北京时间2025-09-09 11:59:38的文件的文件名是什么?(答案格式:abc123.def)

从反编译的结果来看,就是循环读取文件、压缩、加密

然后一步步写进文件

我们侧重一下看写入的是啥,其实主要是跟v94有关,这里先连着写了4个8字节的uint64数据

接着写了1个2字节的uint16数据,然后写入了1个字符串v119(go中的字符串会包含地址和长度),最后将剩下的数据写入,最后面的v94[7],向上跟一下就知道是加密结果

最先写入的v82和v80,显然能看到是时间戳,不难看出获取的是WindowsFileTime

那么我们回到打包后的文件确认一下,确实如此,现在需要找到的是两个时间分别表示什么,从下面的数据来看,第二个时间是大于第一个时间的,虽然在秒级上是一样的,但是FileTime是100纳秒级,还是能看出差别

结合题目会问的创建时间,可以推测其中一个是创建时间,那么另一个就是修改时间或者访问时间,但是从代码里看简直就是灾难

这里我们可以回到main中,能够知道程序会读取files目录,然后生成output.pak文件,那么为何不执行一次呢?

新建一个files目录,导入1个文件,然后执行,对比结果。这样就可以知道,前面一个是创建时间,后面一个是修改时间(到底是修改时间还是访问时间,保存文件后,过一会再打开,就可以使这俩个值不一样了,比对一下就能知道)

还记得程序是连续写入了4个8字节的uint64,1个2字节的uint16吗?现在我们可以比对了,前面2个是时间,那后面2个呢?从题目的提问来看,大概率是文件大小相关的内容。

我们其实已经可以看见,第3个uint64的值是4,对应了文件大小4字节,第4个uint64的值是28,根据提问,这就对应了压缩后的字节大小(由于压缩算法需要包含一些元数据,特别小的文件压缩后可能会变大),又由于我们使用的加密算法是RC4,不会影响数据长度,所以获得密钥后就可以直接解析了。

接下来的uint16和字符串就很清晰了,uint16的值是7,字符串是aaa.txt,显然分别对应文件名长度和文件名称

从反编译的结果来看,就是读取了目录下的所有文件,然后遍历这些文件进行处理,最后读取了创建时间、修改时间、文件大小、文件名这些数据并写入结构体

接下来是11、12两题

packer.exe在进行一次打包时,使用的密钥是否会发生变化?(答案格式:是或否)
member.txt的MD5值是多少?(答案格式:e10adc3949ba59abbe56e057f20f883e)

第11题显然提醒我们,在单词打包中,密钥可能会发生变化,同时,这里也降低了难度,member.txt是打包的第一个文件,他的密钥是存储在文件头部的,所以无须写出完整的unpacker也能拿到这个文件并做一些题目

我们之前分析过,v61后面就是密钥

这里v116是压缩结果,v98就是对应的密钥,那么这里不难发现,每次循环,v98都是由v14进行赋值

根据v71进行判断,分成小于256和小于等于256两种情况

显然用后者更方便,else里面只有3行代码,v93就是压缩结果,v71是长度,这一段意思就是取最后256字节

而else之前,可以分析出,获取256-压缩长度,将压缩结果和初始密钥对应长度的数据拼接得到新的密钥

这样一来,我们就可以实现解包函数了,具体逻辑为

1.获取文件数量,确认要解包的文件数
2.读取初始密钥
3.读取元数据(创建时间、修改时间、文件大小、文件名等)
4.依据压缩结果长度,从文件名之后读取对应长度的数据,用初始密钥进行解密、解压
5.后续如此循环往复,但密钥需要根据之前的解密结果生成,数据大于等于256字节时,直接取最后256字节为密钥,否则和初始密钥拼接,取前256字节

后续数据分析部分,直接给sql

当前余额和流水不符的会员ID有?(答案格式:12,13,14)
层级关系一共有多少层?(答案格式:1)
第100层有多少会员?(答案格式:1)
性别和身份证号码能对应上的会员数量?(答案格式:1)

计算流水不符的情况

select m.member_id,m.wallet_balance,a.balance from member m
left join (select tf.member_id,sum(case tf.flow_typewhen 1 then tf.amountwhen 2 then -tf.amountend
) as balance from transaction_flow tf group by tf.member_id) a on m.member_id = a.member_id
where m.wallet_balance <> a.balance;

层级部分直接用levelTree或其他工具,能算出结果即可

校验性别

selectmember_id,gender,case SUBSTRING(id_card, LENGTH(id_card) - 1, 1) % 2 when 1 then 1when 0 then 2end AS id_gender
frommember
havingid_gender = gender

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/907525.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

销售能力——Steam平台我们应该做什么游戏?

最近刚看一个视频,李亚鹏卖酒的故事挺有意思,分享一下 https://www.bilibili.com/video/BV1CXkuY2EXk/?spm_id_from=333.1391.0.0&vd_source=106601ca71b1d910c1ac4aa2466b744c 这里李亚鹏卖酒卖不出去,和我们…

平静

也许我该试着让自己的心灵安静下来。 隐入世俗的境地,好好和身边的人聊聊天。 从合租的房子里搬出去,自己一个人独居。如此渴望孤独的星空,又渴望与群星作伴。

2025.9.18总结

内容:回顾web学习路线 从最开始的1.黑框增删改查 2.连上数据库,但依旧黑框增删改查 3.网页界面增删改查 4.使用springboot+vue3增删改查。 现在回头看使用ai能够轻松的完成一个表的小项目。 不过对于存粹的技术上面没…

Codeforces 2144F Bracket Groups 题解 [ 紫 ] [ AC 自动机 ] [ DP ] [ 构造 ]

Bracket Groups:赛时猜出来用 ACAM,结果没猜到结论,我是糖比。 首先判掉一些 corner,如果出现了 \(\texttt{()}\) 为单个字符串,则一定无解。 发现后面不太好做,所以可以套路地猜一猜答案上界,发现最多只需要分…

Java进制,数据类型拓展Unicode编码学习

今日学习Java 进制 int i = 10; //十进制,结果为10 int i2 = 010; //八进制,结果为8 int i3 = 0x10; //十六进制,结果为16 二进制符号为0b; float j = 0.1f; 数据类型拓展 银行业务用数据类型BigDecimal,可以进行…

【转】[IDEA] 调试时怎么判断使用哪个配置文件

【转】[IDEA] 调试时怎么判断使用哪个配置文件转自:豆包 在 IntelliJ IDEA 中调试 Spring 项目时,如果发现加载的配置文件不是预期的,通常是由于配置文件加载优先级、启动参数或项目结构问题导致的。以下是具体原因…

软件工程学习日志2025.9.18

今日重点设计了HBase后端数据插入模块,基于Java API实现了高性能的写入方案。以下为关键代码实现及技术要点: HBase数据插入工具类 支持单条插入和批量写入操作 public class HBaseInserter { private static final …

Clean Code/代码简洁性Good-Practice:使用统一异常来取代错误处理

Clean Code/代码简洁性Good-Practice:使用统一异常来取代错误处理通过自定义异常集中处理,将繁琐的参数校验代码转化为清晰、简洁且可维护的艺术。通过自定义异常集中处理,将繁琐的参数校验代码转化为清晰、简洁且可…

U3D动作游戏开发读书笔记--3.1 物理系统详解(上)

第三章 物理系统详解 3.1 物理系统的基本梳理 3.1.1 系统参数设置 了解物理配置:打开Project Settings设置Gravity:重力,常用范围是60~80 Queries Hit Backfaces :进行背面查询,如果需要查询MeshCollider背面的情…

一个联名款电子产品的技术实现和诞生

@目录项目核心亮点(“老年人”非得在地上穿梭也行,恐高嘛)核心技术(技术实现,欢迎各抒己见)市场分析基础核心创新点 项目核心亮点(“老年人”非得在地上穿梭也行,恐高嘛) 欢迎各位青少年小伙伴参与评论互动,…

US$198 Auxiliary Heater Diagnostic Unit for Eberspacher 12V/24V Systems

Auxiliary Heater Diagnostic Unit for Eberspacher 12V/24V SystemsAuxiliary Heater Diagnostic Unit Function:Read out errors from the control boxPerform diagnosis on installed heaterSwitch on heater direc…

JOISC

JOISC开坑。

20250918 之所思 - 人生如梦

20250918 之所思为了改善专注力,到网上找了不少方法,按照教程学习了冥想,但可能是境界不够,效果一直不太好,容易分心,注意力拉不回来,挺沮丧;昨天试验了番茄钟,开始一个任务,接着开始倒计时,发现注意力非常…

初赛知识点复盘

前言 作者觉得自己太菜了,就开始复盘初赛知识点了 接下来是CSP-S/J,虽然在HN很容易进复赛但是还是稳健一点 正文 1.计算机内部结构1.冯诺依曼计算机结构,分为 输入设备,存储器,输出设备,运算器,控制器,其中1.运…

WPF使用Cef加载Vue3页面问题

在WPF中使用CefSharp时遇到两个问题:1.Vue3中使用Ant Design Vue时,table不显示数据 由于之前的老项目用的Vue2框架,数据接口是一样的,页面的功能是差不多的,就把table的columns复制了过来,结果显示不出字段; 数…

curl与wget

wget 和 curl 不是替代关系,而是互补工具。wget 更“傻瓜式”,curl 更“灵活”。 但是curl 支持 40+ 协议,是 API 调试、RESTful 接口测试 的首选工具。HTTP 方法与 API 调用(curl 强项) curl 无法原生实现递归下载…

用 Go 语言与 Tesseract OCR 实现英文数字验证码识别

Go 语言本身不直接支持图像识别,但可以通过调用 Tesseract OCR 引擎来进行图像识别。我们可以使用 Go 的 tesseract 包来实现这一功能。 一、安装与配置 安装 Tesseract OCR 首先,你需要在系统中安装 Tesseract OCR。…

lc1031-两个非重叠子数组的最大和

难度:中等(中期)题目描述给定一个数组和两个长度,找到两个符合长度的不重合的连续子数组,使其和最大示例 输入:nums = [0,6,5,2,2,5,1,9,4], firstLen = 1, secondLen = 2 输出:20 解释:[6, 5] + [9]输入:num…

Segment Analytics-iOS SDK - 专业用户行为追踪解决方案

Segment Analytics-iOS SDK 是一个专业的iOS用户行为分析库,提供完整的事件追踪、用户识别、屏幕浏览统计等功能,支持多种数据集成方式,帮助开发者高效收集和分析用户行为数据。Segment Analytics-iOS SDK Analytic…