Windows核心编程 跨进程操作

目录

进程A拿到进程B句柄是否能用

句柄的权限

关于句柄表

跨进程使用句柄-继承

CreateProcess:bInheritHandles 

OpenProcess

FindWinodw

GetCurrentProcess

跨进程使用句柄-拷贝

跨进程操作内存

WriteProcessMemory

VirtualProtectEx

ReadProcessMemory


进程A拿到进程B句柄是否能用

创建两个基于对话框的MFC分别为A,B

MFC A

ZeroMemory(&si, sizeof(si)); 是一段用于清空内存的代码,它使用了Windows操作系统提供的ZeroMemory函数。

该函数接受两个参数:第一个参数是指向要清空的内存块的指针,第二个参数是要清空的内存块的大小(以字节为单位)。

在这段代码中,&si 是指向变量 si 的指针,sizeof(si) 则是获取变量 si 所占用的内存块的大小。ZeroMemory函数将会将此内存块中的所有字节都设置为0。

void CADlg::OnBnClickedButton1()
{STARTUPINFO si;PROCESS_INFORMATION pi;ZeroMemory(&si, sizeof(si));si.cb = sizeof(si);ZeroMemory(&pi, sizeof(pi));// Start the child process. if (!CreateProcess(NULL, // No module name (use command line). "B.exe", // Command line. NULL,             // Process handle not inheritable. NULL,             // Thread handle not inheritable. FALSE,            // Set handle inheritance to FALSE. 0,                // No creation flags. NULL,             // Use parent's environment block. NULL,             // Use parent's starting directory. &si,              // Pointer to STARTUPINFO structure.&pi)             // Pointer to PROCESS_INFORMATION structure.){AfxMessageBox("CreateProcess failed.");}//显示句柄值SetDlgItemInt(EDT_HANDLE, (UINT)pi.hProcess);
}

MFC B

void CBDlg::OnBnClickedButton1()
{HANDLE hProc = (HANDLE)GetDlgItemInt(EDT_HANDLE);::TerminateProcess(hProc, 0);}

修改输出目录

输出目录如下: 

经过实验后:进程B是无法结束进程A的

剖析:一个进程,它所打开的句柄(或者说它所获得的句柄),进程都会把句柄存起来,这样就会形成一个表记录进程拿到哪些句柄;这个表叫做句柄表;

在B进程的句柄表中,并没有自己的进程的句柄,所以就算手动把A进程的句柄给B进程;让B进程结束——B进程表示在自己的句柄表中找不到自己的进程的句柄,所以报错,传入的句柄是无效句柄;

句柄的权限

句柄的权限是指操作系统对于句柄所代表的对象所授予的操作权限。不同类型的对象(如文件、进程、线程等)具有不同的权限集合。以下是一些常见的句柄权限:

  1. 文件权限:
  • FILE_READ_DATA:允许读取文件内容。
  • FILE_WRITE_DATA:允许写入文件内容。
  • FILE_APPEND_DATA:允许在文件末尾追加数据。
  • FILE_EXECUTE:允许执行文件。
  • FILE_DELETE:允许删除文件。
  • FILE_READ_ATTRIBUTES:允许读取文件属性。
  • FILE_WRITE_ATTRIBUTES:允许修改文件属性。
  1. 进程和线程权限:
  • PROCESS_CREATE_PROCESS:允许创建子进程。
  • PROCESS_TERMINATE:允许终止进程。
  • PROCESS_QUERY_INFORMATION:允许查询进程信息。
  • THREAD_CREATE_THREAD:允许创建线程。
  • THREAD_TERMINATE:允许终止线程。
  • THREAD_QUERY_INFORMATION:允许查询线程信息。
  1. 窗口权限:
  • GWL_STYLE:允许设置窗口样式。
  • GWL_EXSTYLE:允许设置窗口扩展样式。
  • WM_CLOSE:允许关闭窗口。
  • WM_DESTROY:允许销毁窗口。

这些仅是一些常见的句柄权限示例,实际上句柄的权限取决于所代表对象的类型和操作系统的安全策略。在使用句柄进行操作时,需要根据具体的需求和操作对象的类型来确定所需的权限,以确保在合法范围内进行操作。

关于句柄表

