时移世易,篡改天机:吾以 Go 语令 Windows 文件“返老还童“记

38 阅读7分钟

文曰:

时维乙巳年秋,有程序员夜不能寐,忽见文件夹之创建时间,竟露马脚,惶惶如临大敌。遂执 Go 语为剑,调 Windows 之秘术,篡改天机,令旧物焕新,恍若昨日初生。此事奇诡,不可不记。

一、缘起:文件夹"露馅"了!

话说某日,某君正在调试一"压测提单工具"(此乃江湖秘器,专治系统慢如龟),忽觉不妥——

文件夹创建时间竟为今日!

此乃大忌!

若被 QA 看见,必问:"汝昨日方写此工具?何以昨日已用之?"

若被老板察觉,恐遭诘问:"此工具尚未完成,何以三日前已有日志?"

呜呼!时间即证据,证据即破绽。

欲掩其迹,非改其时不可!

然,Linux 之下,touch -t 一令可解;

Windows 之境,却无此等神技。

无奈,只得亲自动手,以代码为笔,重写天命。

二、探秘:Windows 之"三时"真言

Windows 文件,藏有三时,谓之:

CreationTime:诞生之刻,如婴儿初啼; LastAccessTime:最近一瞥,似故人回眸; LastWriteTime:最后修改,若匠人收工。 此三时,皆存于 Win32FileAttributeData 之中,非寻常 os.Stat 可窥全貌。

若欲篡之,须得:

打开文件之"魂"(句柄); 转时间于 FILETIME(此乃 Windows 独门格式,自 1601 年起算,单位百纳秒,玄之又玄); 调 SetFileTime 之秘术,一举改写三时。 此术凶险,稍有不慎,轻则无效,重则系统怒而拒之。

然吾有 Go 语言为盾,syscall 为矛,何惧之有?

三、铸剑:Go 语炼成"时之刃"

于是,吾挥毫泼墨,写下三式神功:

第一式:遍历万文件(GetAllFiles)

// 递归入幽谷,遍历子子孙孙,凡文件者,皆录其名。
func GetAllFiles(dirPath string) ([]string, error) { ... }

此式如扫地僧,默默无闻,却为后文铺路。

第二式:窥天机(PrintFileTime)

// 仰观文件之三时,如观星象,知其生辰八字。
func PrintFileTime(filePath string) { ... }

初看文件,三时毕现,心中有数,方敢下手。

第三式:篡天命(SetFileTime)

// 执时间之笔,重写 Creation、Access、Write 三命。
func SetFileTime(filePath string, ctime, atime, mtime time.Time) error { ... }

此乃核心!调 CreateFile 得其魂,转 time.Time 为 FILETIME,终以 SetFileTime 一锤定音。

注:此术仅通于 Windows。若于 Linux 行之,恐如对牛弹琴,徒增笑耳。

四、施法:令"压测工具"返老还童

主函数中,吾设三处要地:

主目录:D:\tools\压测提单工具 → 设为 2022-02-28 10:27:16 日志目录 runlog → 设为 2022-02-28 11:27:16 通信日志 socketlog → 设为 2022-02-28 11:27:13 先观其旧时,再施法篡改,复观其新时——

果然!三时皆如吾所愿,分毫不差!

文件夹静立如初,却已"穿越"三年,恍若昨日方建。

QA 再查,只见"2022年旧物",必叹曰:"此乃古董级工具,稳定可靠!"

老板见之,亦抚掌笑曰:"此君早有准备,深谋远虑!"

五、警世恒言

然,吾虽得意,亦不敢忘形。

篡改时间,乃双刃之剑:

若为测试、演示、归档所用,善莫大焉; 若为欺瞒、伪造、逃责所图,则天理难容! 且此术仅限 Windows,Linux 诸君莫急,自有 touch、debugfs 等妙法,另文再叙。

六、结语:代码即道,慎用神通

太史公曰:

时者,天地之过客;文件者,代码之遗迹。

能窥其时,改其命,非为逆天,实为顺事。

然顺事者,亦当守其道——技术无善恶,人心定乾坤。

今录此 Go 语真经,非教人作伪,实授人以"可控之权"。

愿诸君执此剑,不为诡道,而为清道。

附:完整代码

package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"syscall"
	"time"
)

// GetAllFiles 递归获取指定目录下所有文件的完整路径(不包括子目录本身)
func GetAllFiles(dirPath string) ([]string, error) {
	var files []string

	// 读取目录内容
	dirEntries, err := ioutil.ReadDir(dirPath)
	if err != nil {
		return nil, fmt.Errorf("无法读取目录 %q: %w", dirPath, err)
	}

	for _, entry := range dirEntries {
		fullPath := dirPath + "/" + entry.Name()

		if entry.IsDir() {
			// 递归处理子目录
			subFiles, err := GetAllFiles(fullPath)
			if err != nil {
				return nil, fmt.Errorf("递归读取子目录 %q 失败: %w", fullPath, err)
			}
			files = append(files, subFiles...)
		} else {
			// 是普通文件,加入结果列表
			files = append(files, fullPath)
		}
	}

	return files, nil
}

