通过安全日志读取WFP防火墙放行日志

news2024/11/24 1:08:03

前言

之前的文档中,描写了如何对WFP防火墙进行操作以及如何在防火墙日志中读取被防火墙拦截网络通讯的日志。这边文档,着重描述如何读取操作系统中所有被放行的网络通信行为。
读取系统中放行的网络通信行为日志,在win10之后的操作系统上,也可以通过前一篇提到的读取阻断日志的方式进行读取(以FWPM_NET_EVENT0.type字段区分),但是在较老的系统中却不支持直接读取。为了保持系统兼容性,可以通过读取操作系统安全日志(EventId:5156)的方式进行网络通信日志的采集。

需要注意的坑点

  1. 查询放行日志时需要注意,每个网络通信行为在日志中只会出现一条放行记录,对应的筛选器ID,只会是首次对其进行审计的过滤器ID。因此,如果有其他权重更高的子层对网络连接进行了审计时,就无法通过筛选器ID匹配的方式获取。如果有这方面需求的话,解决方法只能是尽可能将自身子层的权重设为最高。
  2. 网络日志中读取连入行为时,WIN10/2016/2019源IP和目的IP字段与其它更早的操作系统相反,需要特殊处理。连出行为无异常。

开启审计

采用读取安全日志的方式进行网络事件获取,首先需要在系统中开启审计功能。在代码里面也有多种方式可以开启,之后会单开一篇文档进行描述,在这里先手动开启。

  • 打开本地安全策略(开始——运行——secpol.msc),依次打开:安全设置——本地策略——审核策略如图
    在这里插入图片描述

  • 在右侧窗口中打开 审核对象 标签页,勾选 “成功” 复选框后,点击保存,即可开启网络访问的审计功能
    在这里插入图片描述

  • 右键单击 “我的电脑”——“管理”——“计算机管理”——“系统工具”——“事件查看器”——“Windows日志”——“安全”中,查看5156日志即可。
    在这里插入图片描述

网络通信日志默认情况下是开启状态,为了以防万一,每次获取之前需要使用代码开启一次。使用代码的开启方式下次单开文档分享。

使用WMI方式进行查询

使用ReadEventLog进行查询

优点:兼容性高,可支持XP/2003操作系统。读取性能高。
缺点:无法做过滤,在大量日志中提取少量日志时效率较低
使用ReadEventLog读取Windows的安全日志只需要三步即可,1、打开EventLog句柄;2、使用ReadEventLog循环读取日志;3、关闭EventLog句柄。具体API描述如下。

打开EventLog句柄

    HANDLE OpenEventLog( 
                LPCSTR lpUNCServerName, 
                LPCSTR lpSourceName 
            );
  • 输入参数
    • lpUNCServerName:远程服务器的名称。读取本地的话传入NULL即可。
    • lpSourceName:日志名称。这里读取安全日志传入“Serurity”。其他对应值:系统日志“System”,应用程序日志“Application”
  • 输出参数
    • 返回日志读取句柄。在ReadEventLog中使用,需要调用CloseEventLog手动关闭。

读取日志

BOOL ReadEventLog( 
                        HANDLE hEventLog, 
                        DWORD dwReadFlags, 
                        DWORD dwRecordOffset, 
                        LPVOID lpBuffer, 
                        DWORD nNumberOfBytesToRead, 
                        DWORD *pnBytesRead, 
                        DWORD *pnMinNumberOfBytesNeeded 
                );
  • 输入参数

    • hEventLog:需要读取日志的句柄,就是刚才OpenEventLog返回的那个
    • dwReadFlags:读取标志,可以选择从指定偏移读取(EVENTLOG_SEEK_READ)或按顺序读取(EVENTLOG_SEQUENTIAL_READ),也可以指定正序读取(EVENTLOG_FORWARDS_READ)或倒序读取(EVENTLOG_BACKWARDS_READ)
    • dwRecordOffset:当dwReadFlags中包含EVENTLOG_SEEK_READ时有效,表示开始的位置
    • lpBuffer:分配的缓冲区,由外部划分内存
    • nNumberOfBytesToRead:lpBuffer缓冲区的大小
    • pnBytesRead:返回接收字节数
    • pnMinNumberOfBytesNeeded:返回lpBuffer所需最小缓冲区大小。仅当lpBuffer过小时返回,可判断GetLastError()返回ERROR_INSUFFICIENT_BUFFER
      时有效。
  • 输出参数

    • 正常执行返回非0值,失败后返回0

    关闭EventLog句柄

