Good in study, attitude and health

渗透技巧——"隐藏"注册表的更多测试

0x00 前言


在上篇文章《渗透技巧——”隐藏”注册表的创建》介绍了Poweliks使用过的注册表隐藏技术,分析原理,编写c程序实现功能

本文将做进一步测试,分享一种更为”隐蔽”的方法(该方法暂未找到公开资料,待定)

0x01 简介


本文将要介绍以下内容:

  • 使用Win32 API读取时的错误
  • “\0”放在字符串中间的情况
  • 其他Native API(如NtCreateFile)的应用
  • 更加隐蔽的利用方法
  • 防御检测

0x02 隐藏原理


对于Windows系统,”\0”(即0x0000)会被识别为字符串的结束符

所以在对该字符串读取的过程中,遇到开头的”\0”,会被解析成结束符,提前截断,导致读取错误

而使用Native API设定注册表,需要使用结构体OBJECT_ATTRIBUTES作为参数, 指定读取的字符串长度

只要长度设定正常,就能够读取正确的字符串,避免这个bug

利用的关键:

使用Native API多了一个参数,能够指定读取字符串的长度

那么,对该问题展开进一步思考,就有了如下测试

0x03 使用Win32 API读取时,具体是什么样的错误?


使用HiddenNtRegistry创建测试注册表键值,c++调用代码如下:

printf("=================Normal Key=================\n");
printf("1.CreateKey:\n");
MyCreateKey("\\Registry\\Machine\\Software\\test1");
printf("2.OpenKey:\n");
hKey = MyOpenKey("\\Registry\\Machine\\Software\\test1");
printf("3.SetValueKey:\n");
MySetValueKey(hKey,"test1","0123456789abcdef",REG_SZ);

printf("=================Hidden Key=================\n");
printf("1.OpenKey:\n");
hKey = MyOpenKey("\\Registry\\Machine\\Software\\test1");
printf("2.SetHiddenValueKey:\n");
MySetHiddenValueKey(hKey,"\0test1","hidden0123456789abcdef",REG_SZ);
printf("3.QueryHiddenValueKey:\n");
MyQueryHiddenValueKeyString(hKey,"\0test1");

程序实现以下功能:

  • 创建注册表键值test1,内容为0123456789abcdef
  • 创建注册表键值\0test1,内容为hidden0123456789abcdef

运行如下图

Alt text

使用Win32 API RegQueryValueEx尝试读取以上两个注册表键值

关键代码如下:

LONG lReturnCode = 0;
HKEY hkey;
LPCTSTR RegPath = _T("Software\\test1");
if (ERROR_SUCCESS == ::RegOpenKeyEx(HKEY_LOCAL_MACHINE, RegPath, 0, KEY_READ, &hkey))
{
	char dwValue[1024];
	DWORD dwSzType = REG_SZ;
	DWORD dwSize = sizeof(dwValue);
	lReturnCode = ::RegQueryValueEx(hkey, _T("test1"), 0, &dwSzType, (LPBYTE)&dwValue, &dwSize);   
	if(lReturnCode != ERROR_SUCCESS)
	{
		printf("lReturnCode:%d\n",lReturnCode);
		if(lReturnCode = 2)
			printf("ERROR_FILE_NOT_FOUND\n");			
			return 0;
		}
		printf("RegQueryValue:");
		for (int i=0;i<dwSize/2-1;i++)
		{
			printf("%c",dwValue[i*2]);
		}
	}
	::RegCloseKey(hkey);

读取注册表键值test1,成功获取内容

读取注册表键值\0test1,修改代码如下:

lReturnCode = ::RegQueryValueEx(hkey, _T("\0test1"), 0, &dwSzType, (LPBYTE)&dwValue, &dwSize);

读取失败,返回ERROR_FILE_NOT_FOUND

验证上文原理: 由于”\0”的作用,字符串提前被截断,识别为空字符,导致无法获得名称

接着做进一步尝试

0x04 “\0”放在字符串中间会怎样?


HiddenNtRegistry的代码为:

printf("1.OpenKey:\n");
hKey = MyOpenKey("\\Registry\\Machine\\Software\\test2");
printf("2.SetHiddenValueKey:\n");
MySetHiddenValueKey2(hKey,"test2\0abc","hidden0123456789abcdef",REG_SZ);
printf("3.QueryHiddenValueKey:\n");
MyQueryHiddenValueKeyString2(hKey,"test2\0abc");

注:

原工程HiddenNtRegistry中的MySetHiddenValueKey函数和MyQueryHiddenValueKeyString函数需要作适当修改,重新计算字符串长度,新的函数命名为MySetHiddenValueKey2MyQueryHiddenValueKeyString2

程序实现以下功能:

  • 创建注册表键值test2\0abc,内容为hidden0123456789abcdef
  • 读取注册表键值test2\0abc的内容

运行如下图

Alt text

使用regedit.exe查询该键值,弹框提示无法获取,如下图

Alt text

这里可以做一个大胆的尝试:

既然test2\0abc中的”\0”会截断字符串,那么我们再创建一个名为test2的键值会怎么样呢?

创建注册表键值test2,内容为0123456789abcdef,关键代码如下:

hKey = MyOpenKey("\\Registry\\Machine\\Software\\test2");
MySetValueKey(hKey,"test2","0123456789abcdef",REG_SZ);

再次使用regedit.exe查看注册表,有趣的事情发生了,如下图

Alt text

查询注册表键值\Registry\Machine\Software\test2不再弹框报错,而是显示两个名为test2的键值,内容均为0123456789abcdef

我们知道,注册表不允许创建两个名称相同的注册表键值,而上述测试产生的两个同名键值,实际上是因为其中的一个被错误的截断,导致显示键值名称相同,键值内容也相同,为0123456789abcdef(实际上内容为hidden0123456789abcdef)

这样我们就又多了一种”隐藏”注册表的方法,相比于之前的在首位填”\0”,这个隐藏方法最大的优点是使用regedit.exe查看该键值时不会弹框报错,隐蔽效果更好,同时又具有欺骗性,同正常键值内容相同

对比如下图

Alt text

显示键值内容为0123456789abcdef,实际上为hidden0123456789abcdef

0x05 其他Native API(如NtCreateFile)能否应用?


参考NtCreateKey的实现思路,测试其他Native API,例如NtCreateFile,在创建文件时是否存在相同问题?

使用NtCreateFile创建特殊文件: \0c:\1\test.txt,关键代码如下:

HMODULE             hModule				= NULL;  
NTCREATEFILE        NtCreateFile		= NULL; 
UNICODE_STRING      FileName			= {0};  
OBJECT_ATTRIBUTES   ObjectAttributes	= {0};  
HANDLE              hFile1				= NULL;  
IO_STATUS_BLOCK     IOsb				= {0};  
HANDLE              hFile2				= INVALID_HANDLE_VALUE;  
PWCHAR              pBuffer				= NULL;  
DWORD               dwRet				= 0;  
hModule = LoadLibrary(_T("ntdll.dll"));  
if (!hModule)     
{  
	printf("Could not GetModuleHandle of NTDLL.DLL");
	return FALSE;
}   
NtCreateFile = (NTCREATEFILE)GetProcAddress(hModule, "NtCreateFile");  
if (!NtCreateFile) 
{
	printf("Could not find NtCreateFile entry point in NTDLL.DLL");
	return FALSE;
}	
char *Path = "\\Device\\\HarddiskVolume1\\1\\test.txt";
char *TempBuff;
TempBuff = (char*)malloc(strlen(Path+2)*2);
for(int i=0;i<strlen(Path);i++)
{
	TempBuff[(i+2)*2] = Path[i];
	TempBuff[(i+2)*2+1] = 0x00;
}
TempBuff[0] = 0x00;
TempBuff[1] = 0x00;
TempBuff[2] = 0x00;
TempBuff[3] = 0x00;
FileName.MaximumLength = MAX_PATH * sizeof(WCHAR);  
FileName.Length = (strlen(Path)+2)*sizeof(WCHAR);  
FileName.Buffer = (WCHAR *)TempBuff;
FileName.Buffer[FileName.Length] = L'\0';
InitializeObjectAttributes(&ObjectAttributes,&FileName,OBJ_CASE_INSENSITIVE,NULL,NULL);
NtStatus = NtCreateFile(&hFile1,  
							FILE_GENERIC_WRITE,  
							&ObjectAttributes,
							&IOsb,  
							NULL,  
							FILE_ATTRIBUTE_NORMAL,  
							0,
							FILE_SUPERSEDE,  
							FILE_SEQUENTIAL_ONLY,
							NULL,  
							0  
							); 
if (!NT_SUCCESS(NtStatus))  
{  
	printf("NtCreateFile failed (%x) \n", NtStatus);            
}  
else  
	printf("NtCreateFile succeed \n"); 

返回错误c000003b,表示STATUS_OBJECT_PATH_SYNTAX_BAD

调试程序,跟踪到InitializeObjectAttributes,查看结构体ObjectAttributes的参数,如下图

Alt text

查看Buffer在内存中的内容,如下图

Alt text

同NtCreateKey实现时的参数结构相同

对于NtCreateFile,暂时无法应用

0x06 利用思路与检测


Poweliks使用过的注册表隐藏技术,最大的问题是使用regedit.exe打开时会弹框报错,如果将\0插在字符串中间,同时新建一个\0前字符串的同名键值,就能避免这个问题

对此,检测思路就是要找到这种不寻常的注册表键值,查看注册表键值下是否存在两个相同名称的键值

如果利用这种方式在启动项位置新建注册表键值,使用Autoruns是能够检测出来的

0x07 小结


本文对Poweliks使用过的注册表隐藏技术做了进一步测试,分享一种更为隐蔽的利用方法,同时给出了防御检测的思路,对于其他Native API的应用,还需要更多测试


LEAVE A REPLY