Windows操作系统中的句柄表通常被划分为以下三层:

  1. 用户层:用户层是最高层,包含了应用程序和操作系统之间的交互接口。在用户层,开发人员可以使用操作系统提供的API函数来创建和操作各种内核对象,如文件、进程、线程、窗口、消息等。
  2. 内核层:内核层是操作系统的核心,包括了内核、设备驱动程序等。在内核层,操作系统可以直接访问硬件资源,提供更加底层的操作接口。
  3. 硬件层:硬件层包含了操作系统所运行的计算机的物理硬件设备,如CPU、内存、硬盘、网络设备等。

句柄表通常被放置在内核层,用于管理应用程序和内核对象之间的关系。操作系统为每个进程维护一个独立的句柄表,该句柄表包含了该进程所拥有的句柄。在内核层,操作系统使用句柄来标识和访问内核对象。句柄通常被视为指向内核对象的指针,开发人员可以使用操作系统提供的API函数来获取、创建、操作句柄,并使用句柄来操作内核对象。

父进程和子进程:(A创建的进程都是A的子进程)
父进程和子进程从使用的角度讲,没有什么非常特殊操作的关系,是两个单独的进程,各有4g内存,各自有各自的线程,堆和栈,是独立的;使用的过程中,特别:进程句柄的时候用到父子进程,让项目的结构方便一点,
子进程有父进程的进程id,系统管理进程是由结构体管理的,由表管理,在内核里面,子进程的内核存有父进程的id信息;

跨进程使用句柄-继承

继承父进程的条件:

句柄本身可以被继承,CreateProcess的bInHeritHandle为TRUE。

子进程只能继承在自身被创建之前父进程打开的句柄,自身创建后父进程打开的句柄无法继承。

也就是说,CreateProcess创建进程B,进程B不能继承自身的句柄,自身句柄创建完之后才能拿到。解决办法是:

  1. 获取自己进程的句柄:GetCurrentProcess。返回值为-1,是个伪句柄,该句柄用于操作自身。
  2. 句柄本身也是带有私有公有属性的,和C++的继承很像,所以句柄都有一个是否可被继承的属性,这个属性由OpenProcess函数决定

CreateProcess:bInheritHandles 

新进程是否继承来自父进程的句柄。TRUE则继承。

子进程继承父进程已经打开了的句柄,只能在父子进程之间使用,CreateProcess中的第五个参数可以设置继承关系,TRUE就是可以被子进程继承,FALSE就不会被继承:

CreateProcess:参数三四安全属性指明创建出的子进程的进程句柄和线程句柄能否被继承

安全描述符是一个结构体SECURITY_ATTRIBUTES;定义如下

第一个参数是长度大小,第二个参数一般填NULL,第三个成员决定是否被继承

OpenProcess

OpenProcess函数将打开指定PID的进程,并返回一个与该进程关联的句柄。如果成功,返回的句柄可用于后续的操作,如读取或写入进程的内存、终止进程等。如果操作失败,返回NULL或INVALID_HANDLE_VALUE。

1 // 作用:打开一个存在的进程对象。(获取进程句柄)
2 // 返回值:成功返回进程句柄,失败返回NULL。
3 HANDLE OpenProcess(
4  DWORD dwDesiredAccess, 	// 权限标志,一般填PROCESS_ALL_ACCESS通杀
5  BOOL bInheritHandle, 	// OpenProcess打开的句柄能否被子进程继承
6  DWORD dwProcessId 		// 进程ID
7  );

