Windows和Linux动态注入

  摘要:最近对动态注入有一些兴趣因此搜索了些资料,简单整理了下相关的技术实现。本文只能够带你理解何如注入以及大概如何实现,对注入的方法描述的并不详细。
  关键字:dll注入,hook,提权
  读者须知:读者需要对Windows和Linux dll加载的基本流程比较熟悉。

  注入就是将自己的代码注入到目标进程中强制目标进程执行,而动态注入就是将动态库强制加载进目标进程的进程空间从而对目标进程进行修改。动态注入有利有弊,可以用于反病毒、反外挂也可以用于投毒或者制作外挂。

1 DLL注入

  DLL是windows平台的动态库,一些Windows的基础功能都是以DLL的形式提供的比如Kernel32.dll、GDI32.dll等。开发者将开发的功能按照模块拆分为不同的动态库,再程序需要使用时再将对应的动态库加载到进程的进程地址空间。而DLL注入就是将一个DLL二进制文件注入加载到目标进程中,强制目标进程执行对应DLL的代码的过程。
  DLL注入的实现方式比较多,比如消息Hook注入,输入法注入,远程线程注入,APC注入,EIP注入,注册表注入等,这里简单描述几种。

1.1 Hook注入

  Windows应用基于消息机制,每个UI程序都有自己的消息队列以及对应的消息处理函数。Windows上可以利用系统提供的Hook API来截获对应的消息。
  SetWindowsHookEx可以用来注册某个消息的回调函数。如果应用触发了注册的消息类型就会调用用户指定的回调函数从而劫持消息,在这里就可以对数据进行二次加工或者中间人攻击。API的详情见MSDN-SetWindowsHookEX。
  进程与进程之间是互相独立隔离的,因此我们需要将Hook的代码写在DLL中,通过将该DLL加载到目标进程的进程空间来进行消息劫持。需要注意的是,当我们安装了全局钩子后,只要进程发出钩子可以劫持的消息,操作系统就会将DLL加载到目标进程中,也就是说实际的装载任务不需要我们来做,我们只要注册全局钩子即可。

全局Hook注入实验
  首先,我们需要完成注册全局钩子和注销的相关实现。具体实现比较简单就是调用对应API进行钩子注册和注销以及输出一些简单的帮助信息方便查看当前进程信息,这里不对具体的API进行描述,需要了解的建议直接看MSDN,非常详细。需要注意的是在处理Hook的消息时不要重复触发已经Hook的消息,这样就会出现无限递归,除非电脑注销否则关不掉进程。

//dllmain.cpp
HMODULE gDllModule = nullptr;
BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved){
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
	{
		gDllModule = hModule;
	}break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

//myhook.h
#pragma once
#include <windows.h>
extern "C" {
	_declspec(dllexport) BOOL registerHook();
	_declspec(dllexport) BOOL unregisterHook();
	_declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
}

//myhook.cpp
#define _CRT_SECURE_NO_WARNINGS
#include "myhook.h"
#include <cstdio>

extern HMODULE gDllModule;
#pragma data_seg("hook_data")
HHOOK gHookHandle = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:hook_data,RWS")

bool hooked = false;
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam) {
	FILE *fp = fopen("G://1.txt", "a+");
	char buff[1024]{};
	char    processFullName[_MAX_PATH] = { 0 };
	DWORD dwpid = GetCurrentProcessId();
	GetModuleFileNameA(NULL, processFullName, _MAX_PATH);

	sprintf(buff, "process %s warning hook is coming %d code is %d WParam is %d, LParam is %d\n", processFullName, hooked, code, wParam, lParam);
	fwrite(buff, strlen(buff), 1, fp);
	fclose(fp);
	printf("%s", buff);
	if (!hooked) {
		//MessageBox(NULL, L"warning hook is coming", L"hook", 0);
		hooked = true;
	}

	return ::CallNextHookEx(gHookHandle, code, wParam, lParam);
}

BOOL registerHook() {
	gHookHandle = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, gDllModule, 0);
	return !!gHookHandle;
}

BOOL unregisterHook() {
	gHookHandle ? UnhookWindowsHookEx(gHookHandle) : TRUE;
	return TRUE;
}

  然后我们只需要手动将DLL加载起来运行注册钩子的函数即可。

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

typedef BOOL(*registerHook)();
typedef BOOL(*unregisterHook)();

