[TOC]

文件操作

LOWORD,HIWORD,LOBYTE,HIBYTE

HIWORD,作用是取得某个4字节变量(即32位的值)在内存中处于高位的两个字节,即一个WORD长的数据(16位的无符号整数,范围从0到0xFFFF)。
LOWORD,作用是取得某个4字节变量(即32位的值)在内存中处于低位的两个字节,即一个WORD长的数据(16位的无符号整数,范围从0到0xFFFF)。
HIBYTE,作用是取得某个2字节变量(即16位的值)在内存中处于高位的一个字节,即一个BYTE长的数据(8位的无符号整数,范围从0到0xFF)。
LOBYTE,作用是取得某个2字节变量(即16位的值)在内存中处于低位的一个字节,即一个BYTE长的数据(8位的无符号整数,范围从0到0xFF)。

例如HIWORD(0xFA12EAC3)=0xFA12,LOWORD(0xFA12EAC3)=0xEAC3
HIBYTE(0xFA12)=0xFA,LOBYTE(0xFA12)=0x12

findFirstFileA

参数

如其名,是一个查找文件名的函数,包含两个参数

1
2
3
4
HANDLE FindFirstFileA(
[in] LPCSTR lpFileName,
[out] LPWIN32_FIND_DATAA lpFindFileData
);
  • [in] lpFileName:目录或路径以及文件名。 文件名可以包含通配符,例如星号 (*) 或问号 (?) 。
  • [out] lpFindFileData 指向 WIN32_FIND_DATA结构的指针 ,该结构接收有关找到的文件或目录的信息。

返回值

如果函数成功,则返回值是在对 FindNextFile 或 FindClose 的后续调用中使用的搜索句柄, 而 lpFindFileData 参数包含有关找到的第一个文件或目录的信息。
如果函数失败,因为找不到匹配的文件,可使用 GetLastError 函数,其将返回 ERROR_FILE_NOT_FOUND。

WIN32_FIND_DATAA 结构

成员

  • dwFileAttributes:当前文件的文件属性,具体值可参考 windows 官方文档文件属性常量。可以简单理解16是文件夹,32是文件
  • cFileName:获取当前文件的名称。
  • nFileSizeHigh:文件大小的高阶 DWORD 值(以字节为单位)除非文件大小大于 MAXDWORD,否则此值为零。
  • nFileSizeLow:文件大小的低序 DWORD 值(以字节为单位)。文件大小等于 (nFileSizeHigh * (MAXDWORD+1)) + nFileSizeLow
  • ……

fopen

打开一个文件

参数

  • filename:字符串,表示要打开的文件名称。
  • mode:字符串,表示文件的访问模式

访问模式

mode参数 含义
“r” 打开一个文本文件,文件必须存在,只允许读
“r+” 打开一个文本文件,文件必须存在,允许读写
“rb” 打开一个二进制文件,文件必须存在,只允许读
“rb+” 打开一个二进制文件,文件必须存在,允许读写
“w” 新建一个文本文件,已存在的文件将内容清空,只允许写
“w+” 新建一个文本文件,已存在的文件将内容清空,允许读写
“wb” 新建一个二进制文件,已存在的文件将内容清空,只允许写
“wb+” 新建一个二进制文件,已存在的文件将内容清空,允许读写
“a” 打开或新建一个文本文件,只允许在文件末尾追写
“a+” 打开或新建一个文本文件,可以读,但只允许在文件末尾追写
“ab” 打开或新建一个二进制文件,只允许在文件末尾追写
“ab+” 打开或新建一个二进制文件,可以读,但只允许在文件末尾追写
更多可查看Windows官方文档

fseek

将文件指针移到指定位置(将与 stream 关联的文件指针(如果有)移动到 origin 中为 offset 个字节的新位置)

参数

1
2
3
4
5
int fseek(
FILE *stream,
long offset,
int origin
);
  • stream:指向 FILE 结构的指针。
  • offset:origin 中的字节数。 可以理解为偏移量。
  • origin:初始位置。

参数 origin 必须是 STDIO.H 中定义的以下常量之一:

含义
SEEK_CUR 文件指针的当前位置。 1
SEEK_END 文件结尾。 2
SEEK_SET 文件开头。 0

ftell

获取文件指针的当前位置,并返回当前的文件位置。

参数

  • stream:目标 FILE 结构。

返回值

返回当前的文件位置。