OpenProcess函数是Windows操作系统提供的函数之一,用于打开一个已存在的进程并返回一个与该进程关联的句柄。它的参数如下:

  1. dwDesiredAccess:指定打开进程的访问权限,即访问级别。可以使用以下常量进行设置:

    • PROCESS_ALL_ACCESS:具有完全访问权限的句柄,可以执行任意操作。
    • PROCESS_CREATE_PROCESS:允许创建进程。
    • PROCESS_CREATE_THREAD:允许创建线程。
    • PROCESS_DUP_HANDLE:允许复制句柄。
    • PROCESS_QUERY_INFORMATION:允许查询进程信息。
    • PROCESS_QUERY_LIMITED_INFORMATION:允许有限查询进程信息。
    • PROCESS_SET_INFORMATION:允许设置进程信息。
    • PROCESS_SET_QUOTA:允许设置进程配额。
    • PROCESS_SUSPEND_RESUME:允许挂起或恢复进程。
    • PROCESS_TERMINATE:允许终止进程。
    • PROCESS_VM_OPERATION:允许对进程进行虚拟内存操作。
    • PROCESS_VM_READ:允许读取进程的虚拟内存。
    • PROCESS_VM_WRITE:允许写入进程的虚拟内存。
  2. bInheritHandle:指定打开的句柄是否可被子进程继承。如果为TRUE,则可被子进程继承;如果为FALSE,则不能被子进程继承。

  3. dwProcessId:要打开的进程的标识符(PID)。可以通过其他函数(如EnumProcesses)获取进程的PID,或者使用特定的值来表示特定的进程,如GetCurrentProcessId()表示当前进程的PID。

FindWinodw

FindWindow函数是Windows操作系统提供的函数之一,用于查找具有指定类名和窗口名的顶级窗口。它的参数如下:

  1. lpClassName:指定要查找的窗口类名。可以是一个字符串指针,指向类名的字符串,也可以是预定义的常量,如"Button"、"Edit"等。如果想要查找所有窗口,请将该参数设置为NULL。

  2. lpWindowName:指定要查找的窗口名。可以是一个字符串指针,指向窗口名的字符串。如果想要查找具有指定类名但没有特定窗口名的窗口,请将该参数设置为NULL。

FindWindow函数将根据提供的类名和窗口名在系统中查找匹配的顶级窗口。如果找到匹配的窗口,将返回该窗口的句柄(HWND)。否则,返回NULL。