int main(){
	HMODULE hmodule = LoadLibraryW(TEXT("E:\\code\\tmp\\dll\\dllhook\\Debug\\dllhook.dll"));
	if (!hmodule) {
		printf("can not load library, error code is %d\n", GetLastError());
		exit(1);
	}

	registerHook rh = (registerHook)GetProcAddress(hmodule, "registerHook");
	if (!rh) {
		printf("can not get the register hook address, error code is %d\n", GetLastError());
		exit(1);
	}

	unregisterHook uh = (unregisterHook)GetProcAddress(hmodule, "unregisterHook");
	if (!uh) {
		printf("can not get the unregister hook address, error code is %d\n", GetLastError());
		exit(1);
	}

	printf("load and get process address succ\n");

	auto ret = rh();
	if (!ret) {
		printf("register hook failed\n");
		exit(1);
	}

	MessageBox(NULL, L"warning hook is coming", L"hook", 0);
	while (true) {
	}
	ret = uh();
	if (!ret) {
		printf("unregister hook failed\n");
		exit(1);
	}

	printf("process running succ\n");
	return 0;
}

  下面是Hook成功的一部分日志,可以看到VS和Everthing的消息都劫持到了。

process D:\Microsoft Visual Studio\2017\Community\Common7\IDE\devenv.exe warning hook is coming 1 code is 0 WParam is 1, LParam is 11530596
process D:\Everything\Everything.exe warning hook is coming 1 code is 0 WParam is 0, LParam is 13630404
process D:\Microsoft Visual Studio\2017\Community\Common7\IDE\devenv.exe warning hook is coming 1 code is 0 WParam is 1, LParam is 11531188

  我们看下Everything进程空间加载的DLL,可以看到我们的DLL已经在进程中了。
在这里插入图片描述

1.2 Remote Thread注入

  Remote Thread就是通过创建远程线程,通过该线程对需要注入的DLL在目标进程空间中加载。
  实现的步骤比较简单,我们需要准备三个产物,需要注入的DLL,注入程序,被注入的Rookie。被注入的DLL不用说了自己想怎么写都行,重点是注入程序,注入的过程和Hook不同,这个需要我们自己主动Load,具体过程如下:

  1. 根据目标进程的名称找到进程ID,如果知道进程ID忽略这一步即可;
  2. 打开目标进程,在目标进程上申请一块内存;
  3. 获取目标进程空间中的LoadLibraryW的函数地址;
  4. 创建远程线程调用LoadLibraryW加载需要注入的DLL;
  5. 打扫现场。

  过程比较简单,但是重点是得有目标进程的权限。
  肉鸡进程和注入程序(就是在Attach时显示了一个框)就不展示了,下面是注入的代码:

#include <iostream>
#include <windows.h>
#include <TlHelp32.h>
#include <tchar.h>
//生成系统进程快照,遍历快照根据名字查找对应进程的ID
DWORD fetchProcessID(LPCTSTR processName) {
	HANDLE handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (handle == INVALID_HANDLE_VALUE) {
		printf("can not get the snapshot of process\n"); exit(1);
	}

	PROCESSENTRY32 pe{};
	pe.dwSize = sizeof(pe);
	if (!Process32First(handle, &pe)) {
		printf("can not fetch the process information\n"); exit(1);
	}

	DWORD ret{};
	do {
		if (!lstrcmp(pe.szExeFile, processName)) {
			ret = pe.th32ProcessID; break;
		}
	} while (Process32Next(handle, &pe));
	CloseHandle(handle);
	return ret;
}
//远程线程注入目标DLL
BOOL createRemoteThreadAndInject(DWORD pid, LPCWSTR dllName) {
	HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (!processHandle) {
		printf("can not open process %d\n", pid); exit(1);
	}

	DWORD sz = (_tcslen(dllName) + 1) * sizeof(TCHAR);
	LPVOID palloc = VirtualAllocEx(processHandle, NULL, sz, MEM_COMMIT, PAGE_READWRITE);
	if (!palloc) {
		printf("can not allocate memory on process %d\n", pid); exit(1);
	}

	if (!WriteProcessMemory(processHandle, palloc, dllName, sz, NULL)) {
		printf("can not write memory on process %d\n", pid); exit(1);
	}

	HANDLE pfunc = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
	if (!pfunc) {
		printf("can not fetch LoadLibraryW address on process %d\n", pid); exit(1);
	}

	LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pfunc;
	HANDLE  hThread = ::CreateRemoteThread(processHandle, NULL, 0, addr, palloc, 0, NULL);
	if (!hThread){
		printf("CreateRemoteThread - Error!");
		return FALSE; 
	}
	DWORD DllAddr = 0;
	WaitForSingleObject(hThread, -1);
	GetExitCodeThread(hThread, &DllAddr);
	VirtualFreeEx(processHandle, palloc, sz, MEM_DECOMMIT);
	::CloseHandle(processHandle);
	return TRUE;
}