// SetFileTime 修改文件的创建时间(CreationTime)、最后访问时间(LastAccessTime)和最后修改时间(LastWriteTime)
// 注意:此函数仅适用于 Windows 系统
func SetFileTime(filePath string, creationTime, lastAccessTime, lastWriteTime time.Time) error {
	// 获取文件的绝对路径
	absPath, err := syscall.FullPath(filePath)
	if err != nil {
		return fmt.Errorf("无法获取文件绝对路径 %q: %w", filePath, err)
	}

	// 转换为 UTF-16 指针(Windows API 要求)
	pathPtr, err := syscall.UTF16PtrFromString(absPath)
	if err != nil {
		return fmt.Errorf("路径转 UTF-16 失败: %w", err)
	}

	// 打开文件句柄(仅用于修改属性)
	handle, err := syscall.CreateFile(
		pathPtr,
		syscall.FILE_WRITE_ATTRIBUTES,
		syscall.FILE_SHARE_WRITE,
		nil,
		syscall.OPEN_EXISTING,
		syscall.FILE_FLAG_BACKUP_SEMANTICS,
		0,
	)
	if err != nil {
		return fmt.Errorf("无法打开文件 %q: %w", absPath, err)
	}
	defer syscall.Close(handle)

	// 将 time.Time 转换为 Windows FILETIME 格式
	toFiletime := func(t time.Time) syscall.Filetime {
		return syscall.NsecToFiletime(t.UnixNano())
	}

	ct := toFiletime(creationTime)
	at := toFiletime(lastAccessTime)
	mt := toFiletime(lastWriteTime)

	// 调用 Windows API 设置时间戳
	if err := syscall.SetFileTime(handle, &ct, &at, &mt); err != nil {
		return fmt.Errorf("设置文件时间失败 %q: %w", absPath, err)
	}

	return nil
}

// PrintFileTime 打印 Windows 系统下文件的创建时间、最后访问时间和最后修改时间
func PrintFileTime(filePath string) {
	fileInfo, err := os.Stat(filePath)
	if err != nil {
		fmt.Printf("无法获取文件信息 %q: %v\n", filePath, err)
		return
	}

	// 类型断言为 Windows 特有的文件属性结构
	winAttr, ok := fileInfo.Sys().(*syscall.Win32FileAttributeData)
	if !ok {
		fmt.Printf("不支持的系统或文件类型: %q\n", filePath)
		return
	}

	// FILETIME 是从 1601-01-01 起的 100 纳秒单位,转换为 Unix 时间
	creationTime := time.Unix(0, winAttr.CreationTime.Nanoseconds())
	lastAccessTime := time.Unix(0, winAttr.LastAccessTime.Nanoseconds())
	lastWriteTime := time.Unix(0, winAttr.LastWriteTime.Nanoseconds())

	fmt.Printf("文件路径: %s\n", filePath)
	fmt.Printf("  创建时间: %v\n", creationTime)
	fmt.Printf("  最后访问时间: %v\n", lastAccessTime)
	fmt.Printf("  最后修改时间: %v\n", lastWriteTime)
}

// parseLocalTime 安全地将字符串解析为本地时区的 time.Time
func parseLocalTime(layout, value string) (time.Time, error) {
	return time.ParseInLocation(layout, value, time.Local)
}

func main() {
	// 示例:修改目录及其子目录中特定文件夹的时间戳

	// 1. 修改主目录时间
	dirPath := `D:\tools\压测提单工具`
	PrintFileTime(dirPath)

	t1, err := parseLocalTime("2006-01-02 15:04:05", "2022-02-28 10:27:16")
	if err != nil {
		fmt.Printf("时间解析失败: %v\n", err)
		return
	}
	if err := SetFileTime(dirPath, t1, t1, t1); err != nil {
		fmt.Printf("设置目录时间失败: %v\n", err)
		return
	}
	PrintFileTime(dirPath)

	// 2. 修改 runlog 子目录时间
	runlogPath := `D:\tools\压测提单工具\runlog`
	PrintFileTime(runlogPath)

	t2, err := parseLocalTime("2006-01-02 15:04:05", "2022-02-28 11:27:16")
	if err != nil {
		fmt.Printf("时间解析失败: %v\n", err)
		return
	}
	if err := SetFileTime(runlogPath, t2, t2, t2); err != nil {
		fmt.Printf("设置 runlog 目录时间失败: %v\n", err)
		return
	}
	PrintFileTime(runlogPath)

	// 3. 修改 socketlog 子目录时间
	socketlogPath := `D:\tools\压测提单工具\socketlog`
	PrintFileTime(socketlogPath)

	t3, err := parseLocalTime("2006-01-02 15:04:05", "2022-02-28 11:27:13")
	if err != nil {
		fmt.Printf("时间解析失败: %v\n", err)
		return
	}
	if err := SetFileTime(socketlogPath, t3, t3, t3); err != nil {
		fmt.Printf("设置 socketlog 目录时间失败: %v\n", err)
		return
	}
	PrintFileTime(socketlogPath)
}

运行前,请三思:汝,真需"返老还童"乎?

乙巳年霜月朔日,某程序员焚香沐浴,记于键盘之侧。

往期部分文章列表