// 作用:获取窗口句柄。
// 返回值:成功返回窗口句柄,失败返回NULL。
// 备注:参数二填一即可,另一个写NULL。
HWND FindWindow(LPCTSTR lpClassName, // 类名LPCTSTR lpWindowName // 窗口名);

GetCurrentProcess

// 作用:获取自身进程句柄。
// 返回值:恒为‐1。对任何进程而言,‐1代表自身进程的句柄,‐1是个伪句柄。
HANDLE GetCurrentProcess(void);

测试代码

一个设置句柄权限包括是否可被继承,一个设置可被子进程继承。

//使用继承跨进程使用句柄
void CADlg::OnBnClickedButton1()
{STARTUPINFO si;PROCESS_INFORMATION pi;ZeroMemory(&si, sizeof(si));si.cb = sizeof(si);ZeroMemory(&pi, sizeof(pi));SECURITY_ATTRIBUTES sa = {};sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; //允许进程句柄被子进程继承(赋予保护/公有属性)sa.lpSecurityDescriptor = NULL;// Start the child process. if (!CreateProcess(NULL, // No module name (use command line). "B.exe", // Command line. &sa,             // 允许此句柄被继承NULL,             // Thread handle not inheritable. TRUE,            // 允许子进程继承句柄0,                // No creation flags. NULL,             // Use parent's environment block. NULL,             // Use parent's starting directory. &si,              // Pointer to STARTUPINFO structure.&pi)             // Pointer to PROCESS_INFORMATION structure.){AfxMessageBox("CreateProcess failed.");}//显示句柄值SetDlgItemInt(EDT_HANDLE, (UINT)pi.hProcess);}

跨进程使用句柄-拷贝

把句柄拷贝到别的进程,有没有父子关系都无所谓,从自己的句柄表里拷贝到对方的句柄表里,注意是拷贝一个句柄而不是一个句柄表,继承才是把句柄表打包一份。说是拷贝实际上对方是重新获取了。

拷贝函数DuplicateHandle

BOOL DuplicateHandle(HANDLE   hSourceProcessHandle,  // 源进程句柄HANDLE   hSourceHandle,         // 源句柄HANDLE   hTargetProcessHandle,  // 目标进程句柄LPHANDLE lpTargetHandle,        // 目标句柄DWORD    dwDesiredAccess,       // 访问权限BOOL     bInheritHandle,        // 是否可被继承DWORD    dwOptions              // 选项
);

参数说明: 

  • hSourceProcessHandle:源进程的句柄,即拥有要复制句柄的进程。
  • hSourceHandle:要复制的句柄,即源句柄。
  • hTargetProcessHandle:目标进程的句柄,即要将复制的句柄关联到的进程。
  • lpTargetHandle:指向目标句柄的指针,用于接收复制后的句柄。

当调用DuplicateHandle函数时,有三个参数需要指定具体的取值:

  1. dwDesiredAccess:表示复制后句柄的访问权限。可以使用以下访问权限常量进行设置,也可以通过逻辑或运算符(|)组合多个权限:
  • GENERIC_READ:允许对对象进行读取操作。
  • GENERIC_WRITE:允许对对象进行写入操作。
  • GENERIC_EXECUTE:允许对对象进行执行操作。
  • GENERIC_ALL:允许对对象进行所有操作。

此外,还可以使用特定对象类型的访问权限常量,例如FILE_READ_DATA、FILE_WRITE_DATA等。具体取值取决于复制的句柄所代表的对象类型。

  1. bInheritHandle:表示目标句柄是否可被子进程继承。如果值为TRUE,则子进程可以继承目标句柄;如果值为FALSE,则子进程不会继承目标句柄。

  2. dwOptions:表示一些额外的选项。可以使用以下常量进行设置:

  • 0:没有额外的选项。
  • DUPLICATE_SAME_ACCESS:复制后的句柄将具有与源句柄相同的访问权限。

使用DUPLICATE_SAME_ACCESS选项时,dwDesiredAccess参数中的访问权限可以省略,复制后的句柄将具有与源句柄相同的访问权限。

适用场景:跨权限操作一些东西。比如有system权限的进程拿到句柄给管理员权限的进程用。

伪句柄:把进程自身-1的句柄值拷贝给自己,可以获取进程自身真正的句柄值。

此时 -1 对应的就是当前获取的句柄,它操作的是它自己,这就是伪句柄。

void CADlg::OnBnClickedButton1()
{STARTUPINFO si;PROCESS_INFORMATION pi;ZeroMemory(&si, sizeof(si));si.cb = sizeof(si);ZeroMemory(&pi, sizeof(pi));// Start the child process. if (!CreateProcess(NULL, // No module name (use command line). "B.exe", // Command line. NULL,             // 不允许此句柄被继承NULL,             // Thread handle not inheritable. TRUE,             // 不允许子进程继承句柄0,                // No creation flags. NULL,             // Use parent's environment block. NULL,             // Use parent's starting directory. &si,              // Pointer to STARTUPINFO structure.&pi)              // Pointer to PROCESS_INFORMATION structure.){AfxMessageBox("CreateProcess failed.");}HANDLE hProcInDst = NULL;BOOL fSuccess = DuplicateHandle(GetCurrentProcess(), pi.hProcess, //被拷贝的句柄pi.hProcess, //拷贝到B进程&hProcInDst,0,FALSE,DUPLICATE_SAME_ACCESS);if (!fSuccess)AfxMessageBox("DuplicateHandle failed");//显示句柄值SetDlgItemInt(EDT_HANDLE, (UINT)hProcInDst);}

句柄表里没有自身进程的记录

跨进程操作内存

跨进程读写内存的办法

1. Winhex手动修改进程内存:open Memory - 目标进程 - 修改完保存(写入进程内存)。

2. API修改:跨进程写内存 - WriteProcessMemory。内存属性不可写会写入失败。

3. 跨进程读内存 - ReadProcessMemory。

内存属性

1. 内存访问属性:R读,W写,X执行,C写时拷贝。修改内存访问属性:VirtualProtectEx。

2. ProcessHacker看内存属性:exe双击 - Memory - Protection。

3. WriteProcessMemory不用判断内存属性,每次写之前修改内存属性,写完后还原内存属性。

4. 写完数据不还原属性:会被检测(向只读内存写入数据,看是否触发异常,触发则表明正常)

PS:内存的基本单位4k【一个分页0x1000】,所以当修改0x1225的内存属性实则修改了0x0 ~ 0x2048的内存属性

内存分页

1. 内存分页:大小0x1000(4096),4kb。系统管理内存的基本单位。属性改一字节影响一个分页。

2. 申请内存时,系统至少分配一个分页,一个字节也分配一个分页。

3. new和malloc是在系统分配的基础上再次分配,在系统分配的分页中再次分配需要的字节。

再次以内存属性来看常量区等可读不可写区域

操作系统喜欢将统一权限属性的放在一起的原因是方便管理,且不浪费内存,也更容易维护。

WriteProcessMemory

1 // 作用:将数据写入指定进程内存。
2 BOOL WriteProcessMemory(
3  HANDLE hProcess, // 目标进程句柄
4  LPVOID lpBaseAddress, // 需要修改目标进程的地址
5  LPCVOID lpBuffer, // 写入数据缓冲区
6  SIZE_T nSize, // 写入数据缓冲区的大小
7  SIZE_T * lpNumberOfBytesWritten // 传出参数(可选),写入成功的数据大小,不
需要可以填NULL
8  );

VirtualProtectEx

 // 作用:修改内存的访问属性。
BOOL VirtualProtectEx(HANDLE hProcess, // 目标进程句柄LPVOID lpAddress, // 修改属性的内存地址SIZE_T dwSize, // 修改属性的内存大小DWORD flNewProtect, // 修改后的内存访问属性PDWORD lpflOldProtect // 传出参数,修改前的内存访问属性,填NULL会调用失败
);参数3:修改属性的内存大小
修改内存属性会影响到这一段地址空间涉及到的所有内存分页。
跨越页面边界的2字节范围会导致两个页面的保护属性都被更改。参数4:内存属性
PAGE_READONLY 只读
PAGE_READWRITE 可读可写
PAGE_WRITECOPY 写时拷贝
PAGE_EXECUTE 可执行
PAGE_EXECUTE_READ 可执行可读
PAGE_EXECUTE_READWRITE 可执行可读写
PAGE_EXECUTE_WRITECOPY 可执行可写时拷贝

ReadProcessMemory

BOOL ReadProcessMemory(  HANDLE hProcess,              // handle to the processLPCVOID lpBaseAddress,        // base of memory areaLPVOID lpBuffer,              // data bufferSIZE_T nSize,                 // number of bytes to readSIZE_T * lpNumberOfBytesRead  // number of bytes read);

飞机躲子弹-无敌模式

1.编写飞机躲子弹工具0x00406D6C   4   nplanX,  当前飞机位置
0x00406D70   4   nplanY,
0x00406E10   4   子弹数组首地址
0x00406DA8   4   当前子弹的个数
0x00406D80   4   死亡标志
0x004020F5   1   速度
0x00403616   0xeb  __asm jmp    无敌模式0x74  __asm je     普通模式
0x0040469E   4    初始子弹个数nBulletX >>= 6;nBulletX -= 4;nBulletY >>= 6;nBulletY -= 4;+=0xF子弹:
x坐标 = ary[i * 0xf +0] >>= 6 -= 4
y坐标 = ary[i * 0xf =4] >>= 6 -= 4

代码如下


void CGameAssistDlg::Wudi(BYTE bt)
{//获取窗口句柄HWND hWndGame = ::FindWindow(NULL, "摿孭");if (hWndGame == NULL){AfxMessageBox("获取窗口句柄失败");return;}//获取进程IDDWORD dwProId = 0;DWORD dwThreadId = GetWindowThreadProcessId(hWndGame, &dwProId);if (dwThreadId == 0){AfxMessageBox("获取进程id失败");return;}//获取进程句柄HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProId);if (hProc == NULL){AfxMessageBox("获取进程句柄失败");return;}//修改内存属性LPVOID pAddrGod = (LPVOID)0x00403616;//BYTE bt = 0xeb;DWORD dwOldProc = 0;BOOL bRet = VirtualProtectEx(hProc, pAddrGod, sizeof(bt), PAGE_READWRITE, &dwOldProc);if (!bRet){AfxMessageBox("修改内存属性失败");return ;}//修改内存bRet = WriteProcessMemory(hProc, pAddrGod, &bt, sizeof(bt), NULL);if (!bRet){AfxMessageBox("无敌失败");}//修改完后还原内存属VirtualProtectEx(hProc, pAddrGod, sizeof(bt), dwOldProc, &dwOldProc);//释放进程句柄CloseHandle(hProc);
}

 修改前0x400000是可读的

修改后

还原内存属性

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

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

相关文章

ky10 server x86 auditd安装(日志审计系统)

概述 Auditd工具可以帮助运维人员审计Linux,分析发生在系统中的发生的事情。Linux 内核有用日志记录事件的能力,包括记录系统调用和文件访问。管理员可以检查这些日志,确定是否存在安全漏洞(如多次失败的登录尝试,或者…

Mac Ubuntu双系统解决WiFi和WiFi 5G网络不可用问题

文章目录 设备信息1. Ubuntu WiFi不可用解决方式查看Mac的网卡型号根据网卡型号搜索获取到的解决方法查看WiFi名字问题参考链接 2. 解决WiFi重启后失效问题打开终端创建.sh脚本文件编辑脚本文件复制粘贴脚本修改脚本权限创建并编辑systemd service文件复制粘贴下文到systemd se…

Python可迭代对象排序:深入排序算法与定制排序

更多Python学习内容:ipengtao.com 排序在计算机科学中是一项基础而关键的操作,而Python提供了强大的排序工具来满足不同场景下的排序需求。本文将深入探讨Python中对可迭代对象进行排序的方法,涵盖基础排序算法、sorted函数的应用、以及定制排…

x-www-form-urlencoded的含义解释,getReader()和getParameter()的区别

1、x-www-form-urlencoded x-www-form-urlencoded是一种编码格式,它是一种常见的编码方式,用于在HTTP请求中 传输表单数据 。在这种编码方式下,表单数据被编码为URL格式,然后作为请求体(payload)发送。 需要…

《尚品甄选》:后台系统——结合redis实现用户登录

文章目录 一、统一结果实体类二、统一异常处理三、登录功能实现四、CORS解决跨域五、图片验证码六、登录校验功能实现6.1 拦截器开发6.2 拦截器注册 七、ThreadLocal 要求: 用户输入正确的用户名、密码以及验证码,点击登录可以跳转到后台界面。未登录的用…

【信息隐藏】信息隐藏基础

00 学习资源 0.1 推荐书籍 1.多媒体安全基础导论 复旦大学出版社 蓝皮; 2.隐写学原理与技术(赵险峰)科学出版社 蓝皮 0.2 视频课程 南开大学-信息隐藏技术(没看) 0.3 代码资源 GitHub一位phd:https:/…

JDBC编程方法及细节

JDBC(Java Database Connectivity)是Java编程语言用于连接和操作数据库的API(Application Programming Interface)。它为开发人员提供了一组Java类和接口,用于与各种关系型数据库进行通信。使用JDBC,开发人…

点大商城V2.5.3分包小程序端+小程序上传提示限制分包制作教程

这几天很多播播资源会员反馈点大商城V2.5.3小程序端上传时提示大小超限,官方默认单个包都不能超过2M,总分包不能超20M。如下图提示超了93KB,如果出现超的不多情况下可采用手动删除一些images目录下不使用的图片,只要删除超过100KB…

git提交报错error: failed to push some refs to ‘git url‘

1.产生错误原因 想把本地仓库提交到远程仓库,报错信息如下 git提交报错信息 error: src refspec master does not match any error: failed to push some refs to git url 错误原因: 我们在创建仓库的时候,都会勾选“使用Reamdme文件初始化…

发送一个网络数据包的过程解析

在 ip_queue_xmit 中,也即 IP 层的发送函数里面,有三部分逻辑。第一部分,选取路由,也即我要发送这个包应该从哪个网卡出去。 这件事情主要由 ip_route_output_ports 函数完成。接下来的调用链为:ip_route_output_port…

apipost接口200状态码,浏览器控制台500状态码

后端 url 登录login方法 login(){this.$refs.loginForm.validate(async valid > {if (!valid) return// 由于data属性是一个json对象,需要进行解构赋值{data:result},进行状态码判断const {data: result} await this.$http.post(/api/doLogin,this.…

《使用Python将Excel数据批量写入MongoDB数据库》

在数据分析及处理过程中,我们经常需要将数据写入数据库。而MongoDB作为一种NoSQL数据库,其具有强大的可扩展性、高性能以及支持复杂查询等特性,广泛用于大规模数据存储和分析。在这篇文章中,我们将使用Python编写一个将Excel数据批…