BOOL CloseEventLog( 
                            HANDLE hEventLog 
                    );
  • 输入参数
    • hEventLog:事件句柄,由OpenEventLog返回
  • 输出参数
    • 成功与否,这玩意没啥好判断的。

关联结构体

typedef struct _EVENTLOGRECORD { 
                        DWORD Length; 
                        DWORD Reserved; 
                        DWORD RecordNumber; 
                        DWORD TimeGenerated; 
                        DWORD TimeWritten; 
                        DWORD EventID; 
                        WORD EventType; 
                        WORD NumStrings; 
                        WORD EventCategory; 
                        WORD ReservedFlags; 
                        DWORD ClosingRecordNumber; 
                        DWORD StringOffset; 
                        DWORD UserSidLength; 
                        DWORD UserSidOffset; 
                        DWORD DataLength; 
                        DWORD DataOffset; 
                        } EVENTLOGRECORD, *PEVENTLOGRECORD;
  • 参数说明
    • Length:当前结构体的长度,由于ReadEventLog是以内存块的方式返回,单次返回的内存块中可能包含多个,特别是为了节省资源,可能会在代码中刻意一次读取大量的EventLogRecord结构体。这些结构体在内存块中,就以Length参数作为分界线来进行分割

    • Reserved:保留,没有可以研究过用于啥

    • RecordNumber:日志序号,可配合ReadEventLog函数中的dwReadFlags参数和dwReadOffset参数设置读取的偏移地址

    • TimeGenerated:事件时间,转time_t就可以

    • TimeWrittern:日志写入时间,time_t格式

    • EventID:事件ID,最高两位代表严重性,第三位代表是否为系统事件,低16位代表在安全日志中可见的ID。

    • EventType:事件类型,

    • NumStrings:包含字符串数目

    • EventCategory:事件类别

    • ReservedFlags:保留

    • ClosingRecordNumber:保留

    • StringOffset:事件包含字符串起始地址的偏移。字符串按顺序依次在内存中存储,0长度的字符串代表结束。字符串的顺序,和在事件查看器中看到的顺序一致,如图
      在这里插入图片描述

    • UserSidLength/UserSidOffset: 用户SID的长度及偏移,与字符串类似

    • DataLength/DataOffset:数据部分的长度及偏移

参考代码

#include <iostream>
#include <windows.h>
#include <string>
#include <vector>

