3.6 Windows驱动开发:内核进程汇编与反汇编

在笔者上一篇文章《内核MDL读写进程内存》简单介绍了如何通过MDL映射的方式实现进程读写操作,本章将通过如上案例实现远程进程反汇编功能,此类功能也是ARK工具中最常见的功能之一,通常此类功能的实现分为两部分,内核部分只负责读写字节集,应用层部分则配合反汇编引擎对字节集进行解码,此处我们将运用capstone引擎实现这个功能。

首先是实现驱动部分,驱动程序的实现是一成不变的,仅仅只是做一个读写功能即可,完整的代码如下所示;

#include <ntifs.h>
#include <windef.h>#define READ_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ALL_ACCESS)
#define WRITE_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_ALL_ACCESS)#define DEVICENAME L"\\Device\\ReadWriteDevice"
#define SYMBOLNAME L"\\??\\ReadWriteSymbolName"typedef struct
{DWORD pid;       // 进程PIDUINT64 address;  // 读写地址DWORD size;      // 读写长度BYTE* data;      // 读写数据集
}ProcessData;// MDL读取封装
BOOLEAN ReadProcessMemory(ProcessData* ProcessData)
{BOOLEAN bRet = TRUE;PEPROCESS process = NULL;// 将PID转为EProcessPsLookupProcessByProcessId(ProcessData->pid, &process);if (process == NULL){return FALSE;}BYTE* GetProcessData = NULL;__try{// 分配堆空间 NonPagedPool 非分页内存GetProcessData = ExAllocatePool(NonPagedPool, ProcessData->size);}__except (1){return FALSE;}KAPC_STATE stack = { 0 };// 附加到进程KeStackAttachProcess(process, &stack);__try{// 检查进程内存是否可读取ProbeForRead(ProcessData->address, ProcessData->size, 1);// 完成拷贝RtlCopyMemory(GetProcessData, ProcessData->address, ProcessData->size);}__except (1){bRet = FALSE;}// 关闭引用ObDereferenceObject(process);// 解除附加KeUnstackDetachProcess(&stack);// 拷贝数据RtlCopyMemory(ProcessData->data, GetProcessData, ProcessData->size);// 释放堆ExFreePool(GetProcessData);return bRet;
}// MDL写入封装
BOOLEAN WriteProcessMemory(ProcessData* ProcessData)
{BOOLEAN bRet = TRUE;PEPROCESS process = NULL;// 将PID转为EProcessPsLookupProcessByProcessId(ProcessData->pid, &process);if (process == NULL){return FALSE;}BYTE* GetProcessData = NULL;__try{// 分配堆GetProcessData = ExAllocatePool(NonPagedPool, ProcessData->size);}__except (1){return FALSE;}// 循环写出for (int i = 0; i < ProcessData->size; i++){GetProcessData[i] = ProcessData->data[i];}KAPC_STATE stack = { 0 };// 附加进程KeStackAttachProcess(process, &stack);// 分配MDL对象PMDL mdl = IoAllocateMdl(ProcessData->address, ProcessData->size, 0, 0, NULL);if (mdl == NULL){return FALSE;}MmBuildMdlForNonPagedPool(mdl);BYTE* ChangeProcessData = NULL;__try{// 锁定地址ChangeProcessData = MmMapLockedPages(mdl, KernelMode);// 开始拷贝RtlCopyMemory(ChangeProcessData, GetProcessData, ProcessData->size);}__except (1){bRet = FALSE;goto END;}// 结束释放MDL关闭引用取消附加
END:IoFreeMdl(mdl);ExFreePool(GetProcessData);KeUnstackDetachProcess(&stack);ObDereferenceObject(process);return bRet;
}NTSTATUS DriverIrpCtl(PDEVICE_OBJECT device, PIRP pirp)
{PIO_STACK_LOCATION stack;stack = IoGetCurrentIrpStackLocation(pirp);ProcessData* ProcessData;switch (stack->MajorFunction){case IRP_MJ_CREATE:{break;}case IRP_MJ_CLOSE:{break;}case IRP_MJ_DEVICE_CONTROL:{// 获取应用层传值ProcessData = pirp->AssociatedIrp.SystemBuffer;DbgPrint("进程ID: %d | 读写地址: %p | 读写长度: %d \n", ProcessData->pid, ProcessData->address, ProcessData->size);switch (stack->Parameters.DeviceIoControl.IoControlCode){// 读取函数case READ_PROCESS_CODE:{ReadProcessMemory(ProcessData);break;}// 写入函数case WRITE_PROCESS_CODE:{WriteProcessMemory(ProcessData);break;}}pirp->IoStatus.Information = sizeof(ProcessData);break;}}pirp->IoStatus.Status = STATUS_SUCCESS;IoCompleteRequest(pirp, IO_NO_INCREMENT);return STATUS_SUCCESS;
}VOID UnDriver(PDRIVER_OBJECT driver)
{if (driver->DeviceObject){UNICODE_STRING SymbolName;RtlInitUnicodeString(&SymbolName, SYMBOLNAME);// 删除符号链接IoDeleteSymbolicLink(&SymbolName);IoDeleteDevice(driver->DeviceObject);}
}NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{NTSTATUS status = STATUS_SUCCESS;PDEVICE_OBJECT device = NULL;UNICODE_STRING DeviceName;DbgPrint("[LyShark] hello lyshark.com \n");// 初始化设备名RtlInitUnicodeString(&DeviceName, DEVICENAME);// 创建设备status = IoCreateDevice(Driver, sizeof(Driver->DriverExtension), &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &device);if (status == STATUS_SUCCESS){UNICODE_STRING SymbolName;RtlInitUnicodeString(&SymbolName, SYMBOLNAME);// 创建符号链接status = IoCreateSymbolicLink(&SymbolName, &DeviceName);// 失败则删除设备if (status != STATUS_SUCCESS){IoDeleteDevice(device);}}// 派遣函数初始化Driver->MajorFunction[IRP_MJ_CREATE] = DriverIrpCtl;Driver->MajorFunction[IRP_MJ_CLOSE] = DriverIrpCtl;Driver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverIrpCtl;// 卸载驱动Driver->DriverUnload = UnDriver;return STATUS_SUCCESS;
}

上方的驱动程序很简单其中的关键部分已经做好了备注,接下来才是本节课的重点,让我们开始了解一下Capstone这款反汇编引擎吧!

3.6.1 内存反汇编的应用

Capstone 是一款轻量级、多平台、多架构的反汇编引擎,旨在成为二进制分析和反汇编的终极工具。它支持多种平台和架构的反汇编,包括x86、ARM、MIPS等,并且可以轻松地集成到各种二进制分析工具中。Capstone的主要优点是它易于使用和快速的反汇编速度,而且由于其开源和活跃的社区支持,可以很容易地更新和维护。因此,Capstone被广泛用于二进制分析、安全研究和反汇编工作中。

  • 反汇编引擎GitHub地址:https://github.com/capstone-engine

这款反汇编引擎如果你想要使用它,则第一步就是调用cs_open()打开一个句柄,这个打开功能的函数原型如下所示;

cs_err cs_open(cs_arch arch,cs_mode mode,csh *handle
);
  • 参数 arch:指定架构类型,例如 CS_ARCH_X86 表示为 x86 架构。
  • 参数 mode:指定模式,例如 CS_MODE_32 表示为 32 位模式。
  • 参数 handle:打开的句柄,用于后续对引擎的调用。由于其是传递指针的方式,因此需要先分配好该指针的内存。函数执行成功后,该句柄将被填充,可以用于后续的反汇编操作。

函数cs_open()Capstone反汇编引擎提供的,它用于初始化Capstone库并打开一个句柄,以便进行后续的反汇编操作。该函数有三个参数,分别是架构类型、执行模式和指向句柄的指针。

具体地说,第一个参数CS_ARCH_X86指定了反汇编的架构类型,这里表示为Windows平台;第二个参数CS_MODE_32CS_MODE_64则指定了反汇编的执行模式,即32位模式或64位模式;第三个参数则是指向一个Capstone库句柄的指针,通过该指针可以进行后续的反汇编操作。

打开句柄后,我们可以使用其他的Capstone函数进行反汇编操作,比如cs_disasm()函数用于对二进制代码进行反汇编,反汇编后的结果可以用于分析和理解程序的行为。最后,我们还需要使用cs_close()函数关闭打开的句柄以释放资源。

第二步也是最重要的一步,调用cs_disasm()反汇编函数,函数返回实际反汇编的指令数,或者如果发生错误,则返回0。该函数的原型如下所示;

size_t cs_disasm(csh handle, const uint8_t *code, size_t code_size, uint64_t address, size_t count, cs_insn *insn);

其中各参数的含义为:

  • 参数 handle:要使用的Capstone引擎的句柄,指定dasm_handle反汇编句柄
  • 参数 code:要反汇编的二进制代码的指针,定你要反汇编的数据集或者是一个缓冲区
  • 参数 code_size:要反汇编的二进制代码的大小(以字节为单位),指定你要反汇编的长度64
  • 参数 address:要反汇编的二进制代码在内存中的地址(用于计算跳转目标地址),输出的内存地址起始位置 0x401000
  • 参数 count:要反汇编的指令数量限制。如果设置为0,则表示没有数量限制,将会反汇编所有有效的指令
  • 参数 insn:用于存储反汇编结果的结构体数组。它是一个输出参数,由调用者分配内存。用于输出数据的一个指针

如上所示的cs_open()以及cs_disasm()两个函数如果能搞明白,那么反汇编完整代码即可写出来了,根据如下流程实现;

  • 创建一个句柄 handle,用于连接到驱动程序。
  • 定义 ProcessData 结构体,包含需要读取的进程 ID、起始地址、读取的字节数以及存储读取结果的 BYTE 数组。
  • 使用 DeviceIoControl() 函数从指定进程读取机器码,将结果存储到 data 结构体的 data 字段中。
  • 使用 cs_open() 函数打开 Capstone 引擎的句柄 dasm_handle,指定了架构为 x86 平台,模式为 32 位。
  • 使用 cs_disasm() 函数将 data 结构体中的机器码进行反汇编,将结果存储到 insn 数组中,同时返回反汇编指令的数量 count。
  • 循环遍历 insn 数组,将每个反汇编指令的地址、长度、助记符和操作数打印出来。
  • 使用 cs_free() 函数释放 insn 数组占用的内存。
  • 使用 cs_close() 函数关闭 Capstone 引擎的句柄 dasm_handle。
  • 关闭连接到驱动程序的句柄 handle

根据如上实现流程,我们可以写出如下代码片段;

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <iostream>
#include <inttypes.h>
#include <capstone/capstone.h>#pragma comment(lib,"capstone64.lib")#define READ_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ALL_ACCESS)
#define WRITE_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_ALL_ACCESS)typedef struct
{DWORD pid;UINT64 address;DWORD size;BYTE* data;
}ProcessData;int main(int argc, char* argv[])
{// 连接到驱动HANDLE handle = CreateFileA("\\??\\ReadWriteSymbolName", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);ProcessData data;DWORD dwSize = 0;// 指定需要读写的进程data.pid = 6932;data.address = 0x401000;data.size = 64;// 读取机器码到BYTE字节数组data.data = new BYTE[data.size];DeviceIoControl(handle, READ_PROCESS_CODE, &data, sizeof(data), &data, sizeof(data), &dwSize, NULL);for (int i = 0; i < data.size; i++){printf("0x%02X ", data.data[i]);}printf("\n");// 开始反汇编csh dasm_handle;cs_insn *insn;size_t count;// 打开句柄if (cs_open(CS_ARCH_X86, CS_MODE_32, &dasm_handle) != CS_ERR_OK){return 0;}// 反汇编代码count = cs_disasm(dasm_handle, (unsigned char *)data.data, data.size, data.address, 0, &insn);if (count > 0){size_t index;for (index = 0; index < count; index++){/*for (int x = 0; x < insn[index].size; x++){printf("机器码: %d -> %02X \n", x, insn[index].bytes[x]);}*/printf("地址: 0x%"PRIx64" | 长度: %d 反汇编: %s %s \n", insn[index].address, insn[index].size, insn[index].mnemonic, insn[index].op_str);}cs_free(insn, count);}cs_close(&dasm_handle);getchar();CloseHandle(handle);return 0;
}

通过驱动加载工具加载WinDDK.sys然后在运行本程序,你会看到正确的输出结果,反汇编当前位置处向下64字节。

3.6.2 内存汇编的应用

实现了反汇编接着就需要讲解如何对内存进行汇编操作,汇编引擎这里采用了XEDParse该引擎小巧简洁,著名的x64dbg就是在运用本引擎进行汇编替换的,XEDParse 是一个开源的汇编引擎,用于将汇编代码转换为二进制指令。它基于Intel的XED库,并提供了一些易于使用的接口。

  • 汇编引擎GitHub地址:https://github.com/x64dbg/XEDParse

一般而言,再进行汇编转换之前需要做如下几个步骤的工作;

1.定义xed_state_t结构体,该结构体包含有关目标平台的信息,例如处理器架构和指令集。可以使用xed_state_zero()函数来初始化该结构体。

xed_state_t state;
xed_state_zero(&state);
state.mmode = XED_MACHINE_MODE_LONG_64;
state.stack_addr_width = XED_ADDRESS_WIDTH_64b;

2.定义xed_error_enum_t类型的变量来接收转换过程中可能出现的错误信息。

xed_error_enum_t error = XED_ERROR_NONE;

3.定义xed_encoder_request_t结构体,该结构体包含要转换的汇编指令的信息,例如操作码和操作数。

xed_encoder_request_t request;
xed_encoder_request_zero_set_mode(&request, &state);
request.iclass = XED_ICLASS_MOV;
request.operand_order[0] = 0;
request.operand_order[1] = 1;
request.operands[0].name = XED_REG_RAX;
request.operands[1].name = XED_REG_RBX;

4.使用XEDParseAssemble()函数将汇编代码转换为二进制指令,并将结果存储在xed_uint8_t类型的数组中。此函数返回转换后的指令长度。

xed_uint8_t binary[15];
xed_uint_t length = XEDParseAssemble(&request, binary, sizeof(binary), &error);
if (error != XED_ERROR_NONE) {// handle error
}

5.使用转换后的二进制指令进行后续操作。

typedef int (*func_t)(void);
func_t func = (func_t)binary;
int result = func();

在本次转换流程中我们只需要向XEDParseAssemble()函数传入一个规范的结构体即可完成转换,通过向XEDPARSE结构传入需要转换的指令,并自动转换为机器码放入到data.data堆中,实现核心代码如下所示;

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <iostream>extern "C"
{
#include "D:/XEDParse/XEDParse.h"
#pragma comment(lib, "D:/XEDParse/XEDParse_x64.lib")
}using namespace std;#define READ_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ALL_ACCESS)
#define WRITE_PROCESS_CODE CTL_CODE(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_ALL_ACCESS)typedef struct
{DWORD pid;UINT64 address;DWORD size;BYTE* data;
}ProcessData;int main(int argc, char* argv[])
{// 连接到驱动HANDLE handle = CreateFileA("\\??\\ReadWriteSymbolName", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);ProcessData data;DWORD dwSize = 0;// 指定需要读写的进程data.pid = 6932;data.address = 0x401000;data.size = 0;XEDPARSE xed = { 0 };xed.x64 = FALSE;// 输入一条汇编指令并转换scanf_s("%llx", &xed.cip);gets_s(xed.instr, XEDPARSE_MAXBUFSIZE);if (XEDPARSE_OK != XEDParseAssemble(&xed)){printf("指令错误: %s\n", xed.error);}// 生成堆data.data = new BYTE[xed.dest_size];// 设置长度data.size = xed.dest_size;for (size_t i = 0; i < xed.dest_size; i++){// 替换到堆中printf("%02X ", xed.dest[i]);data.data[i] = xed.dest[i];}// 调用控制器,写入到远端内存DeviceIoControl(handle, WRITE_PROCESS_CODE, &data, sizeof(data), &data, sizeof(data), &dwSize, NULL);printf("[LyShark] 指令集已替换. \n");getchar();CloseHandle(handle);return 0;
}

通过驱动加载工具加载WinDDK.sys然后在运行本程序,你会看到正确的输出结果,可打开反内核工具验证是否改写成功。

打开反内核工具,并切换到观察是否写入了一条mov eax,1的指令集机器码,如下图已经完美写入。

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

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

相关文章

4.1 Windows驱动开发:内核中进程与句柄互转

在内核开发中&#xff0c;经常需要进行进程和句柄之间的互相转换。进程通常由一个唯一的进程标识符&#xff08;PID&#xff09;来标识&#xff0c;而句柄是指对内核对象的引用。在Windows内核中&#xff0c;EProcess结构表示一个进程&#xff0c;而HANDLE是一个句柄。 为了实…

实时数仓-Flink使用总结

阿里云实时计算Flink版是阿里云基于Apache Flink构建的企业级、高性能实时大数据处理系统。具备一站式开发运维管理平台&#xff0c;支持作业开发、数据调试、运行与监控、自动调优、智能诊断等全生命周期能力。本期将对Flink的使用进行总结。 1. Flink产品回顾 阿里云实时计算…

python趣味编程-5分钟实现一个Flappy Bird游戏(含源码、步骤讲解)

Python 中的 Flappy Bird 游戏可以免费下载开源代码,它是为想要学习 Python 的初学者创建的。 该项目系统使用了 Pygame 和 Random 模块。 Pygame 是一组跨平台的 Python 模块,专为编写视频游戏而设计。 Python 中的 Flappy Bird 代码 – 项目信息 项目名称:Python 中的 Fl…

2023年中国骨质疏松治疗仪发展趋势分析:小型且智能将成为产品优化方向[图]

骨质疏松治疗仪利用磁场镇静止痛、消肿消炎的治疗作用迅速缓解患者腰背疼痛等骨质疏松临床症状。同时利用磁场的磁-电效应产生的感生电势和感生电流&#xff0c;改善骨的代谢和骨重建&#xff0c;通过抑制破骨细胞、促进成骨细胞的活性来阻止骨量丢失、提高骨密度。 骨质疏松治…

2023-2024 年适用于 Windows 电脑的顶级视频录制软件

想捕捉您正在在线观看的视频吗&#xff1f;使用网络摄像头录制视频会议以供日后参考。正在寻找可以完成这些任务的视频捕捉软件&#xff1f;这篇文章说明了一切。以下是一些适用于 Windows PC 的最佳视频录制工具。 什么是视频录制软件&#xff1f; 顾名思义&#xff0c;视频捕…

【Vue 本地项目运行https服务】

配置本地开发环境的https访问 1、下载证书生成库2、创建证书颁发机构3、创建证书4、创建成功后会有4个文件在我们项目根目录5、定位到ca.crt 文件所在在位置 双击 安装证书6、在vue.config.js中引入证书&#xff1b; 1、下载证书生成库 npm install -g mkcert2、创建证书颁发机…

openGauss学习笔记-123 openGauss 数据库管理-设置账本数据库-账本数据库概述

文章目录 openGauss学习笔记-123 openGauss 数据库管理-设置账本数据库-账本数据库概述123.1 背景信息123.2 操作步骤 openGauss学习笔记-123 openGauss 数据库管理-设置账本数据库-账本数据库概述 123.1 背景信息 账本数据库融合了区块链思想&#xff0c;将用户操作记录至两…

合肥数字孪生赋能工业制造,加速推进制造业数字化转型

聚焦国家战略需求和先进制造业发展方向&#xff0c;加快数字化发展战略部署&#xff0c;数字孪生、工业互联网、工业物联网已被广泛认为是工业革命的新引擎。合肥数字孪生正在推动工业制造从制造转向智造。通过数字化建模和仿真的方式&#xff0c;优化设计、生产、质量管理、供…

Spring Task使用介绍

文章目录 Spring Task介绍cron表达式入门案例Spring Task使用步骤全注解的方式代码开发测试结果 代码仓库 Spring Task 介绍 Spring Task 是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑。 定位定时任务框架 作用定时自动执行某段Java…

centos的root密码忘记或失效的解决办法

目录 前言1 单机维护模式2 利用具有管理员权限的用户切换到root用户3 救援模式 前言 在Linux系统中&#xff0c;root用户是最高权限的用户&#xff0c;可以执行任何命令和操作。但是&#xff0c;如果我们忘记了root用户的密码&#xff0c;或者需要修改root用户的密码&#xff…

.NET8.0 AOT 经验分享 FreeSql/FreeRedis/FreeScheduler 均已通过测试

2023年11月15日&#xff0c;对.net的开发圈是一个重大的日子&#xff0c;.net 8.0正式版发布。 圈内已经预热了有半个月有余&#xff0c;性能不断超越&#xff0c;开发体验越来越完美&#xff0c;早在.net 5.0的时候就各种吹风Aot编译&#xff0c;直到6.0 7.0使用仍然比较麻烦…

什么是PWA(Progressive Web App)?它有哪些特点和优势?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

【PB续命05】WinHttp.WinHttpRequest的介绍与使用

0 WinHttp.WinHttpRequest简介 winhttp.winhttprequest是Windows操作系统中的一个API函数&#xff0c;用于创建和发送HTTP请求。它可以用于从Web服务器获取数据&#xff0c;或将数据发送到Web服务器。该函数提供了许多选项&#xff0c;例如设置请求头、设置代理服务器、设置超…

Js:获取最近6个月的月份(包含本月、不包含本月)

一、需求 获取最近6个月的月份&#xff08;不包含本月&#xff09;&#xff0c;比如现在是11月份&#xff0c;则需要获取到的月份是&#xff1a;10、9、8、7、6、5将月份从小到大排列 二、解决 1、获取最近的6个月份&#xff08;不包含本月&#xff09; var monthALL[]; …

Excel-快速将公式运用到一整列

先在该列的第一个单元格里写好公式&#xff0c;然后单击该单元格 在图中标示的地方输入我们需要填充的单元格区域 同时按住Ctrl和Enter键&#xff0c;这时需要填充的单元格区域就都被选中了 然后单击一下图中公式的后面&#xff0c;再次按下Ctrl和Enter键&#xff0c;这样就完…

注册表单mvc

jsp给我们的ControllerServlet 1在哪看到我们的数据呢 2什么时候用了session,有什么用 register.jsp 获取表单的name,email formBean.name是怎么定义的 3为什么就可以formbean访问 要使用的jsp对象都在servlet里面用setAttribute定义的 request.getSession().setAttribute…

云服务器windows service2022 部署git服务器

1 安装 下载地址gitblit 解压到你的一个目录,我这里给的是C:\gitblit 根据官网提示要下载jre or jdk7.0,这里建议使用下载jre (jdk 有时候运行出问题,或者2个都安装),自行安装java,这里不做环境配置的说明 ==================================== 进入c:\gitblit\data 目录里面…

全栈工程师必须要掌握的前端Html技能

作为一名全栈工程师&#xff0c;在日常的工作中&#xff0c;可能更侧重于后端开发&#xff0c;如&#xff1a;C#&#xff0c;Java&#xff0c;SQL &#xff0c;Python等&#xff0c;对前端的知识则不太精通。在一些比较完善的公司或者项目中&#xff0c;一般会搭配前端工程师&a…

SpringBoot实现IP地址归属地查询

SpringBoot实现IP地址归属地查询 功能特性 标准化的数据格式 每个 IP 数据段的 region 信息都固定了格式&#xff1a; 国家|区域|省份|城市|ISP&#xff0c;只有中国的数据绝大部分精确到了城市&#xff0c;其他国家部分数据只能定位到国家&#xff0c;后前的选项全部是 0。…

React父组件怎么调用子组件的方法

调用方法&#xff1a;1、类组件中的调用可以利用React.createRef()、ref的函数式声明或props自定义onRef属性来实现&#xff1b;2、函数组件、Hook组件中的调用可以利用useImperativeHandle或forwardRef抛出子组件ref来实现。 【程序员必备开发工具推荐】Apifox一款免费API管理…