在之前几篇文章已经学习了解了几种钩取的方法
● 浅谈调试模式钩取
● 浅谈热补丁
● 浅谈内联钩取原理与实现
● 导入地址表钩取技术
这篇文章就利用钩取方式完成进程隐藏的效果。
在实现进程隐藏时,首先需要明确遍历进程的方法。
CreateToolhelp32Snapshot函数用于创建进程的镜像,当第二个参数为0时则是创建所有进程的镜像,那么就可以达到遍历所有进程的效果。
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
int main()
{
//设置编码,便于后面能够输出中文
setlocale(LC_ALL, "zh_CN.UTF-8");
//创建进程镜像,参数0代表创建所有进程的镜像
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
std::cout << "Create Error" << std::endl;
exit(-1);
}
/*
* typedef struct tagPROCESSENTRY32 {
* DWORD dwSize; 进程信息结构体大小,首次调用之前必须初始化
* DWORD cntUsage; 引用进程的次数,引用次数为0时,则进程结束
* DWORD th32ProcessID; 进程的ID
* ULONG_PTR th32DefaultHeapID; 进程默认堆的标识符,除工具使用对我们没用
* DWORD th32ModuleID; 进程模块的标识符
* DWORD cntThreads; 进程启动的执行线程数
* DWORD th32ParentProcessID; 父进程ID
* LONG pcPriClassBase; 进程线程的基本优先级
* DWORD dwFlags; 保留
* TCHAR szExeFile[MAX_PATH]; 进程的路径
* } PROCESSENTRY32;
* typedef PROCESSENTRY32 *PPROCESSENTRY32;
*/
PROCESSENTRY32 pi;
pi.dwSize = sizeof(PROCESSENTRY32);
//取出第一个进程
BOOL bRet = Process32First(hSnapshot, &pi);
while (bRet)
{
wprintf(L"进程路径:%s\t进程号:%d\n", pi.szExeFile, pi.th32ProcessID);
//取出下一个进程
bRet = Process32Next(hSnapshot, &pi);
}
}
EnumProcesses用于将所有进程号的收集。
#include <iostream>
#include <Windows.h>
#include <Psapi.h>
int main()
{
setlocale(LC_ALL, "zh_CN.UTF-8");
DWORD processes[1024], dwResult, size;
unsigned int i;
//收集所有进程的进程号
if (!EnumProcesses(processes, sizeof(processes), &dwResult))
{
std::cout << "Enum Error" << std::endl;
}
//进程数量
size = dwResult / sizeof(DWORD);
for (i = 0; i < size; i++)
{
//判断进程号是否为0
if (processes[i] != 0)
{
//用于存储进程路径
TCHAR szProcessName[MAX_PATH] = { 0 };
//使用查询权限打开进程
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ,
FALSE,
processes[i]);
if (hProcess != NULL)
{
HMODULE hMod;
DWORD dwNeeded;
//收集该进程的所有模块句柄,第一个句柄则为文件路径
if (EnumProcessModules(hProcess, &hMod, sizeof(hMod),
&dwNeeded))
{
//根据句柄获取文件路径
GetModuleBaseName(hProcess, hMod, szProcessName,
sizeof(szProcessName) / sizeof(TCHAR));
}
wprintf(L"进程路径:%s\t进程号:%d\n", szProcessName, processes[i]);
}
}
}
}
ZwQuerySystemInfomation函数是CreateToolhelp32Snapshot函数与EnumProcesses函数底层调用的函数,也用于遍历进程信息。代码参考https://cloud.tencent.com/developer/article/1454933
#include <iostream>
#include <Windows.h>
#include <ntstatus.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
//定义函数指针
typedef NTSTATUS(WINAPI* NTQUERYSYSTEMINFORMATION)(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
);
int main()
{
//设置编码
setlocale(LC_ALL, "zh_CN.UTF-8");
//获取模块地址
HINSTANCE ntdll_dll = GetModuleHandle(L"ntdll.dll");
if (ntdll_dll == NULL) {
std::cout << "Get Module Error" << std::endl;
exit(-1);
}
NTQUERYSYSTEMINFORMATION ZwQuerySystemInformation = NULL;
//获取函数地址
ZwQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)GetProcAddress(ntdll_dll, "ZwQuerySystemInformation");
if (ZwQuerySystemInformation != NULL)
{
SYSTEM_BASIC_INFORMATION sbi = { 0 };
//查询系统基本信息
NTSTATUS status = ZwQuerySystemInformation(SystemBasicInformation, (PVOID)&sbi, sizeof(sbi), NULL);
if (status == STATUS_SUCCESS)
{
wprintf(L"处理器个数:%d\r\n", sbi.NumberOfProcessors);
}
else
{
wprintf(L"ZwQuerySystemInfomation Error\n");
}
DWORD dwNeedSize = 0;
BYTE* pBuffer = NULL;
wprintf(L"\t----所有进程信息----\t\n");
PSYSTEM_PROCESS_INFORMATION psp = NULL;
//查询进程数量
status = ZwQuerySystemInformation(SystemProcessInformation, NULL, 0, &dwNeedSize);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
pBuffer = new BYTE[dwNeedSize];
//查询进程信息
status = ZwQuerySystemInformation(SystemProcessInformation, (PVOID)pBuffer, dwNeedSize, NULL);
if (status == STATUS_SUCCESS)
{
psp = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
wprintf(L"\tPID\t线程数\t工作集大小\t进程名\n");
do {
//获取进程号
wprintf(L"\t%d", psp->UniqueProcessId);
//获取线程数量
wprintf(L"\t%d", psp->NumberOfThreads);
//获取工作集大小
wprintf(L"\t%d", psp->WorkingSetSize / 1024);
//获取路径
wprintf(L"\t%s\n", psp->ImageName.Buffer);
//移动
psp = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)psp + psp->NextEntryOffset);
} while (psp->NextEntryOffset != 0);
delete[]pBuffer;
pBuffer = NULL;
}
else if (status == STATUS_UNSUCCESSFUL) {
wprintf(L"\n STATUS_UNSUCCESSFUL");
}
else if (status == STATUS_NOT_IMPLEMENTED) {
wprintf(L"\n STATUS_NOT_IMPLEMENTED");
}
else if (status == STATUS_INVALID_INFO_CLASS) {
wprintf(L"\n STATUS_INVALID_INFO_CLASS");
}
else if (status == STATUS_INFO_LENGTH_MISMATCH) {
wprintf(L"\n STATUS_INFO_LENGTH_MISMATCH");
}
}
}
}
通过上述分析可以知道遍历进程的方式有三种,分别是利用CreateToolhelp32Snapshot、EnumProcesses以及ZwQuerySystemInfomation函数
但是CreateToolhelp32Snapshot与EnumProcesses函数底层都是调用了ZwQuerySystemInfomation函数,因此我们只需要钩取该函数即可。
由于测试环境是Win11,因此需要判断在Win11情况下底层是否还是调用了ZwQuerySystemInfomation函数。
可以看到在Win11下还是会调用ZwQuerySystemInfomation函数,在用户态下该函数的名称为NtQuerySystemInformation函数。
这里采用内联钩取的方式对ZwQuerySystemInfomation进行钩取处理,具体怎么钩取在浅谈内联钩取原理与实现已经介绍过了,这里就不详细说明了。这里对自定义的ZwQuerySystemInfomation函数进行说明。
首先第一步需要进行脱钩处理,因为后续需要用到初始的ZwQuerySystemInfomation函数,紧接着获取待钩取函数的地址即可。
...
//脱钩
UnHook("ntdll.dll", "ZwQuerySystemInformation", g_pOrgBytes);
HMODULE hModule = GetModuleHandleA("ntdll.dll");
//获取待钩取函数的地址
PROC pfnOld = GetProcAddress(hModule, "ZwQuerySystemInformation");
//调用原始的ZwQuerySystemInfomation函数
NTSTATUS status = ((NTQUERYSYSTEMINFORMATION)pfnOld)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
...
为了隐藏指定进程,我们需要遍历进程信息,找到目标进程并且删除该进程信息实现隐藏的效果。这里需要知道的是进程信息都存储在SYSTEM_PROCESS_INFORMATION结构体中,该结构体是通过单链表对进程信息进行链接。因此我们通过匹配进程名称找到对应的SYSTEM_PROCESS_INFORMATION结构体,然后进行删除即可,效果如下图。
通过单链表中删除节点的操作,取出目标进程的结构体。代码如下
...
pCur = (PSYSTEM_PROCESS_INFORMATION)(SystemInformation);
while (true)
{
if (!lstrcmpi(pCur->ImageName.Buffer, L"test.exe"))
{
//需要隐藏的进程是最后一个节点
if (pCur->NextEntryOffset == 0)
pPrev->NextEntryOffset = 0;
//不是最后一个节点,则将该节点取出
else
pPrev->NextEntryOffset += pCur->NextEntryOffset;
}
//不是需要隐藏的节点,则继续遍历
else
pPrev = pCur;
//链表遍历完毕
if (pCur->NextEntryOffset == 0)
break;
pCur = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)pCur + pCur->NextEntryOffset);
}
...
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/inlineHook.c
但是采用内联钩取的方法去钩取任务管理器就会出现一个问题,这里将断点取消,利用内联钩取的方式去隐藏进程。
首先利用bl命令查看断点
紧着利用 bc [ID]删除断点
在注入之后任务管理器会在拷贝的时候发生异常
在经过一番调试后发现,由于多线程共同执行导致原本需要可写权限的段被修改为只读权限
在windbg可以用使用!vprot + address查看指定地址的权限,可以看到由于程序往只读权限的地址进行拷贝处理,所以导致了异常。
但是在执行拷贝阶段是先修改了该地址为可写权限,那么导致该原因的情况就是其他线程执行了权限恢复后切换到该线程中进行写,所以导致了这个问题。
因此内联钩取是存在多线程安全的问题,此时可以使用微软自己构建的钩取库Detours,可以在钩取过程中确保线程安全。
【----帮助网安学习,需要网安学习资料关注我,私信回复“资料”免费获取----】
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC漏洞分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)
项目地址:https://github.com/microsoft/Detours
参考:https://www.cnblogs.com/linxmouse/p/14168712.html
使用vcpkg下载
vcpkg.exe install detours:x86-windows
vcpkg.exe install detours:x64-windows
vcpkg.exe integrate install
挂钩
利用Detours挂钩非常简单,只需要根据下列顺序,并且将自定义函数的地址与被挂钩的地址即可完成挂钩处理。
...
//用于确保在 DLL 注入或加载时,恢复被 Detours 修改的进程镜像,保持稳定性
DetourRestoreAfterWith();
//开始一个新的事务来附加或分离
DetourTransactionBegin();
//进行线程上下文的更新
DetourUpdateThread(GetCurrentThread());
//挂钩
DetourAttach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
//提交事务
error = DetourTransactionCommit();
...
脱钩
然后根据顺序完成脱钩即可。
...
//开始一个新的事务来附加或分离
DetourTransactionBegin();
//进行线程上下文的更新
DetourUpdateThread(GetCurrentThread());
//脱钩
DetourDetach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
//提交事务
error = DetourTransactionCommit();
...
从上述可以看到,Detours是通过事务确保了在DLL加载与卸载时后的原子性,但是如何确保多线程安全呢?后续通过调试去发现。
可以利用x ntdl!ZwQuerySystemInformation查看函数地址,可以看到函数的未被挂钩前的情况如下图。
挂钩之后原始的指令被修改为一个跳转指令把前八个字节覆盖掉,剩余的3字节用垃圾指令填充。
该地址里面又是一个jmp指令,并且完成间接寻址的跳转。
该地址是自定义函数ZwQuerySystemInformationEx,因此该间接跳转是跳转到的自定义函数内部。
跳转到TrueZwQuerySystemInformation内部发现ZwQuerySystemInformation函数内部的八字节指令被移动到该函数内部。紧接着又完成一个跳转。
该跳转到ZwQuerySystemInformation函数内部紧接着完成ZwQuerySystemInformation函数的调用。
综上所述,整体流程如下图。实际上Detours实际上使用的是热补丁的思路,但是Detours并不是直接在原始的函数空间中进行补丁,而是开辟了一段临时空间,将指令存储在里面。因此在挂钩后不需要进行脱钩处理就可以调用原始函数。因此就不存在多线程中挂钩与脱钩的冲突。
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/detoursHook.c
浏览器是一种软件,它可以从远程服务器(或本地磁盘)中加载文件并显示文件,它可以允许用户和它交互。
浏览器的核心是浏览器引擎。在不同的浏览器中,根据浏览器引擎的不同,它们显示页面的内容或者顺序会有所区别。
火狐的浏览器引擎称为Gecko,Chrome的浏览器引擎称为Blink,而最新版的IE浏览器内核也已经投入Blink的怀抱。
浏览器处理的是字节而不是我们写的代码,因此,它需要对我们的代码比如html,css,js进行数据的转换才能处理。
HTML是标记语言,它是由一个个闭合的标签构成的,而浏览器需要将他们进行拆分统计,形成一个从根节点到子节点的数据结构,我们把这种结构叫做dom。可以说dom树是浏览器的一个核心功能,正是有了dom树的存在,我们的html代码才有了层次和结构。
css是样式表,它是用来给文档添加样式的。同HTML一样,它也需要进行解析,它会解析成CSSOM树,有了树级结构,样式就可以继承和单独拆分了。
DOM和CSSOM树结构是两个独立的结构。
DOM包含有关页面HTML元素的关系的所有信息,而CSSOM包含有关元素样式的信息。
浏览器将DOM和CSSOM树合并为一个称为渲染树的东西。有了它,我们才能看到丰富多彩的页面。
渲染树包含有关页面上所有可见DOM内容的信息,以及不同节点所需的所有CSSOM信息。
请注意,如果元素被CSS隐藏,该节点将不会在渲染树中表示。
隐藏的元素将出现在DOM中,但不会出现在渲染树中。
原因是渲染树结合了来自DOM和CSSOM的信息,因此它知道在树中不包括隐藏元素。
构建好渲染树后,下一步就是执行“布局”。
有了渲染树之后,我们在屏幕上就拥有了所有可见内容的内容和样式信息,但实际上我们还没有在屏幕上呈现任何内容。
浏览器需要通过从DOM和CSSOM接收到的内容和样式计算页面上每个对象的确切大小和位置。这样才能显示在页面上。这个过程就是布局,也被称为重排。
布局之后,我们需要的就是在浏览器中绘制出我们的页面。
根据渲染树和布局之后的信息,浏览器只要一个个像素点进行绘制,就可以画出整个页面。
一个不错的Web应用程序肯定会使用一些JavaScript。而且JavaScript可以修改页面的内容和样式。
言外之意,JS可以从DOM树中删除和添加元素,也可以修改元素的CSSOM属性。为什么很多时候我们都会把引入的js放到页面底部,目的就是为了防止JS阻塞布局。因为浏览器是顺序读取的,所以越早的构造出渲染树,我们看页面的时间就越短。不过async异步的引入,让我们可以不会因为js的加载而停止dom树的构建,而是在js加载完成之后来使用。
在接收HTML,CSS和JS字节并将它们转换为屏幕上的渲染像素之间采取的步骤的过程称为关键渲染路径。
优化您的网站的性能就是优化关键的渲染路径。
一个经过优化的站点应该进行渐进式渲染,并且不会阻塞整个过程。
这就是网络应用程序被视为慢速或快速的区别。
现在的浏览器都提供开发者了开发者工具,通过它我们可以详细了解到页面的渲染,资源的加载整个过程,我们平时只要多打开它,多分析它,就能让我们的网站以最快的方式呈现在客户面前。
<html>
<head>
<title>DOM 教程</title>
</head>
<body>
<h1>DOM 第一课</h1>
<p class="example">Hello world!</p>
<input name="myInput" type="text" size="20" /><br />
<h1 id="myHeader">This is a header</h1>
</body>
</html>
上面的 HTML 中:
<html> 节点没有父节点;它是根节点
<head> 和 <body> 的父节点是 <html> 节点
文本节点 "Hello world!" 的父节点是 <p> 节点
并且:
<html> 节点拥有两个子节点:<head> 和 <body>
<head> 节点拥有一个子节点:<title> 节点
<title> 节点也拥有一个子节点:文本节点 "DOM 教程"
<h1> 和 <p> 节点是同胞节点, 同时也是 <body> 的子节点
并且:
<head> 元素是 <html> 元素的首个子节点
<body> 元素是 <html> 元素的最后一个子节点
<h1> 元素是 <body> 元素的首个子节点
<p> 元素是 <body> 元素的最后一个子节点
访问节点:
var oLi = document.getElementsByTagName("li");
var oLi = document.getElementById("myHeader");
var oLi = document.getElementsByName("myInput"); //通过name属性访问
querySelector访问方式: IE8开始支持, IE8以下不支持
var div = document.querySelector("#myHeader"); //通过id访问
var div = document.querySelector("li"); //通过标签访问
document.querySelector(".example"); //通过class属性访问
获取表单值
document.getElementById(id).value
querySelector() 方法返回文档中匹配指定 CSS 选择器的一个元素。
注意: querySelector() 方法仅仅返回匹配指定选择器的第一个元素。
如果你需要返回所有的元素, 请使用 querySelectorAll() 方法替代。
利用父子兄关系查找节点:
使用childNodes属性
对象属性
nodeName 返回当前节点名字
元素节点的 nodeName 是标签名称
属性节点的 nodeName 是属性名称
文本节点的 nodeName 永远是 #text
文档节点的 nodeName 永远是 #document
nodeValue 返回当前节点的值, 仅对文本节点和属性节点
对于文本节点, nodeValue 属性包含文本。
对于属性节点, nodeValue 属性包含属性值。
nodeValue 属性对于文档节点和元素节点是不可用的。
注意:nodeValue与tagName的区别对于空白节点的返回值:nodeValue返回null, tagName返回undefined
对于文本节点的返回值:nodeValue返回文本, tagName返回undefined
nodeType 检测节点类型:
alert(document.nodeType);
元素节点的nodeType值为1; 标签名称
属性节点的nodeType值为2; 属性名称 属性节点不能算是其元素节点的子节点
文本节点的nodeType值为3; #text
注释(Comment) 8: #comment
文档(Document) 9 #document <HTML>
文档类型(DocumentType) 10: <!DOCTYPE HTML PUBLIC"...">
节点 nodeType nodeName nodeValue
元素节点 1 大写的标签名 null
属性节点 2 属性名 属性值
文本节点 3 #text 文本值
tagName 返回标签的名称, 仅对元素节点
parentNode 返回当前节点的父节点, 如果存在的话
childNodes 返回当前节点的子节点集合
firstChild 对标记的子节点集合中第一个节点的引用, 如果存在的话
lastChild 对标记的子节点集合中最后一个节点的引用, 如果存在的话
previousSibling 对同属一个父节点的前一个兄弟节点的引用
nextSibling 对同属一个父节点的下一个兄弟节点的引用
Attributes 返回当前节点(标记)属性的列表 用于XML文件
ownerDocument 返回节点所属的根元素
一些 DOM 对象方法
getElementById() 返回带有指定 ID 的元素。
getElementsByTagName() 返回包含带有指定标签名称的所有元素的节点列表(集合/节点数组)。
getElementsByName() 返回包含带有指定类名的所有元素的节点列表。
appendChild() 把新的子节点添加到指定节点。
removeChild() 删除子节点。
replaceChild() 替换子节点。
insertBefore() 在指定的子节点前面插入新的子节点。
createAttribute() 创建属性节点。
createElement() 创建元素节点。
createTextNode() 创建文本节点。
getAttribute() 返回指定的属性值。
setAttribute() 把指定属性设置或修改为指定的值。
删除、替换、插入子节点必须通过父节点的removeChild()方法来完成的
createAttribute() 创建属性节点
var att=document.createAttribute("class");
att.value="democlass";
document.getElementsByTagName("H1")[0].setAttributeNode(att);
以上代码可以简化为
document.getElementsByTagName("H1")[0].class="democlass";
createAttribute()结合setAttributeNode()使用
等同于:
document.getElementsByTagName("H1")[0].setAttributeNode("class", "democlass");
DOM获取所有子节点:
<html>
<head>
<title>childNodes</title>
<script language="javascript">
function myDOMInspector(){
var oUl = document.getElementById("myList"); //获取<ul>标记
var DOMString = "";
if(oUl.hasChildNodes()){ //判断是否有子节点
var oCh = oUl.childNodes;
for(var i=0;i<oCh.length;i++) //逐一查找
DOMString += oCh[i].nodeName + "\n";
}
alert(DOMString);
}
</script>
</head>
<body onload="myDOMInspector()">
<ul id="myList">
<li>糖醋排骨</li>
<li>圆笼粉蒸肉</li>
<li>泡菜鱼</li>
<li>板栗烧鸡</li>
<li>麻婆豆腐</li>
</ul>
</body>
</html>
使用parentNode属性:
<html>
<head>
<title>parentNode</title>
<script language="javascript">
function myDOMInspector(){
var myItem = document.getElementById("myDearFood");
alert(myItem.parentNode.tagName); //返回值为ul
}
</script>
</head>
<body onload="myDOMInspector()">
<ul>
<li>糖醋排骨</li>
<li>圆笼粉蒸肉</li>
<li>泡菜鱼</li>
<li id="myDearFood">板栗烧鸡</li>
<li>麻婆豆腐</li>
</ul>
</body>
</html>
DOM的兄弟关系:
<html>
<head>
<title>Siblings</title>
<script language="javascript">
function myDOMInspector(){
var myItem = document.getElementById("myDearFood");
//访问兄弟节点
var nextListItem = myItem.nextSibling;
var preListItem = myItem.previousSibling;
alert(nextListItem.tagName +" "+ preListItem.tagName);
}
</script>
</head>
<body onload="myDOMInspector()">
<ul>
<li>糖醋排骨</li>
<li>圆笼粉蒸肉</li>
<li>泡菜鱼</li>
<li id="myDearFood">板栗烧鸡</li>
<li>麻婆豆腐</li>
</ul>
</body>
</html>
编写自定义函数解决Firefox等浏览器包含众多的空格作为文本节点问题。
<html>
<head>
<title>Siblings</title>
<script language="javascript">
function nextSib(node){
var tempLast = node.parentNode.lastChild;
//判断是否是最后一个节点,如果是则返回null
if(node == tempLast)
return null;
var tempObj = node.nextSibling;
//逐一搜索后面的兄弟节点,直到发现元素节点为止
while(tempObj.nodeType!=1 && tempObj.nextSibling!=null)
tempObj = tempObj.nextSibling;
//三目运算符,如果是元素节点则返回节点本身,否则返回null
return (tempObj.nodeType==1)?tempObj:null;
}
function prevSib(node){
var tempFirst = node.parentNode.firstChild;
//判断是否是第一个节点,如果是则返回null
if(node == tempFirst)
return null;
var tempObj = node.previousSibling;
//逐一搜索前面的兄弟节点,直到发现元素节点为止
while(tempObj.nodeType!=1 && tempObj.previousSibling!=null)
tempObj = tempObj.previousSibling;
return (tempObj.nodeType==1)?tempObj:null;
}
function myDOMInspector(){
var myItem = document.getElementById("myDearFood");
//获取后一个元素兄弟节点
var nextListItem = nextSib(myItem);
//获取前一个元素兄弟节点
var preListItem = prevSib(myItem);
alert("后一项:" + ((nextListItem!=null)?nextListItem.firstChild.nodeValue:null) + " 前一项:" + ((preListItem!=null)?preListItem.firstChild.nodeValue:null) );
}
</script>
</head>
<body onload="myDOMInspector()">
<ul>
<li>糖醋排骨</li>
<li>圆笼粉蒸肉</li>
<li>泡菜鱼</li>
<li id="myDearFood">板栗烧鸡</li>
<li>麻婆豆腐</li>
</ul>
</body>
</html>
注意:最新版的IE浏览器也包含众多的空格作为文本节点;
设置节点属性:
getAttribute()方法和setAttibute()方法
<html>
<head>
<title>getAttribute()</title>
<script language="javascript">
function myDOMInspector(){
//获取图片
var myImg = document.getElementsByTagName("img")[0];
//获取图片title属性
alert(myImg.getAttribute("title")); //也可以用myImg.title获取属性值
}
</script>
</head>
<body onload="myDOMInspector()">
<img src="01.jpg" title="情人坡" />
</body>
</html>
<html>
<head>
<title>setAttribute()</title>
<script language="javascript">
function changePic(){
//获取图片
var myImg = document.getElementsByTagName("img")[0];
//设置图片src和title属性
myImg.setAttribute("src","02.jpg"); //可以在属性节点不存在时,添加节点的属性值;
myImg.setAttribute("title","紫荆公寓"); //也可以通过myImg.title="紫荆公寓";
}
</script>
</head>
<body>
<img src="01.jpg" title="情人坡" onclick="changePic()" />
</body>
</html>
setAttribute()设置HTML标签的属性
oTable.setAttribute("border", "3"); //为表格边框设置宽度
oTable.setAttribute("border", 3);
oTable.setAttribute("border", "3px"); //经过测试, 此种写法也正确
建议: 具体格式参照HTML属性值的语法格式
setAttibute()设置行内样式
obj.setAttribute("style", "position:absolute;left:200px;top:200px");
注意:具体格式参考CSS样式的语法格式
setAttibute()设置事件属性
obj.setAttribute("onclick", "remove_img()"); //remove_img() 编写自定义函数, 这里不能使用自定义函数
注意:关于文本节点兼容性
元素节点
子节点: childNodes children
首尾子节点: firstChild firstElementChild
lastChild lastElementChild
兄弟节点: nextSibling nextElementSibling
previousSibling previousElementSibling
childNodes firstChild lastChild nextSibling previousSibling属性IE6-IE8版本浏览器不会返回空白节点,
IE9以上版本浏览器会返回文本节点, W3C浏览器(包括火狐浏览器)也会返回文本节点
children firstElementChild lastElementChild nextElementSibling previousElementSibling 只返回元素节点, 不会返回空白节点
注意: DOM操作必须保住DOM节点必须存在, 当然也包括使用css样式display:none隐藏的DOM节点, 否则会导致js语法错误;
*请认真填写需求信息,我们会在24小时内与您取得联系。