int main(){
	DWORD pid = fetchProcessID(L"remote_hook_rookie.exe");
	createRemoteThreadAndInject(pid, L"E:\\code\\tmp\\dll\\remote_thread_hook\\Debug\\remote_thread_hook.dll");
}

1.3 突破Session 0远程线程注入

Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。Windows只使用其中的两个级别RING0和RING3,RING0只给操作系统用,RING3谁都能用。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。

ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之…… 拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。

  Windows的服务和应用程序运行与Session之上,Windows内核6.0之前服务运行于第一个启动运行的Session 0上,该用户的应用程序也运行在Session 0上,后续的用户的应用程序分别运行于Seccion 2,Session 3,…,Session n上。由于有些服务会提权运行,将应用程序和服务运行于相同的Session有安全风险。因此Windos Vista之后只有服务可以托管到Session 0上,应用程序都托管到后续的Session,这样可以将应用程序和服务隔离提高安全性。因此使用CreateRemoteThread进行远程线程注入的时候会遇到权限问题。
  可以通过一些底层的一些API对当前进程进行提权,然后进行注入,如下:

// session_0_inject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <Windows.h>
#include <stdio.h>
#include <iostream>

#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
	PHANDLE ThreadHandle,
	ACCESS_MASK DesiredAccess,
	LPVOID ObjectAttributes,
	HANDLE ProcessHandle,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	ULONG CreateThreadFlags,
	SIZE_T ZeroBits,
	SIZE_T StackSize,
	SIZE_T MaximumStackSize,
	LPVOID pUnkown);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
	PHANDLE ThreadHandle,
	ACCESS_MASK DesiredAccess,
	LPVOID ObjectAttributes,
	HANDLE ProcessHandle,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	BOOL CreateSuspended,
	DWORD dwStackSize,
	DWORD dw1,
	DWORD dw2,
	LPVOID pUnkown);
#endif

void ShowError(const char* pszText){
	char szError[MAX_PATH] = { 0 };
	::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
	::MessageBox(NULL, szError, "ERROR", MB_OK);
}

// 提权函数
BOOL EnableDebugPrivilege(){
	HANDLE hToken;
	BOOL fOk = FALSE;
	if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)){
		TOKEN_PRIVILEGES tp;
		tp.PrivilegeCount = 1;
		LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);

		tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
		AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);

		fOk = (GetLastError() == ERROR_SUCCESS);
		CloseHandle(hToken);
	}

	return fOk;
}


