渗透技巧——"隐藏"注册表的更多测试
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
运行如下图
使用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
函数需要作适当修改,重新计算字符串长度,新的函数命名为MySetHiddenValueKey2
和MyQueryHiddenValueKeyString2
程序实现以下功能:
- 创建注册表键值test2\0abc,内容为hidden0123456789abcdef
- 读取注册表键值test2\0abc的内容
运行如下图
使用regedit.exe查询该键值,弹框提示无法获取,如下图
这里可以做一个大胆的尝试:
既然test2\0abc中的”\0”会截断字符串,那么我们再创建一个名为test2的键值会怎么样呢?
创建注册表键值test2,内容为0123456789abcdef,关键代码如下:
hKey = MyOpenKey("\\Registry\\Machine\\Software\\test2");
MySetValueKey(hKey,"test2","0123456789abcdef",REG_SZ);
再次使用regedit.exe查看注册表,有趣的事情发生了,如下图
查询注册表键值\Registry\Machine\Software\test2不再弹框报错,而是显示两个名为test2的键值,内容均为0123456789abcdef
我们知道,注册表不允许创建两个名称相同的注册表键值,而上述测试产生的两个同名键值,实际上是因为其中的一个被错误的截断,导致显示键值名称相同,键值内容也相同,为0123456789abcdef(实际上内容为hidden0123456789abcdef)
这样我们就又多了一种”隐藏”注册表的方法,相比于之前的在首位填”\0”,这个隐藏方法最大的优点是使用regedit.exe查看该键值时不会弹框报错,隐蔽效果更好,同时又具有欺骗性,同正常键值内容相同
对比如下图
显示键值内容为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的参数,如下图
查看Buffer在内存中的内容,如下图
同NtCreateKey实现时的参数结构相同
对于NtCreateFile,暂时无法应用
0x06 利用思路与检测
Poweliks使用过的注册表隐藏技术,最大的问题是使用regedit.exe打开时会弹框报错,如果将\0插在字符串中间,同时新建一个\0前字符串的同名键值,就能避免这个问题
对此,检测思路就是要找到这种不寻常的注册表键值,查看注册表键值下是否存在两个相同名称的键值
如果利用这种方式在启动项位置新建注册表键值,使用Autoruns是能够检测出来的
0x07 小结
本文对Poweliks使用过的注册表隐藏技术做了进一步测试,分享一种更为隐蔽的利用方法,同时给出了防御检测的思路,对于其他Native API的应用,还需要更多测试