#define MAX_EVENTLOG_READONCE   2048
void ReadLogsByReadEventLogAPI()
{
    LPBYTE pEventLogBuffer = new(std::nothrow) BYTE[MAX_EVENTLOG_READONCE];
    if (pEventLogBuffer == NULL)
    {
        return;
    }
    ZeroMemory(pEventLogBuffer, MAX_EVENTLOG_READONCE);

    HANDLE hEventLog = OpenEventLog(NULL, L"Security");
    if (hEventLog == NULL)
    {
        delete[] pEventLogBuffer;
        pEventLogBuffer = NULL;
        return;
    }

    DWORD dwMemoryLen = MAX_EVENTLOG_READONCE;
    DWORD dwReaded = 0, dwMiniMemoryNeeded = 0;
    while (true)//  -_-|||
    {
        BOOL isSucc = ReadEventLog(hEventLog, EVENTLOG_BACKWARDS_READ | EVENTLOG_SEQUENTIAL_READ, 0, pEventLogBuffer, dwMemoryLen, &dwReaded, &dwMiniMemoryNeeded);
        if (isSucc == FALSE && GetLastError() == ERROR_INSUFFICIENT_BUFFER && dwMiniMemoryNeeded)
        {
            delete[] pEventLogBuffer;
            pEventLogBuffer = NULL;

            pEventLogBuffer = new (std::nothrow) BYTE[dwMiniMemoryNeeded];
            if (pEventLogBuffer == NULL)
            {
                break;
            }
            ZeroMemory(pEventLogBuffer, dwMiniMemoryNeeded);
            dwMemoryLen = dwMiniMemoryNeeded;
            isSucc = ReadEventLog(hEventLog, EVENTLOG_BACKWARDS_READ | EVENTLOG_SEQUENTIAL_READ, 0, pEventLogBuffer, dwMemoryLen, &dwReaded, &dwMiniMemoryNeeded);
        }

        if (isSucc == FALSE)
        {
            break;
        }

        int pos = 0;
        do 
        {
            EVENTLOGRECORD* pTempRecord = (EVENTLOGRECORD*)(pEventLogBuffer + pos);
            __time32_t occurTime = pTempRecord->TimeWritten;
            int nEventId = pTempRecord->EventID & 0xffff;
            if (nEventId != 5156)
            {
                pos = pos + pTempRecord->Length;
                continue;
            }

            std::vector<std::wstring> vecEventParam;

            LPBYTE pTempBuffer = (LPBYTE)pTempRecord + pTempRecord->StringOffset;
            int strCount = 0;
            while ((pTempBuffer < ((LPBYTE)pTempRecord + pTempRecord->Length)) && (strCount < pTempRecord->NumStrings))
            {
                int len = wcslen((wchar_t*)pTempBuffer);
                if (len == 0)
                {
                    break;
                }

                vecEventParam.push_back((wchar_t*)pTempBuffer);

                pTempBuffer = pTempBuffer + (len + 1) * sizeof(wchar_t);
                strCount++;
            }

            if (vecEventParam.size() > 10)
            {
                std::wcout << "\n\n=========================================" << std::endl;
                std::wcout << L"Source:\t" << vecEventParam[3] << L"[" << vecEventParam[4] << L"]" << std::endl;
                std::wcout << L"Destination:\t" << vecEventParam[5] << L"[" << vecEventParam[6] << L"]" << std::endl;
                std::wcout << L"Protocol Code:\t" << vecEventParam[7] << std::endl;
                std::wcout << L"Process Id:\t" << vecEventParam[0] << std::endl;
                std::wcout << L"Process Name:\t" << vecEventParam[1] << std::endl;
                std::wcout << "=========================================\n\n" << std::endl;
            }
            pos = pos + pTempRecord->Length;
        } while (pos < dwReaded);
    }

    CloseEventLog(hEventLog);
    
    delete[] pEventLogBuffer;
    pEventLogBuffer = NULL;
}

输出截图
在这里插入图片描述

使用Evt系列API进行查询

优点:性能高,可自由配置过滤条件,方便在海量日志中检索,可读取的内容相当丰富
缺点:不支持xp/2003操作系统
Evt系列API涉及到的功能较多,如果只需要读取系统日志的话,只需要枚举、遍历、读取、关闭四步即可完成。

枚举当前日志

EVT_HANDLE EvtQuery( 
                                EVT_HANDLE Session, 
                                LPCWSTR Path, 
                                LPCWSTR Query, 
                                DWORD Flags 
                        );

遍历日志,获取下一条

BOOL EvtNext( 
                                    EVT_HANDLE ResultSet, 
                                    DWORD EventsSize, 
                                    PEVT_HANDLE Events, 
                                    DWORD Timeout, 
                                    DWORD Flags, 
                                    PDWORD Returned 
                                );

读取日志内容

BOOL EvtRender( 
                                    EVT_HANDLE Context, 
                                    EVT_HANDLE Fragment, 
                                    DWORD Flags, 
                                    DWORD BufferSize, 
                                    PVOID Buffer, 
                                    PDWORD BufferUsed, 
                                    PDWORD PropertyCount 
                            );
  • 输入参数
    • Context:
    • Fragment:
    • Flags:
    • BufferSize:
    • Buffer:
    • BufferUsed:
    • PropertyCount:
  • 输出参数
    *

关闭枚举句柄

BOOL EvtClose( 
                            EVT_HANDLE Object 
                        );

实例代码