// 使用 ZwCreateThreadEx 实现远线程注入
BOOL ZwCreateThreadExInjectDll(DWORD PID, const char* pszDllFileName){
	HANDLE hProcess = NULL;
	SIZE_T dwSize = 0;
	LPVOID pDllAddr = NULL;
	FARPROC pFuncProcAddr = NULL;
	HANDLE hRemoteThread = NULL;
	DWORD dwStatus = 0;

	EnableDebugPrivilege();

	// 打开注入进程,获取进程句柄
	hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
	if (!hProcess){
		printf("OpenProcess - Error!\n\n");
		return -1;
	}
	// 在注入的进程申请内存地址

	dwSize = lstrlen(pszDllFileName) + 1;
	pDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
	if (!pDllAddr){
		ShowError("VirtualAllocEx - Error!\n\n");
		return FALSE;
	}
	//写入内存地址

	if (!WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
	{
		ShowError("WriteProcessMemory - Error!\n\n");
		return FALSE;
	}
	//加载ntdll
	HMODULE hNtdllDll = LoadLibrary("ntdll.dll");
	if (!hNtdllDll){
		ShowError("LoadLirbary");
		return FALSE;
	}
	// 获取LoadLibraryA函数地址
	pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
	if (!pFuncProcAddr){
		ShowError("GetProcAddress_LoadLibraryA - Error!\n\n");
		return FALSE;
	}

	//获取ZwCreateThreadEx函数地址
	typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
	if (!ZwCreateThreadEx){
		ShowError("GetProcAddress_ZwCreateThread - Error!\n\n");
		return FALSE;
	}
	// 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入
	dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
	if (!ZwCreateThreadEx){
		ShowError("ZwCreateThreadEx - Error!\n\n");
		return FALSE;
	}
	// 关闭句柄
	::CloseHandle(hProcess);
	::FreeLibrary(hNtdllDll);

	return TRUE;
}

int main(int argc, char* argv[]){
#ifdef _WIN64
	BOOL bRet = ZwCreateThreadExInjectDll(4924, "E:\code\tmp\dll\remote_thread_hook\Debug\remote_thread_hook.dll");
#else 
	BOOL bRet = ZwCreateThreadExInjectDll(15596, "E:\\code\\tmp\\dll\\remote_thread_hook\\Debug\\remote_thread_hook.dll");
#endif
	if (!bRet){
		printf("Inject Dll Error!\n\n");
	}else {
		printf("Inject Dll OK!\n\n");
	}
	return 0;
}

2 Linux 动态注入

  Linux下的动态注入相对比较简单,有两种方式:

  1. 符号表劫持。Linux上ld记载动态库之前会先加载LD_PRELOAD定义的动态库,而符号解析是按照库的载入顺序来的,也就是现在入的库的符号会覆盖后载入的库的符号,利用这种方式我们就可以劫持符号,但是意义不大;
  2. 利用gdb或者lldb手动重定位期望劫持的API的地址。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Springboot+Vue的校园招聘系统(进阶版)

本项目是一年前写的一个项目的升级版&#xff0c;因为某些原因将它作了一个升级改进&#xff0c; 好多兄弟来问有没有演示&#xff0c;现在先来写个说明&#xff01;&#xff01;&#xff01; 目录 一. &#x1f981; 前言二. &#x1f981; 开源代码与组件使用情况说明三. &am…

Qt之事件过滤器讲解并且实现快捷键切换鼠标焦点

目录 1、需求背景2、使用Qt键盘事件3、安装事件过滤器4、事件处理级别 1、需求背景 现在有一个类似于下方图的ui&#xff0c;用户需要在输入前一行内容后&#xff0c;需要摁下指定案件能够跳转到下一行继续进行输入。 2、使用Qt键盘事件 一种更为直接的解决方案是子类化QLi…

如何在 Linux 中安装、设置和使用 SNMP?

概要 SNMP&#xff08;Simple Network Management Protocol&#xff09;是一种用于管理和监控网络设备的协议。它允许网络管理员通过远程方式收集设备的运行状态、性能数据和错误信息&#xff0c;以便进行故障排除和网络优化。在Linux系统中&#xff0c;我们可以安装、设置和使…

IDEA配置本地Maven详细教程

IDEA配置本地Maven详细教程 一、下载二、安装三、配置环境变量四、IDEA配置Maven 一、下载 官网下载&#xff1a;点击下载 网盘下载&#xff1a;点击下载 二、安装 将下载后的zip文件&#xff08;免安装版&#xff09;解压到自己想要放的位置&#xff0c;&#xff0c;我这里…

对MVVM和MVC开发模式的理解

对MVVM和MVC开发模式的理解 1、MVVM2、MVC3、MVVM与MVC的区别 1、MVVM MVVM最早由微软提出来&#xff0c;它借鉴了桌面应用程序的MVC思想&#xff0c;在前端页面中&#xff0c;把Model用纯JavaScript对象表示&#xff0c;View负责显示&#xff0c;两者做到了最大限度的分离&am…

UE4/5 通过Control rig的FullBody【蜘蛛模型,不用basic ik】

目录 根设置 FullBody IK 额外骨设置 ​编辑 晃动效果 根设置 第一步你需要准备一个蜘蛛模型&#xff0c;不论是官方示例或者是epic上购买的模型 然后我用的是epic上面购买的一个眼球蜘蛛&#xff1a; 第一步&#xff0c;我们从根创建一个空项【这个记得脱离父子级到root之…

多传感器时频信号处理:多通道非平稳数据的分析工具(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

四、用户管理

云尚办公系统&#xff1a;用户管理 B站直达【为尚硅谷点赞】: https://www.bilibili.com/video/BV1Ya411S7aT 本博文以课程相关为主发布&#xff0c;并且融入了自己的一些看法以及对学习过程中遇见的问题给出相关的解决方法。一起学习一起进步&#xff01;&#xff01;&#x…

【服务器远程工具】一款好用的xshell

这里写目录标题 背景Tabby简介安装使用SSHSFTPPowerShellGit 设置外观颜色快捷键窗口 插件支持总结 背景 作为一名后端开发&#xff0c;我们经常需要和Linux系统打交道&#xff0c;免不了要使用Xshell这类终端工具来进行远程管理。今天给大家推荐一款更炫酷的终端工具Tabby&…

【SQL应知应会】分析函数的点点滴滴(三)

欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 本文收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习&#xff0c;有基础也有进阶&#xff0c;有MySQL也有Oracle 分析函数的点点滴滴 1.什么是分析函数&#xff1a;…

计算机中CPU、内存、缓存的关系

CPU&#xff08;Central Processing Unit&#xff0c;中央处理器&#xff09; 内存&#xff08;Random Access Memory&#xff0c;随机存取存储器&#xff09; 缓存&#xff08;Cache&#xff09; CPU、内存和缓存之间有着密切的关系&#xff0c;它们共同构成了计算机系统的核…

go-cqhttp签名服务sign-server的本地搭建

新版go-cqhttp新增签名服务器签名服务器相关问题 Issue #2242 Mrs4s/go-cqhttp GitHubhttps://github.com/Mrs4s/go-cqhttp/issues/2242 其在win10系统下本地化搭建的方式为&#xff1a; 1.解压缩qq安装包提取lib里面的libfekit.so、libQSec.so文件并存放至一个文件夹&…

nodejs高版本降为低版本的详细解决方案

部分老旧项目需要使用低版本的node,网上很多是无效的,高版本无法直接安装低版本node,但是低版本nodejs可以安装部分高版本node,从而达到升级效果,下面这篇文章主要给大家介绍了关于nodejs高版本降为低版本的详细解决方案,需要的朋友可以参考下 1.首先通过控制面板应用卸载当前环…

Spark 4/5

4. 启动Spark Shell编程 4.1 什么是Spark Shell spark shell是spark中的交互式命令行客户端&#xff0c;可以在spark shell中使用scala编写spark程序&#xff0c;启动后默认已经创建了SparkContext&#xff0c;别名为sc 4.2 启动Spark Shell Shell /opt/apps/spark-3.2.3-bi…

opencv检测二维码和条形码

文章目录 1 excel制作简单二维码2 识别二维码和条形码2.1 相关库2.2 decode解码2.3 圈出二维码的位置2.4 判断二维码是否授权 3 完整代码3.1 使用图片进行识别3.2 使用摄像头实时识别 4 总结 1 excel制作简单二维码 使用excel可以实现制作二维码&#xff0c;但只能实现做英文和…

Docker容器日志管理详解

1. 简介 Docker容器日志是指容器在运行过程中产生的各种日志信息&#xff0c;包括错误、警告、信息等。Docker将所有容器的日志统一管理&#xff0c;方便用户对容器进行监控、故障排查和性能分析。 1.1 什么是Docker容器日志 Docker容器日志是容器在运行过程中产生的各种日志…

ASEMI代理NXP高压三端双向可控硅BT139-800E综合指南

编辑-Z BT139-800E是一种高压三端双向可控硅开关&#xff0c;近年来由于其卓越的性能和多功能性而广受欢迎。这种强大的半导体器件广泛应用于各种应用&#xff0c;包括电机控制、照明控制和温度调节。 BT139-800E的特点 1.高压能力&#xff1a;BT139-800E设计用于处理高压&am…

[Leetcode] 0026. 删除有序数组中的重复项

26. 删除有序数组中的重复项 点击上方&#xff0c;跳转至Leetcode 题目描述 给你一个 升序排列 的数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使每个元素 只出现一次 &#xff0c;返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。 由于在某些语…

yum安装LNMP

目录 前言 一、yum安装要用在线yum源 二、安装Nginx 1、搭建Nginx环境 2、安装yum 3、查看Nginx是否安装成功 4、设置开机自启 三、安装MySQL 1、除系统中所有以"mariadb"开头的软件包 2、安装MySQL 3、设置开机自启 4、查看MySQL初始密码 5、修改MySQL密码…

C#和LABVIEW的对决:哪种上位机编程语言更适合你?

今天&#xff0c;我们将谈论主流的上位机编程语言。你听说过C#和LABVIEW吗&#xff1f;它们是的上位机编程语言&#xff0c;C#作为自动化主流编程语言特别受欢迎&#xff0c;LABVIEW用于自动化测试&#xff0c; 首先&#xff0c;我们来了解C#语言。C#是一种文本语言&#xff0c…