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

news2024/11/28 14:28:37

在笔者上一篇文章《内核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;       // 进程PID
    UINT64 address;  // 读写地址
    DWORD size;      // 读写长度
    BYTE* data;      // 读写数据集
}ProcessData;

// MDL读取封装
BOOLEAN ReadProcessMemory(ProcessData* ProcessData)
{
    BOOLEAN bRet = TRUE;
    PEPROCESS process = NULL;

    // 将PID转为EProcess
    PsLookupProcessByProcessId(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转为EProcess
    PsLookupProcessByProcessId(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.coloradmin.cn/o/1218566.html

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

相关文章

滚雪球学Java(09-2):Java中的关系运算符,你真的掌握了吗?

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

java“贪吃蛇”小游戏

基于java实现贪吃蛇小游戏&#xff0c;主要通过绘制不同的图片并以一定速度一帧一帧地在窗体上进行展示。 我是在javaSwing项目下创建了一个包 名字叫做&#xff1a;Snakes包 包下有一个启动类和一个设置代码的主界面两个类 代码主界面&#xff1a; 代码主界面主要讲解的是 …

2023年中国冲击波治疗仪市场发展趋势分析:未来市场增长空间更大[图]

冲击波在临床医学领域最早应用于体外冲击波碎石&#xff0c;在二十世纪八十年代末期&#xff0c;体外冲击波碎石技术开始被运用到骨科及康复理疗领域&#xff0c;经过十余年的临床研究&#xff0c;冲击波疗法日益完善&#xff0c;应用范围也日益扩大。冲击波作为一种介于保守疗…

【Linux】Linux进程间通信(二)

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;Linux &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【Linux】…

【开源】基于Vue和SpringBoot的固始鹅块销售系统

项目编号&#xff1a; S 060 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S060&#xff0c;文末获取源码。} 项目编号&#xff1a;S060&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 鹅块类型模块2.3 固…

Elasticsearch:运用向量搜索通过图像搜索找到你的小狗

作者&#xff1a;ALEX SALGADO 你是否曾经遇到过这样的情况&#xff1a;你在街上发现了一只丢失的小狗&#xff0c;但不知道它是否有主人&#xff1f; 了解如何使用向量搜索或图像搜索来做到这一点。 通过图像搜索找到你的小狗 您是否曾经遇到过这样的情况&#xff1a;你在街…

【自留地】前端 - uniapp - Vue - React - Flutter

uniapp uniapp自用速查表 - 我的常用组件 uniapp自用速查表 - 我的常用组件_uniapp static/customicons.css-CSDN博客文章浏览阅读1.8k次。uniapp项目登录退出、全局变量与状态、本地存储、Tabbar标签栏、顶部导航栏、下拉刷新、触底刷新、Ajax交互、内置组件样式修改、自定义…

2018年五一杯数学建模C题江苏省本科教育质量综合评价解题全过程文档及程序

2019年五一杯数学建模 C题 江苏省本科教育质量综合评价 原题再现 随着中国的改革开放&#xff0c;国家的综合实力不断增强&#xff0c;中国高等教育发展整体已进入世界中上水平。作为一个教育大省&#xff0c;江苏省的本科教育发展在全国名列前茅&#xff0c;而江苏省13个地级…

Linux:进程替换和知识整合

文章目录 进程程序替换替换原理进程替换的理解 环境变量与进程替换命令行解释器实现逻辑 进程程序替换 前面已经学习了子进程的创建&#xff0c;但是子进程的创建不管怎么说&#xff0c;都是父进程代码的一部分&#xff0c;那么实际上如果想要子进程执行新的程序呢&#xff1f…

2023年中国脑电仿生电刺激仪发展趋势分析:智能化、精准化、使用感不断提高[图]

脑电仿生电刺激仪是一种通过直接数字频率合成技术合成脑电仿真低频生物电流&#xff0c;通过粘贴于两耳侧乳突、太阳穴或风池穴部位表皮的电极&#xff0c;用仿生物电自颅外无创伤地穿透颅骨屏障刺激小脑顶核区的电疗设备。此电流刺激可启动颅脑固有神经保护机制&#xff0c;改…

【MATLAB】史上最全的9种数据拟合算法全家桶

有意向获取代码&#xff0c;请转文末观看代码获取方式~ 大家吃一顿火锅的价格便可以拥有9种数据拟合算法&#xff0c;绝对不亏&#xff0c;知识付费是现今时代的趋势&#xff0c;而且都是我精心制作的教程&#xff0c;有问题可随时反馈~也可单独获取某一算法的代码&#xff08…

Odoo 15开发手册第六章 模型 - 结构化应用数据

本章我们更进一步学习模型层&#xff0c;以及如何使用模型来设计支撑应用的数据结构。我们会探讨可用的模型类型&#xff0c;以及在使用这些类型时如何定义强制进行数据验证的约束。 模型由支持不同数据类型的数据字段组成&#xff0c;一些字段类型支持定义模型间的关联。对于…

delphi电子处方流转 sm2 sm4(医院)

【delphi电子处方流转(医院) sm2 sm4】支持 就诊登记、电子处方上传预核验、处方处方医保电子签名、电子处方上传、电子处方撤销、电子处方信息查询、电子处方审核结果查询、电子处方取药结果查询、电子处方药品目录查询等功能。技术交流Q 648437169 下载链接&#xff1a;http…

4. hdfs高可用集群搭建

简介 前面把hadoop机器已经准备好了&#xff0c;zk集群搭建好了&#xff0c;本本就是开始搭建hdfs环境 hadoop环境准备 创建hadoop用户 三台机器都创建hadoop用户 useradd hadoop -d /home/hadoop echo "1q1w1e1r" | passwd --stdin hadoophadoop用户相互免密登…

语义检索系统【全】:基于milvus语义检索系统指令全流程-快速部署版

搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术细节以及项目实战(含码源) 专栏详细介绍:搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术…

Linux操作系统基础 – 正则表达式快速入门

Linux操作系统基础 – 正则表达式快速入门 Linux Operating System Essentials - Introduction to Regular Expressions 通常在计算机科学领域&#xff0c;正则表达式被解释为对字符串操作的一种逻辑公式&#xff0c;即用事先定义好的特定字符及其组合组成所谓的“规则字符串”…

SpringBoot使用DevTools实现后端热部署

&#x1f4d1;前言 本文主要SpringBoot通过DevTools实现热部署的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f304;每日一句&…

GZ038 物联网应用开发赛题第10套

2023年全国职业院校技能大赛 高职组 物联网应用开发 任 务 书 &#xff08;第10套卷&#xff09; 工位号&#xff1a;______________ 第一部分 竞赛须知 一、竞赛要求 1、正确使用工具&#xff0c;操作安全规范&#xff1b; 2、竞赛过程中如有异议&#xff0c;可向现场考…

ai剪辑矩阵系统源码+无人直播系统源码技术开发

开发AI剪辑矩阵系统和无人直播系统源码&#xff0c;需要以下步骤&#xff1a; 1. 市场调研&#xff1a;了解市场需求和竞品情况&#xff0c;明确系统的功能和特点。 2. 系统设计&#xff1a;设计系统的整体架构和功能模块&#xff0c;包括视频剪辑、直播推流、实时互动、数据分…

【Linux】Ubuntu16.04下安装python高版本--源码安装

Ubuntu16.04下完美安装python高版本及对应版本的pip 方法一:直接用命令安装python3.6&#xff08;但我没安装成功&#xff09; 好像是因为Ubuntu16.04的软件仓库&#xff08;源&#xff09;中python的最高版本就是python3.5&#xff0c;所以无法直接用apt来安装 #方法一 sudo…