构建几乎任何一个Web项目时,调试HTML是不可避免的。调试部分是技术上的,一部分是直觉上的艺术形式,只能通过实践来学习。随着你构建更多的项目,你在不同情况下尝试的调试方法将变得更加明显。想要获得更多调试技巧,建议参加HTML5培训学习,有很多实践课程可以学习调试技巧和方法。
检查语法错误
首先问自己:“我是否漏掉了分号?我忘记关闭HTML元素了吗?“不管你有多丰富的经验,你还是会犯打字错误。通常情况下,你可以简单地切换回文本编辑器,查看你最后编写的内容,并解决问题。
防止跨浏览器问题
HTML和CSS中的大多数显示问题都来自于跨浏览器问题。该网站在一个浏览器中看起来不错,但在另一个浏览器中存在问题。在任何web项目中,这几乎都是一个问题,但是你可以通过首先防止许多跨浏览器问题的发生,来最大限度地减少跨浏览器问题的数量。通过HTML5培训课程,你可以在短时间内学到真正有用的知识和技能,获得快速提升。
首先,始终确保使用了良好的CSS重置,例如normalize。这将“消除”浏览器之间的不一致,并使它们的行为更加一致。即使你只是在做一个快速的单页站点,而没有使用CSS框架,你仍然应该使用重置。
其次,检查以确保你使用的HTML 元素和CSS 属性确实受支持。HTML 和CSS 一直在发展,浏览器供应商尽其所能跟上最新的标准。
使用Web Developer 绘制元素
调试时,在页面上勾勒出元素的轮廓,以了解它们是如何相互关联呈现的,这会很有帮助。你可以自己编写CSS来实现这一点,但更好的方法是使用适用于Chrome、Firefox和Opera的WebDeveloper浏览器扩展。在HTML5培训学习中,专业讲师面授指导教学,还有实操项目锻炼学习,理论+实践,双管齐下,让学习更科学,更有效。
WebDeveloper扩展允许你根据不同的条件(如显示类型或元素类型)概述元素。乍一看,这似乎微不足道,但在一个层叠声明的复杂网络中,很容易弄不清哪个元素最终得到了哪些属性和值,这有助于捕获恶意浮动或混合显示类型。
检查显示类型
网页上的每个元素都有一个显示类型,例如inline、block、inline-block、table、flex、none等等。查看 MDN文档显示页面以获取完整列表。
调试是一种伴随实践而来的复杂的艺术形式,因此编写一份全面的指南几乎是不可能的。但是,如果你想了解更多关于调试的信息,可以考虑参加HTML5培训,有实践课程学习,可以学习更多调试方法。
了解更多
在前面的话
在此之前,我一直都在研究JavaScript相关的反调试技巧。但是当我在网上搜索相关资料时,我发现网上并没有多少关于这方面的文章,而且就算有也是非常不完整的那种。所以在这篇文章中,我打算跟大家总结一下关于JavaScript反调试技巧方面的内容。值得一提的是,其中有些方法已经被网络犯罪分子广泛应用到恶意软件之中了。
对于JavaScript来说,你只需要花一点时间进行调试和分析,你就能够了解到JavaScript代码段的功能逻辑。而我们所要讨论的内容,可以给那些想要分析你JavaScript代码的人增加一定的难度。不过我们的技术跟代码混淆无关,我们主要针对的是如何给代码主动调试增加困难。
本文所要介绍的技术方法大致如下:
1. 检测未知的执行环境(我们的代码只想在浏览器中被执行);
2. 检测调试工具(例如DevTools);
3. 代码完整性控制;
4. 流完整性控制;
5. 反模拟;
简而言之,如果我们检测到了“不正常”的情况,程序的运行流程将会改变,并跳转到伪造的代码块,并“隐藏”真正的功能代码。
一、函数重定义
这是一种最基本也是最常用的代码反调试技术了。在JavaScript中,我们可以对用于收集信息的函数进行重定义。比如说,console.log()函数可以用来收集函数和变量等信息,并将其显示在控制台中。如果我们重新定义了这个函数,我们就可以修改它的行为,并隐藏特定信息或显示伪造的信息。
我们可以直接在DevTools中运行这个函数来了解其功能:
console.log("HelloWorld");
var fake = function() {};
window['console']['log']= fake;
console.log("Youcan't see me!");
运行后我们将会看到:
VM48:1 Hello World
你会发现第二条信息并没有显示,因为我们重新定义了这个函数,即“禁用”了它原本的功能。但是我们也可以让它显示伪造的信息。比如说这样:
console.log("Normalfunction");
//First we save a reference to the original console.log function
var original = window['console']['log'];
//Next we create our fake function
//Basicly we check the argument and if match we call original function with otherparam.
// If there is no match pass the argument to the original function
var fake = function(argument) {
if (argument === "Ka0labs") {
original("Spoofed!");
} else {
original(argument);
}
}
// We redefine now console.log as our fake function
window['console']['log']= fake;
//Then we call console.log with any argument
console.log("Thisis unaltered");
//Now we should see other text in console different to "Ka0labs"
console.log("Ka0labs");
//Aaaand everything still OK
console.log("Byebye!");
如果一切正常的话:
Normal function
VM117:11 This is unaltered
VM117:9 Spoofed!
VM117:11 Bye bye!
实际上,为了控制代码的执行方式,我们还能够以更加聪明的方式来修改函数的功能。比如说,我们可以基于上述代码来构建一个代码段,并重定义eval函数。我们可以把JavaScript代码传递给eval函数,接下来代码将会被计算并执行。如果我们重定义了这个函数,我们就可以运行不同的代码了:
//Just a normal eval
eval("console.log('1337')");
//Now we repat the process...
var original = eval;
var fake = function(argument) {
// If the code to be evaluated contains1337...
if (argument.indexOf("1337") !==-1) {
// ... we just execute a different code
original("for (i = 0; i < 10;i++) { console.log(i);}");
}
else {
original(argument);
}
}
eval= fake;
eval("console.log('Weshould see this...')");
//Now we should see the execution of a for loop instead of what is expected
eval("console.log('Too1337 for you!')");
运行结果如下:
1337
VM146:1We should see this…
VM147:10
VM147:11
VM147:12
VM147:13
VM147:14
VM147:15
VM147:16
VM147:17
VM147:18
VM147:19
正如之前所说的那样,虽然这种方法非常巧妙,但这也是一种非常基础和常见的方法,所以比较容易被检测到。
二、断点
为了帮助我们了解代码的功能,JavaScript调试工具(例如DevTools)都可以通过设置断点的方式阻止脚本代码执行,而断点也是代码调试中最基本的了。
如果你研究过调试器或者x86架构,你可能会比较熟悉0xCC指令。在JavaScript中,我们有一个名叫debugger的类似指令。当我们在代码中声明了debugger函数后,脚本代码将会在debugger指令这里停止运行。比如说:
console.log("Seeme!");
debugger;
console.log("Seeme!");
很多商业产品会在代码中定义一个无限循环的debugger指令,不过某些浏览器会屏蔽这种代码,而有些则不会。这种方法的主要目的就是让那些想要调试你代码的人感到厌烦,因为无限循环意味着代码会不断地弹出窗口来询问你是否要继续运行脚本代码:
setTimeout(function(){while (true) {eval("debugger")
三、时间差异
这是一种从传统反逆向技术那里借鉴过来的基于时间的反调试技巧。当脚本在DevTools等工具环境下执行时,运行速度会非常慢(时间久),所以我们就可以根据运行时间来判断脚本当前是否正在被调试。比如说,我们可以通过测量代码中两个设置点之间的运行时间,然后用这个值作为参考,如果运行时间超过这个值,说明脚本当前在调试器中运行。
演示代码如下:
set Interval(function(){
var startTime = performance.now(), check,diff;
for (check = 0; check < 1000; check++){
console.log(check);
console.clear();
}
diff = performance.now() - startTime;
if (diff > 200){
alert("Debugger detected!");
}
},500);
四、DevTools检测(Chrome)
这项技术利用的是div元素中的id属性,当div元素被发送至控制台(例如console.log(div))时,浏览器会自动尝试获取其中的元素id。如果代码在调用了console.log之后又调用了getter方法,说明控制台当前正在运行。
简单的概念验证代码如下:
let div = document.createElement('div');
let loop = setInterval(() => {
console.log(div);
console.clear();
});
Object.defineProperty(div,"id", {get: () => {
clearInterval(loop);
alert("Dev Tools detected!");
}});
五、隐式流完整性控制
当我们尝试对代码进行反混淆处理时,我们首先会尝试重命名某些函数或变量,但是在JavaScript中我们可以检测函数名是否被修改过,或者说我们可以直接通过堆栈跟踪来获取其原始名称或调用顺序。
arguments.callee.caller可以帮助我们创建一个堆栈跟踪来存储之前执行过的函数,演示代码如下:
function getCallStack() {
var stack = "#", total = 0, fn =arguments.callee;
while ( (fn = fn.caller) ) {
stack = stack + "" +fn.name;
total++
}
return stack
}
function test1() {
console.log(getCallStack());
}
function test2() {
test1();
}
function test3() {
test2();
}
function test4() {
test3();
}
test4();
注意:源代码的混淆程度越强,这个技术的效果就越好。
六、代理对象
代理对象是目前JavaScript中最有用的一个工具,这种对象可以帮助我们了解代码中的其他对象,包括修改其行为以及触发特定环境下的对象活动。比如说,我们可以创建一个嗲哩对象并跟踪每一次document.createElemen调用,然后记录下相关信息:
const handler = { // Our hook to keep the track
apply: function (target, thisArg, args){
console.log("Intercepted a call tocreateElement with args: " + args);
return target.apply(thisArg, args)
}
}
document.createElement= new Proxy(document.createElement, handler) // Create our proxy object withour hook ready to intercept
document.createElement('div');
接下来,我们可以在控制台中记录下相关参数和信息:
VM64:3 Intercepted a call to createElement with args: div
我们可以利用这些信息并通过拦截某些特定函数来调试代码,但是本文的主要目的是为了介绍反调试技术,那么我们如何检测“对方”是否使用了代理对象呢?其实这就是一场“猫抓老鼠”的游戏,比如说,我们可以使用相同的代码段,然后尝试调用toString方法并捕获异常:
//Call a "virgin" createElement:
try {
document.createElement.toString();
}catch(e){
console.log("I saw your proxy!");
}
信息如下:
"function createElement() { [native code] }"
S是解释型语言,是逐条语句解释执行的,如果错误发生在某个语句块,此语句块以前的语句一般都可以正常执行。这不同于C等编译型语言。
代码调试的重点在于找到错误发生点,然后才能有的放矢。
通常可以使用警告框来提示变量信息。
alert(document.body.innerHTML);
当警告框弹出时,用户将需要单击“确定”来继续。
<div id="demo">
<script>
var arr=[1,2,3,4,5]
document.write(arr[2] + ' ') //3
</script>
</div>
需要注意的是是,以下写法会替换整个页面的内容:
<button type="button" onclick="document.write(5 + 6)">试一试</button>
即使是函数调用也是如此。
document.getElementById("demo").innerHTML =""
<h1>JavaScript Array.filter()</h1>
<p>使用通过测试的所有数组元素创建一个新数组。</p>
<p id="demo"></p> //45,25
<script>
var numbers = [45, 4, 9, 16, 25];
var over18 = numbers.filter(myFunction);
document.getElementById("demo").innerHTML = over18;
function myFunction(value, index, array) {
return value > 18;
}
</script>
JS的运行环境是浏览器,由浏览器引擎解释执行JS代码,一般来说,浏览器也提供调试器,如chrome按F12即可调出高试器:
<!DOCTYPE html>
<html>
<body>
<h4>我的第一张网页</h4>
<p>使用F12在浏览器(Chrome、IE、Firefox)中激活调试,然后在调试器菜单中选择“控制台”。</p>
<script>
a = 5;
b = 6;
c = a + b;
console.log(c);
</script>
</body>
</html>
如果您的浏览器支持调试,那么您可以使用 console.log() 在调试窗口中显示 JavaScript 的值:
内置的调试器可打开或关闭,强制将错误报告给用户。
通过调试器,您也可以设置断点(代码执行被中断的位置),并在代码执行时检查变量。
<p id="demo"></p>
<script>
try {
adddlert("欢迎您,亲爱的用户!");
}
catch(err) {
demo.innerHTML = err.message; //adddlert is not defined
}
</script>
JavaScript 实际上会创建带有两个属性的 Error 对象:name 和 message。
name 设置或返回错误名。
message 设置或返回错误消息(一条字符串)。
debugger停止执行 JavaScript,并调用调试函数(如果可用)。
可以注释掉一些可疑代码来确定错误发生点。
或者考虑逐步增加代码的方法,逐步验证,以避免错误。
8.1 意外使用赋值运算符
如果程序员在 if 语句中意外使用赋值运算符(=)而不是比较运算符(===),JavaScript 程序可能会产生一些无法预料的结果。
8.2 令人困惑的加法和级联
加法用于加数值。
级联(Concatenation)用于加字符串。
在 JavaScript 中,这两种运算均使用相同的 + 运算符。
正因如此,将数字作为数值相加,与将数字作为字符串相加,将产生不同的结果:
var x = 10 + 5; // x 中的结果是 15
var x = 10 + "5"; // x 中的结果是 "105"
而加法以外的其它算法运算符可以将字符串进行自动类型转换。
10-"5" // 5
8.3 令人误解的浮点数
JavaScript 中的数字均保存为 64 位的浮点数(Floats),符合IEEE754的标准。
所有编程语言,包括 JavaScript,都存在处理浮点值的困难:
var x = 0.1;
var y = 0.2;
var z = x + y // z=0.30000000000000004
8.4 错位的分号
因为一个错误的分号,此代码块无论 x 的值如何都会执行:
if (x == 19);
{
// code block
}
在一行的结尾自动关闭语句是默认的 JavaScript 行为。
在 JavaScript 中,用分号来关闭(结束)语句是可选的。
8.5 对象使用命名索引
在 JavaScript 中,数组使用数字索引。
在 JavaScript 中,对象使用命名索引。
如果您使用命名索引,那么在访问数组时,JavaScript 会将数组重新定义为标准对象。
<p id="demo"></p>
<script>
var person = [];
person["firstName"] = "Bill";
person["lastName"] = "Gates";
person["age"] = 46;
var x = person.length; // person.length 将返回 0
var y = person[0]; // person[0] 将返回 undefined
y=person["age"]; //ok
y=person.age;//ok
document.getElementById("demo").innerHTML = y
</script>
8.6 Undefined 不是 Null
Undefined 的类型是 Undefined,Null的类型是Object。
JavaScript 对象、变量、属性和方法可以是未定义的。
此外,空的 JavaScript 对象的值可以为 null。
在测试非 null 之前,必须先测试未定义:
if (typeof myObj !== "undefined" && myObj !== null)
8.7 JS没有块作用域(与C语言不同)
在 ES2015 之前,JavaScript 只有两种类型的作用域:全局作用域和函数作用域。
<!DOCTYPE html>
<html>
<body>
<h2>JavaScript</h2>
<p>JavaScript不会为每个代码块创建新的作用域。</p>
<p>此代码将显示 i(10)的值,即使在 for 循环块之外:</p>
<p id="demo"></p>
<script>
for (var i = 0; i < 10; i++) {
// some code
}
document.getElementById("demo").innerHTML = i; //10
</script>
</body>
</html>
ES2015 引入了两个重要的 JavaScript 新关键词:let 和 const。
这两个关键字在 JavaScript 中提供了块作用域(Block Scope)变量(和常量)。
for (let i = 0; i < 10; i++) {
// some code
}
document.getElementById("demo").innerHTML = i; //不能访问
-End-
*请认真填写需求信息,我们会在24小时内与您取得联系。