void parseEventXML(std::wstring wstrXML)
{
    std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> cv;
    std::string utfXML = cv.to_bytes(wstrXML);

    tinyxml2::XMLDocument rootXML;
    if (rootXML.Parse(utfXML.c_str()) != tinyxml2::XML_SUCCESS)
    {
        return;
    }

    tinyxml2::XMLNode* rootNode = rootXML.FirstChild();
    if (rootNode == NULL)
    {
        return;
    }

    std::string processId;
    std::string processName;
    std::string sourceAddress;
    std::string sourcePort;
    std::string destAddress;
    std::string destPort;
    std::string protocol;

    tinyxml2::XMLNode* eventNode = rootXML.FirstChildElement("Event");
    if (eventNode)
    {
        tinyxml2::XMLElement* eventDataNode = eventNode->FirstChildElement("EventData");
        if (eventDataNode == NULL)
        {
            return;
        }
        tinyxml2::XMLElement* dataNode = eventDataNode->FirstChildElement("Data");
        do 
        {
            std::string strName = dataNode->Attribute("Name");
            if (_stricmp(strName.c_str(), "ProcessID") == 0)
            {
                processId = dataNode->GetText();
            }
			else if (_stricmp(strName.c_str(), "Application") == 0)
			{
				processName = dataNode->GetText();
			}
            else if (_stricmp(strName.c_str(), "SourceAddress") == 0)
			{
				sourceAddress = dataNode->GetText();
			}
            else if (_stricmp(strName.c_str(), "SourcePort") == 0)
			{
				sourcePort = dataNode->GetText();
			}
            else if (_stricmp(strName.c_str(), "DestAddress") == 0)
			{
				destAddress = dataNode->GetText();
			}
            else if (_stricmp(strName.c_str(), "DestPort") == 0)
			{
				destPort = dataNode->GetText();
			}
            else if (_stricmp(strName.c_str(), "Protocol") == 0)
			{
				protocol = dataNode->GetText();
			}

            dataNode = dataNode->NextSiblingElement("Data");
        } while (dataNode);

		std::cout << "\n\n=========================================" << std::endl;
		std::cout << "Source:\t" << sourceAddress << "[" << sourcePort << "]" << std::endl;
		std::cout << "Destination:\t" << destAddress << "[" << destPort << "]" << std::endl;
		std::cout << "Protocol Code:\t" << protocol << std::endl;
		std::cout << "Process Id:\t" << processId << std::endl;
		std::cout << "Process Name:\t" << processName << std::endl;
		std::cout << "=========================================\n\n" << std::endl;
    }
}