feof

测试流的文件尾。

参数

  • stream:目标 FILE 结构。

返回值

如果读取操作已尝试读取超过文件的末尾,feof 函数将返回非零值;否则该函数返回 0。即读取文件结束返回0,否则返回非0值。

fgetc

从流中读取字符。

参数

  • stream:目标 FILE 结构。

返回值

fgetc 返回作为 int 读取的字符或返回 EOF 以指示错误或文件尾。
可以理解为等效于getc函数
fgetwc是宽字符版本fgetc;当以文本模式或二进制模式打开时stream,它将 c 读取为多字节字符或宽字符。

fputc

将字符写入流。

参数

  • c:要写入的字符。
  • stream:指向 FILE 结构的指针。

返回值

其中每个函数都会返回写入的字符。 对于 fputc,返回值 EOF 指示一个错误。 对于 fputwc,返回值 WEOF 指示一个错误。

ReadFile

如其名,读入文件函数

参数

1
2
3
4
5
6
7
BOOL ReadFile(
[in] HANDLE hFile,
[out] LPVOID lpBuffer,
[in] DWORD nNumberOfBytesToRead,
[out, optional] LPDWORD lpNumberOfBytesRead,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
  • [in] hFile:设备句柄 (例如文件、文件流、物理磁盘、卷、控制台缓冲区、磁带驱动器、套接字、通信资源、mailslot 或管道) 。必须使用读取访问权限创建 hFile 参数。
  • [out] lpBuffer:指向接收从文件或设备读取数据的缓冲区的指针。可以简单理解为要接收文件内容的变量的指针
  • [in] nNumberOfBytesToRead:要读取的最多字节数。
  • [out, optional] lpNumberOfBytesRead:指向使用同步 hFile 参数时接收读取的字节数的变量的指针。 ReadFile 将此值设置为零,然后再执行任何工作或错误检查。 如果这是一个异步操作,请对此参数使用 NULL ,以避免潜在的错误结果。
  • [in, out, optional] lpOverlapped:指向OVERLAPPED结构体的指针,如果文件或设备是以FILE_FLAG_OVERLAPPED标志打开的,这个参数必须有效,并且指定读取操作的起始位置;如果文件或设备不支持重叠操作,这个参数可以为NULL。

返回值

如果函数成功,则返回值为非零 (TRUE) 。
如果函数失败或异步完成,则返回值为零, (FALSE) 。

ReadFile(hObject, v11, 0x10u, &NumberOfBytesRead, 0);
这个函数调用的作用是从hObject指定的文件或设备读取16个字节的数据,存入v11指向的缓冲区,并把实际读取的字节数赋给NumberOfBytesRead变量。如果函数成功执行,返回值为非零;如果函数失败或正在完成异步操作,返回值为零,并且可以用GetLastError函数获取错误代码。

实例

[黄河流域公安院校网络安全技能挑战赛]WIFE

文件下载
查壳一下是无壳32位,直接用ida打开
很容易定位到这个函数

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
int __fastcall sub_401090(char a1)
{
char v1; // si
int result; // eax
FILE *v3; // edi
char v4; // si
FILE *v5; // esi
char v6; // al
char v7; // [esp-8h] [ebp-868h]
char v8; // [esp+0h] [ebp-860h]
HANDLE hFindFile; // [esp+14h] [ebp-84Ch]
struct _WIN32_FIND_DATAA FindFileData; // [esp+18h] [ebp-848h] BYREF
char NewFilename[256]; // [esp+158h] [ebp-708h] BYREF
char OldFilename[256]; // [esp+258h] [ebp-608h] BYREF
CHAR FileName[256]; // [esp+358h] [ebp-508h] BYREF
char Command[1028]; // [esp+458h] [ebp-408h] BYREF

v1 = a1;
memset(FileName, 0, sizeof(FileName));
sub_401050(FileName, "%s\\*.*", v1);
sub_401010(Format, (char)FileName);
hFindFile = FindFirstFileA(FileName, &FindFileData);
if ( hFindFile == (HANDLE)-1 )
return sub_401010("查找文件失败!\n", v8);
do
{
if ( FindFileData.dwFileAttributes == 16 )
{
if ( FindFileData.cFileName[0] != 46 )
{
memset(NewFilename, 0, sizeof(NewFilename));
sub_401050(NewFilename, "%s\\%s", v1);
sub_401010(asc_4031D4, (char)NewFilename);
Sleep(0x3E8u);
sub_401090(NewFilename);
}
}
else
{
memset(NewFilename, 0, sizeof(NewFilename));
sub_401050(NewFilename, "%s\\%s", v1);
sub_401010(asc_4031E8, (char)NewFilename);
v3 = fopen(NewFilename, "rb");
if ( v3 )
{
sub_401010("打开 %s 文件成功!\n", (char)NewFilename);
fseek(v3, 0, 2);
v4 = ftell(v3);
fseek(v3, 0, 0);
sub_401010(asc_403168, v4);
memset(OldFilename, 0, sizeof(OldFilename));
sub_401050(OldFilename, "%s\\%s", a1);
sub_401010("%s\n", (char)OldFilename);
v5 = fopen(OldFilename, "wb");
while ( !feof(v3) )
{
v6 = fgetc(v3);
fputc(v6 ^ 0x66, v5);
fputc(97, v5);
}
fclose(v3);
fclose(v5);
memset(Command, 0, 0x400u);
sub_401050(Command, "del \"%s\"", (char)NewFilename);
sub_401010("%s\n", (char)Command);
system(Command);
rename(OldFilename, NewFilename);
sub_401010("\n", v7);
v1 = a1;
}
else
{
sub_401010("打开文件失败\n", v8);
}
}
result = FindNextFileA(hFindFile, &FindFileData);
}
while ( result );
return result;
}

加密逻辑是遍历文件路径下的所有文件对其进行异或0x66与隔一位加字符a的操作,脚本如下

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
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <stdlib.h>
void jiemi(const char* filename,const char* pathname) {
char ch;
FILE* fp;
FILE* fpw;
char tmp[1024];
memset(tmp, 0, 1024);
sprintf(tmp, "%s\\tmp", pathname);
fp = fopen(filename, "rb");
fpw = fopen(tmp, "wb");
fseek(fpw, 0, SEEK_SET);

int i = 0;
while (!feof(fp)) {
ch = fgetc(fp);
if (0 == (i % 2)) {
i = 1;
fputc(ch ^ 0x66, fpw);
}
else {
i = 0;
continue;
}
}
fclose(fp);
fclose(fpw);

char commend[1024];
memset(commend, 0, 1024);
sprintf(commend, "del \"%s\"", filename); //访问路径包含空格增加双引号
system(commend);
rename(tmp, filename); //调用C语rename函数重命名文件
printf("\n");
return;
}

int main() {
jiemi("C:\\Users\\Tree\\Downloads\\Compressed\\wife\\secret","C:\\Users\\Tree\\Downloads\\Compressed\\wife");
return 0;
}

运行完脚本用01editor打开secret
发现是png文件
img
修改后缀名打开secret.png
img
flag{DHIUASHDUIASH}

[NKCTF2023]PMKF

定位到关键函数

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
int sub_401090()
{
int v1; // [esp+20h] [ebp-140h]
int v2; // [esp+24h] [ebp-13Ch]
int k; // [esp+2Ch] [ebp-134h]
int i; // [esp+30h] [ebp-130h]
int v5; // [esp+30h] [ebp-130h]
int j; // [esp+30h] [ebp-130h]
int v7; // [esp+30h] [ebp-130h]
char v8; // [esp+34h] [ebp-12Ch]
DWORD NumberOfBytesRead; // [esp+3Ch] [ebp-124h] BYREF
unsigned __int8 Buffer; // [esp+43h] [ebp-11Dh] BYREF
char v11[16]; // [esp+44h] [ebp-11Ch] BYREF
char v12[256]; // [esp+54h] [ebp-10Ch] BYREF

memset(v12, 0, sizeof(v12));
ReadFile(hObject, &Buffer, 1u, &NumberOfBytesRead, 0);
if ( Buffer != 5 )
{
sub_401690((int)"Wrong!\n");
CloseHandle(hObject);
exit(0);
}
ReadFile(hObject, v12, Buffer, &NumberOfBytesRead, 0);
for ( i = 0; i < Buffer; ++i )
{
if ( v12[i] != byte_405100[i] )
{
sub_401690((int)"Wrong!\n");
CloseHandle(hObject);
exit(0);
}
}
v5 = 0;
v8 = 0;
while ( v5 < Buffer )
v8 += v12[v5++];
ReadFile(hObject, v11, 0x10u, &NumberOfBytesRead, 0);
v2 = 18;
for ( j = 0; j < 16; ++j )
v11[j] ^= v8;
v7 = 0;
v1 = 1;
while ( v7 < 16 )
{
for ( k = 6; k >= 0; k -= 2 )
{
switch ( ((unsigned __int8)v11[v7] >> k) & 3 )
{
case 0:
v2 -= 18;
break;
case 1:
++v2;
break;
case 2:
v2 += 18;
break;
case 3:
--v2;
break;
default:
break;
}
if ( aN[v2] == 42 || aN[v2] == 32 )
{
v1 = 0;
break;
}
if ( aN[v2] == 75 )
{
sub_401690((int)"Congratulations! you found it!\n");
break;
}
}
++v7;
}
CloseHandle(hObject);
return v1;
}

从文件中读入一个5,然后从byte_405100中读取5个字符,点进去可知为"nkman"然后求和得v8,v8作为异或的值
aN为一个迷宫。

1
2
3
4
5
6
7
8
9
10
11
******************
N...*****...*....*
**.*****..*...**.*
...*****.*****.*.*
.**....*..*...*..*
....**.*.*..*...**
*.**...*.*.*******
..*****..*......**
.*......**.****.**
...*****...*K...**
******************

然后继续从文件中读入,异或v8,手动走一下迷宫
ddssaassdssassddwdddddwdwwwwwdwddsddwdddsssasaawaasassdddddssaaa
switch语句简单看一下得出上是0,右是1,下是2,左是3
1122332212232211011111010000010112110111222323303323221111122333
关键是这句话的理解for ( k = 6; k >= 0; k -= 2 ) (v11[v7] >> k) & 3
这句话是将v11的每两个二进制位转换为一个四进制位,
那我们就需要转换回去,转回16进制再加上05和’nkman’即为flag
flag:nkctf{056e6b6d616e4fef7eb0004415047000bea9eeb043aa}

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
v12 = [0x6E, 0x6B, 0x6D, 0x61, 0x6E]#nkman
v8 = 0
for i in range(5):
v8 += v12[i]
v8 ^= 0x200
#因为ReadFile(hObject, &Buffer, 1u, &NumberOfBytesRead, 0);
#这段读入函数只读取了v11中的0x10u即16个字节的数据,也就是取了低八位,
#所以等价于与v8的低八位的异或,v8是0x215,我们只取0x15
#有以下迷宫,N为起点,K为终点,只能走'.'的地方,上下左右分别用wsad表示
# ******************
# N...*****...*....*
# **.*****..*...**.*
# ...*****.*****.*.*
# .**....*..*...*..*
# ....**.*.*..*...**
# *.**...*.*.*******
# ..*****..*......**
# .*......**.****.**
# ...*****...*K...**
# ******************
path="ddssaassdssassddwdddddwdwwwwwdwddsddwdddsssasaawaasassdddddssaaa"
path =path.replace("w","0")
path =path.replace("d","1")
path =path.replace("s","2")
path =path.replace("a","3")
s = [path[i:i+4] for i in range(0, len(path), 4)]
qua = [int(x, 4) for x in s]
for i in range(len(qua)):
qua[i]^=v8
v11=['0x05', '0x6e', '0x6b', '0x6d', '0x61', '0x6e']
for i in range(len(qua)):
v11.append(hex(qua[i]).zfill(4))#有0x0出现用zfill填满四位
flag = 'nkctf{'+''.join(v11).replace('0x','')+'}'
print(flag)

本月学习其他知识点

python 相关

  • 通过encode(),decode()可以实现字符串和二进制字符串的转化默认使用’utf-8’编码。如果需要,也可以指定其他的编码方式
  • AES解密可以使用 Crypto.Cipher 里的 AES 包
  • zfill可以用于在字符串的左边填充’0’字符,使字符串达到指定的长度。接受一个参数,表示要填充的长度。注意会越过前置的±号
  • 如果想填充其他字符,可以采用center,ljust,rjust,居中为center这时候原来的字符串将会在中间,扩充物出现在两边。ljust和rjust分别将字符串放在左边和右边而填充物填充于右边和左边。这三个都需要接收两个参数,分别是填充长度和填充字符
  • 将列表中的元素连接成一个字符串可以用join,例如s=‘+’.join(a),就表示将列表a中的元素用’+'连接成一个字符串赋值给s

UPX 特征修复

img

参考