void ReadLogsByEvtAPI()
{
    DWORD dwEventLogBufferLen = MAX_EVENTLOG_READONCE;
	LPBYTE pEventLogBuffer = new(std::nothrow) BYTE[MAX_EVENTLOG_READONCE];
	if (pEventLogBuffer == NULL)
	{
		return;
	}
	ZeroMemory(pEventLogBuffer, MAX_EVENTLOG_READONCE);

    std::wstring wstrQuery = std::wstring(
        L"<QueryList>"
        L"	<Query>"
        L"		<Select>Event/System[EventID=5156]</Select>"
        L"	</Query>"
        L"</QueryList>");

    EVT_HANDLE hResult = EvtQuery(NULL, L"Security", wstrQuery.c_str(), EvtQueryChannelPath | EvtQueryReverseDirection);
    if (hResult == NULL)
    {
        delete[] pEventLogBuffer;
        pEventLogBuffer = NULL;
        return;
    }

    while (true) // -_-
    {

		EVT_HANDLE hEventArrs[MAX_PATH] = { 0 };
		DWORD dwReturnEvents = 0;
		BOOL isSucc = EvtNext(hResult, MAX_PATH, hEventArrs, INFINITE, 0, &dwReturnEvents);
		if (isSucc && dwReturnEvents)
		{
			for (int eventPos = 0; eventPos < dwReturnEvents; eventPos++)
			{
				DWORD dwBufferUsed = 0;
				DWORD dwPropertyCount = 0;
				int nBufferSize = dwEventLogBufferLen;
				ZeroMemory(pEventLogBuffer, nBufferSize);
				isSucc = EvtRender(NULL, hEventArrs[eventPos], EvtRenderEventXml, nBufferSize, pEventLogBuffer, &dwBufferUsed, &dwPropertyCount);
				if (isSucc == FALSE && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
				{
					delete[] pEventLogBuffer;
					pEventLogBuffer = NULL;

					dwEventLogBufferLen = dwBufferUsed + 2;
					pEventLogBuffer = new(std::nothrow) BYTE[dwEventLogBufferLen];
					if (pEventLogBuffer == NULL)
					{
						break;
					}

					nBufferSize = dwEventLogBufferLen;
					ZeroMemory(pEventLogBuffer, dwEventLogBufferLen);
					isSucc = EvtRender(NULL, hEventArrs[eventPos], EvtRenderEventXml, nBufferSize, pEventLogBuffer, &dwBufferUsed, &dwPropertyCount);
				}

				if (isSucc)
				{
					std::wstring strEventXML = std::wstring((wchar_t*)pEventLogBuffer);
					parseEventXML(strEventXML);
				}
			}
		}
    }

    if (pEventLogBuffer)
    {
		delete[] pEventLogBuffer;
		pEventLogBuffer = NULL;
    }
}

输出截图
在这里插入图片描述

备注

实例代码中解析xml使用的是tinyxml2库,对应github地址为:https://github.com/leethomason/tinyxml2/tree/master
当前最新版(9.0.0)版本下载地址:https://download.csdn.net/download/QQ1113130712/88235095

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

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

相关文章

vmware17 开启虚拟化

前言 有时候需要在虚拟机上安装虚拟机&#xff0c;方便做一些测试 解决办法 在vmware17 上对虚拟机开启虚拟化即可 下图中都勾上即可 设置完成之后就可以在虚拟机上安装虚拟机

【LLM评估篇】Ceval | rouge | MMLU等指标

note 一些大模型的评估模型&#xff1a;多轮&#xff1a;MTBench关注评估&#xff1a;agent bench长文本评估&#xff1a;longbench&#xff0c;longeval工具调用评估&#xff1a;toolbench安全评估&#xff1a;cvalue&#xff0c;safetyprompt等 文章目录 note常见评测benchm…

18.安全机制

文章目录 安全机制认证&#xff08;Authentication&#xff09;鉴权&#xff08;Authorization&#xff09;概念和组成创建Role和ClusterRole创建RoleBinding 和ClusterRoleBindingResources 准入控制&#xff08;Admission Control&#xff09;实验&#xff1a;创建一个用户管…

模型微调(fine-tune)

一、关于模型微调的一些基础知识 1、模型微调&#xff08;fine-tune&#xff09; 微调(fine-tune)通过使用在大数据上得到的预训练好的模型来初始化自己的模型权重&#xff0c;从而提升精度。这就要求预训练模型质量要有保证。微调通常速度更快、精度更高。当然&#xff0c;自己…

解放数据库,实时数据同步利器:Alibaba Canal

文章首发地址 Canal是一个开源的数据库增量订阅&消费组件&#xff0c;主要用于实时数据同步和数据订阅的场景&#xff0c;特别适用于构建分布式系统、数据仓库、缓存更新等应用。它支持MySQL、阿里云RDS等主流数据库&#xff0c;能够实时捕获数据库的增删改操作&#xff…

nvm的安装配置(node多版本切换控制)

安装 1. 首先要先卸载已安装的node 找到node&#xff0c;卸载就可以。 2. 下载 NVM 直接进入安装包下载地址&#xff1a;https://github.com/coreybutler/nvm-windows/releases&#xff0c;选择 nvm-setup.zip&#xff0c;下载后直接安装。 3. 配置环境变量(有的电脑会配好…

linux学习(文件系统+inode)[14]

输出重定向可分离 stdout -> 1printf("hello printf 1\n");fprintf(stdout,"hello fprintf 1\n");// stderr -> 2errno 1;perror("hello perror 2"); //stderrconst char *s1 "hello write 1\n";write(1, s1, strlen(s1));con…

PV3D: A 3D GENERATIVE MODEL FOR PORTRAITVIDEO GENERATION 【2023 ICLR】

ICLR&#xff1a;International Conference on Learning Representations CCF-A 国际表征学习大会&#xff1a;深度学习的顶级会议 生成对抗网络(GANs)的最新进展已经证明了生成令人惊叹的逼真肖像图像的能力。虽然之前的一些工作已经将这种图像gan应用于无条件的2D人像视频生…

人工智能轨道交通行业周刊-第56期(2023.8.14-8.20)

本期关键词&#xff1a;数字化建设、巡检机器人、智慧城轨、福州地铁4号线、避雷器、LangChain 1 整理涉及公众号名单 1.1 行业类 RT轨道交通人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交通联盟VSTR铁路与城市轨道交通RailMet…

vactor中迭代器失效问题

目录 什么是迭代器失效导致迭代器失效的操作VS和g环境下对与迭代器失效的态度 什么是迭代器失效 迭代器的底层其实就是一个指针&#xff0c;或者对指针进行了封装 vector的迭代器就是一个指针T* 一个迭代器指向某一个空间&#xff0c;此时这块空间被释放了&#xff0c;这个迭…

【Spring Boot】详解条件注解以及条件拓展注解@Conditional与@ConditionOnXxx

Spring Conditional Spring 4.0提供的注解。作用是给需要装载的Bean增加一个条件判断。只有满足条件才会装在到IoC容器中。而这个条件可以由自己去完成的&#xff0c;可以通过重写Condition接口重写matches()方法去实现自定义的逻辑。所以说这个注解增加了对Bean装载的灵活性。…

OJ练习第153题——分发糖果

分发糖果 力扣链接&#xff1a;135. 分发糖果 题目描述 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求&#xff0c;给这些孩子分发糖果&#xff1a; 每个孩子至少分配到 1 个糖果。 相邻两个孩子评分更高的孩子会获得更多的糖果。…

一篇文章搞懂MVCC

事务 什么是事务&#xff1f;当事务对数据库进行多个更改时&#xff0c;要么在事务提交时所有更改都成功&#xff0c;要么在事务回滚时所有更改都被撤销。 在 MySQL 中&#xff0c;事务支持是在引擎层实现的。MySQL 是一个支持多引擎的系统&#xff0c;但并不是所有的引擎都支…

详解反向迭代器适配器

目录 一、基本介绍 二、模拟实现 2.1 - operator* 2.2 - vector 和 list 的反向迭代器 一、基本介绍 反向迭代器适配器&#xff08;reverse_iterator&#xff09;&#xff0c;可简称为反向迭代器或逆向迭代器&#xff0c;常用来对容器进行反向遍历。 反向迭代器底层只能选…

Qt安卓开发经验技巧总结V202308

01&#xff1a;01-05 pro中引入安卓拓展模块 QT androidextras 。pro中指定安卓打包目录 ANDROID_PACKAGE_SOURCE_DIR $$PWD/android 指定引入安卓特定目录比如程序图标、变量、颜色、java代码文件、jar库文件等。 AndroidManifest.xml 每个程序唯一的一个全局配置文件&…

史上最简洁实用人工神经元网络c++编写202301

这是史上最简单、清晰…… C语言编写的 带正向传播、反向传播(Forward ……和Back Propagation&#xff09;……任意Nodes数的人工神经元神经网络……。 大一学生、甚至中学生可以读懂。 适合于&#xff0c;没学过高数的程序员……照猫画虎编写人工智能、深度学习之神经网络……

Android OpenCV(七十五): 看看刚”转正“的条形码识别

前言 2021年,我们写过一篇《OpenCV 条码识别 Android 平台实践》,当时的条形码识别模块位于 opencv_contrib 仓库,但是 OpenCV 4.8.0 版本开始, 条形码识别模块已移动到 OpenCV 主仓库,至此我们无需自行编译即可轻松地调用条形码识别能力。 Bar code detector and decoder…

Perl兼容正则表达式函数-PHP8知识详解

在php8中有两类正则表达式函数&#xff0c;一类是perl兼容正则表达式函数&#xff0c;另一类是posix扩展正则表达式函数。二者区别不大&#xff0c;我们推荐使用Perl兼容正则表达式函数。 1、使用正则表达式对字符串进行匹配 用正则表达式对目标字符串进行匹配是正则表达式的主…

59.C++ string容器

目录 1.1string容器的基本概念 1.2string构造函数 1.3string赋值操作 1.4string字符串拼接 1.5string查找和替换 1.6string字符串比较 1.7string字符串存取 1.8string字符串插入和删除 1.9string子串 1.1string容器的基本概念 本质&#xff1a; string是C风格的字…

五家项目进度管理工具,哪家好?

项目进度管理十分依赖项目经理对于项目信息的掌握程度&#xff0c;数字化工具可以很好的解决项目信息不统一的问题。一款好用的项目进度十分重要。目前市面上项目进度管理工具哪家好&#xff1f; 1、Zoho Projects&#xff1b;2、Microsoft Project&#xff1b;3、Trello&#…