ghidra/GhidraDocs/GhidraClass/Intermediate/VersionTracking.html

954 lines
26 KiB
HTML

<!DOCTYPE html>
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=Edge">
<title>Version Tracking</title>
<!-- Your Slides -->
<!-- One section is one slide -->
<!-- This is the first slide -->
<section>
<h1>Ghidra Version Tracking</h1>
</section>
<section>
<header>What is Version Tracking?</header>
<br><br>
<ul>
<li>Matching functions and data and their markup from an older version of a binary to a new version of a binary</li>
</ul>
</section>
<section>
<header>Version Tracking</header>
<br>
<ul class="medium">
<li>Allows for multiple version tracking algorithms</li>
<li>Has both function and data matching capabilities</li>
<li>Saves state of version tracking on each matched item so you can come back to it later, change states, etc...</li>
<li>Matches shown in sortable table with numerous ways to sort and filter so that items in list can be tailored however you like (example: remove when accepted or ignored, show only ones from particular algorithm, etc...</li>
<li>Shows all possible matches to a particular function (from both programs)</li>
</ul>
</section>
<section>
<header>Version Tracking (continued)</header>
<ul>
<li>Allows moving of markup from source version to destination version</li>
<li>Can rerun using current state and add new information to your current version tracking session</li>
<li>Drag and drop markup items if necessary</li>
<li>Has validators to help determine if programs are sufficiently analyzed to get good version tracking results</li>
<li>Provides list of what functions remaining to match</li>
</ul>
</section>
<section>
<header>Preparing for Version Tracking</header>
<br>
<ul>
<li>Both binaries should have similar memory maps</li>
<li>Both binaries should be mostly disassembled correctly</li>
<li>Both binaries should have mostly all functions created correctly and with similar calling conventions</li>
</ul>
<div role="note">
<p><u><b>Notes:</b></u></p>
<p>If the following conditions are not met, the results of Version Tracking will be incomplete or incorrect:</p>
<ul>
<li>Both binaries should have similar memory maps</li>
<ul>
<li>AVOID: One program with a lot of memory segments and one program with one merged segment</li>
<li>AVOID: duplicate code segments</li>
<li>AVOID: same segments with different permissions</li>
</ul>
<li>Both binaries should be mostly disassembled correctly</li>
<ul>
<li>AVOID: areas where data has been incorrectly turned into code</li>
<li>AVOID: areas with &quot;red&quot; X&apos;s</li>
<li>AVOID: off-cut references</li>
</ul>
<li>Both binaries should have mostly all functions created correctly</li>
<ul>
<li>AVOID: missing functions</li>
<li>AVOID: functions that have been disassembled but are not defined as functions</li>
<li>AVOID: functions with no returns that have not been marked as such</li>
<li>AVOID: functions that do not decompile correctly</li>
<li>AVOID: functions with very different calling conventions</li>
</ul>
</ul>
</div>
</section>
<section>
<header>Version Tracking</header>
<br>
<ul class="medium">
<li>The Version Tracking tool is made up of two Code Browsers and a Window that manages runs/result tables all connected via a Version Tracking session.</li>
<li>A Version Tracking session stores and manages your version tracking programs and match information.</li>
<li>Source program is the program with interesting markup.</li>
<li>Destination program is the new program that you want to pull match information into.</li>
<li>Validators check to see if there are any potential issues with the programs that would keep you from getting good results.</li>
</ul>
<div role="note">
<p>To open the Version Tracking tool do one of the following:</p>
<ul>
<li>Click on the blue footprints icon in the Ghidra Program Manager. This will bring up an empty Version Tracking tool. To get started, click on the blue footprints in the empty tool. You will be asked for a session name and to choose the two programs you want to version track.</li>
<li>Drag one program onto the blue footprint. This will prompt you for a session name and the other program to version track. Make sure you use the swap button to get the source and destination programs correct.</li>
<li>Drag two programs onto the blue footprint. This will prompt you for a session name. Make sure you use the swap button to get the source and destination programs correct.</li>
</ul>
<p>The <b>source</b> program is the program with interesting markup.</p>
<p>The <b>destination</b> program is the new program that you want to pull match information into.</p>
<p><b>Validators</b> check to see if there are any potential issues with the programs that would keep you from getting good results. If you get any warnings or red X's you should inspect and potentially fix any problems in your programs before continuing. <b>Any results that are obtained using programs that are not sufficiently cleaned up are not guaranteed to be complete or correct!</b></p>
</div>
</section>
<section>
<header>Validators</header>
<br>
<ul class="medium">
<li>Help user determine if programs are ready to version track by showing for each program:</li>
<ul>
<li>Number of errors</li>
<li>Percent analyzed</li>
<li>Memory Map similarity</li>
<li>Number of functions</li>
<li>Numer of non-returning functions</li>
<li>Number of off-cut references</li>
</ul>
<li>To get optimal results, users should fix up issues before proceeding to the next step</li>
<li>Once finished, two Codebrowsers, one containing source program and one containing destination program, and a session manager window will open.
</ul>
</section>
<section>
<header>Correlation Algorithms</header>
<br>
<ul class="medium">
<li>Click on green plus icon in the version tracking session manager to run one or more version correlator at a time</li>
<li>Correlators include:</li>
<ul>
<li>Exact and Similar Data Match Correlators</li>
<li>Exact and Similar Symbol Name Correlators</li>
<li>Various types of Exact Function Correlators</li>
<li>Various Reference Correlators that use results from known matches</li>
</ul>
<li>Read help for more detailed information about each</li>
</ul>
</section>
<section>
<header>Match Table</header>
<br>
<ul class="medium">
<li>Click on match and both browsers navigate to match items</li>
<li>Buttons (accept, apply, reject, clear, tag, options)</li>
<li>Filters (name, score, length, advanced)</li>
</ul>
<li>Can apply one or more match at once (select rows in table or do Ctrl-A for whole table selection)</li>
<li>Special filter button in lower right allows filtering by correlator, match type, and accepted/available matches</li>
</ul>
</section>
<section>
<header>Markup Table</header>
<br>
<ul class="medium">
<li>Shows markup matches and their status</li>
<li>Allows individual markup if necessary</li>
<li>Highlights current markup in dummy listing view</li>
</ul>
</section>
<section>
<header>Decompile/Listing Views</header>
<br>
<ul class="medium">
<li>Shows view of current selection in match table</li>
<li>Can view as decompiler or listing</li>
<li>In listing can drag and drop markup to alternate location</li>
<li>Shows similarity and differences between two match items</li>
</ul>
</section>
<section>
<header>Functions List</header>
<ul class="small">
<li>Open with Window->Version Tracking functions</li>
<li>Shows list of all functions in each program by default</li>
<li>Can change to show only accepted or available matches</li>
<li>Can manually match items.</li>
</ul>
</section>
<section>
<header>Exercise 1</header>
<br>
<ul class="medium">
<li>Import and analyze WallaceSrc.exe and WallaceVersion2.exe from ../ExerciseFiles/VersionTrackFiles. </li>
<li>Open WallaceSrc.exe in a CodeBrowser and run the MarkupWallaceSrcScript.</li>
<li>Use the Version Tracking tool to determine what function(s) are different, new, or removed in WallaceVersion2.exe.</li>
<li>Apply appropriate markup items from WallaceSrc.exe to WallaceVersion2.exe.</li>
</ul>
</section>
<!-- COPY THE TEXT BELOW TO START A NEW SLIDE
<section>
<header>Insert Title of Slide Here</header>
<ul class="small" comment="NOTE: remove the class attribute for regular size, adjust the name if you want big, small, or tiny">
<li>Bullet text here</li>
<ul>
<li>Nested bullet here</li>
</ul>
</ul>
<div role="note">
<p>Insert notes here</p>
<p>And here, too</p>
</div>
</section>
END COPY -->
<!-- Your Style -->
<!-- Define the style of your presentation -->
<!-- Maybe a font from http://www.google.com/webfonts ? -->
<!--link href='http://fonts.googleapis.com/css?family=Oswald' rel='stylesheet'-->
<style>
html, .view body { background-color: black; counter-reset: slideidx; }
body, .view section { background-color: black; border-radius: 12px; color: white; }
/* A section is a slide. It's size is 800x600, and this will never change */
section, .view head > title {
font-family: arial, serif;
font-size: 35px;
}
.view section:after {
counter-increment: slideidx;
content: counter(slideidx, decimal-leading-zero);
position: absolute; bottom: -80px; right: 100px;
color: black;
}
.view head > title {
color: black;
text-align: center;
margin: 1em 0 1em 0;
}
h1, h2 {
margin-top: 200px;
text-align: center;
font-size: 80px;
font-family: 'Times New Roman'
}
h3 {
margin: 100px 0 50px 100px;
}
/* My custom list sizes */
.big ul {
font-size: 45px;
}
.big ol {
font-size: 45px;
}
.big li {
font-size: 45px;
}
.big li:before {
font-size: 200px;
}
.medium ul {
margin: 0px 0px;
font-size: 30px;
}
.medium ol {
margin: 0px 0px;
font-size: 30px;
}
.medium li {
margin: 0px 0px;
font-size: 30px;
}
.medium li:before {
font-size: 120px;
}
.small ul {
margin: 0px 0px;
font-size: 25px;
}
.small ol {
margin: 0px 0px;
font-size: 25px;
}
.small li {
margin: 0px 0px;
font-size: 25px;
}
.small li:before {
font-size: 80px;
}
.tiny ul {
margin: 0px 0px;
font-size: 20px;
}
.tiny ol {
margin: 0px 0px;
font-size: 20px;
}
.tiny li {
margin: 0px 0px;
font-size: 20px;
}
.tiny li:before {
font-size: 70px;
}
/* end custom list sizes */
/* Standard list size */
ul {
margin: 10px 50px;
font-size: 35px;
list-style-type: none;
margin-left: 0;
padding-left: 1em;
text-indent: -1em;
}
ol {
margin: 10px 50px;
font-size: 35px;
list-style-type: none;
margin-left: 0;
padding-left: 1em;
text-indent: -1em;
}
ol.decimal {
list-style-position: inside;
list-style-type: decimal;
}
li {
margin: 10px 10px;
font-size: 35px;
}
ul > li:before {
content:"·";
font-size:160px;
vertical-align:middle;
line-height: 20px;
color: red;
}
/* end custom list sizes */
p {
margin: 75px;
font-size: 100px;
}
blockquote {
height: 100%;
background-color: black;
color: white;
font-size: 60px;
padding: 50px;
}
blockquote:before {
content: open-quote;
}
blockquote:after {
content: close-quote;
}
/* Figures are displayed full-page, with the caption
on top of the image/video */
figure {
background-color: black;
width: 100%;
height: 100%;
}
figure > * {
position: absolute;
}
figure > img, figure > video {
width: 100%; height: 100%;
}
figcaption {
margin: 70px;
font-size: 50px;
}
footer {
position: absolute;
bottom: 0;
width: 100%;
padding: 40px;
text-align: right;
background-color: black;
border-top: 1px solid #CCC;
}
header {
font-family: 'Times New Roman';
position: relative;
top: 0px;
width: 100%;
padding: 0px;
text-align: center;
background-image: url(Images/GhidraLogo64.png), url(Images/GhidraLogo64.png);
background-repeat: no-repeat, no-repeat;
background-position: left top, right top;
background-size: contain, contain;
border-bottom: 1px solid red;
font-size: 50px;
}
/* Transition effect */
/* Feel free to change the transition effect for original
animations. See here:
https://developer.mozilla.org/en/CSS/CSS_transitions
How to use CSS3 Transitions: */
section {
-moz-transition: left 400ms linear 0s;
-webkit-transition: left 400ms linear 0s;
-ms-transition: left 400ms linear 0s;
transition: left 400ms linear 0s;
}
.view section {
-moz-transition: none;
-webkit-transition: none;
-ms-transition: none;
transition: none;
}
.view section[aria-selected] {
border: 5px red solid;
}
/* Before */
section { left: -150%; }
/* Now */
section[aria-selected] { left: 0; }
/* After */
section[aria-selected] ~ section { left: +150%; }
/* Incremental elements */
/* By default, visible */
.incremental > * { opacity: 1; }
/* The current item */
.incremental > *[aria-selected] { opacity: 1; }
/* The items to-be-selected */
.incremental > *[aria-selected] ~ * { opacity: 0; }
/* The progressbar, at the bottom of the slides, show the global
progress of the presentation. */
#progress-bar {
height: 2px;
background: #AAA;
}
</style>
<!-- {{{{ dzslides core
#
#
# __ __ __ . __ ___ __
# | \ / /__` | | | \ |__ /__`
# |__/ /_ .__/ |___ | |__/ |___ .__/ core :€
#
#
# The following block of code is not supposed to be edited.
# But if you want to change the behavior of these slides,
# feel free to hack it!
#
-->
<div id="progress-bar"></div>
<!-- Default Style -->
<style>
* { margin: 0; padding: 0; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }
[role="note"] { display: none; }
body {
width: 800px; height: 600px;
margin-left: -400px; margin-top: -300px;
position: absolute; top: 50%; left: 50%;
overflow: hidden;
display: none;
}
.view body {
position: static;
margin: 0; padding: 0;
width: 100%; height: 100%;
display: inline-block;
overflow: visible; overflow-x: hidden;
/* undo Dz.onresize */
transform: none !important;
-moz-transform: none !important;
-webkit-transform: none !important;
-o-transform: none !important;
-ms-transform: none !important;
}
.view head, .view head > title { display: block }
section {
position: absolute;
pointer-events: none;
width: 100%; height: 100%;
}
.view section {
pointer-events: auto;
position: static;
width: 800px; height: 600px;
margin: -150px -200px;
float: left;
transform: scale(.4);
-moz-transform: scale(.4);
-webkit-transform: scale(.4);
-o-transform: scale(.4);
-ms-transform: scale(.4);
}
.view section > * { pointer-events: none; }
section[aria-selected] { pointer-events: auto; }
html { overflow: hidden; }
html.view { overflow: visible; }
body.loaded { display: block; }
.incremental {visibility: hidden; }
.incremental[active] {visibility: visible; }
#progress-bar{
bottom: 0;
position: absolute;
-moz-transition: width 400ms linear 0s;
-webkit-transition: width 400ms linear 0s;
-ms-transition: width 400ms linear 0s;
transition: width 400ms linear 0s;
}
.view #progress-bar {
display: none;
}
</style>
<script>
var Dz = {
remoteWindows: [],
idx: -1,
step: 0,
html: null,
slides: null,
progressBar : null,
params: {
autoplay: "1"
}
};
Dz.init = function() {
document.body.className = "loaded";
this.slides = Array.prototype.slice.call($$("body > section"));
this.progressBar = $("#progress-bar");
this.html = document.body.parentNode;
this.setupParams();
this.onhashchange();
this.setupTouchEvents();
this.onresize();
this.setupView();
}
Dz.setupParams = function() {
var p = window.location.search.substr(1).split('&');
p.forEach(function(e, i, a) {
var keyVal = e.split('=');
Dz.params[keyVal[0]] = decodeURIComponent(keyVal[1]);
});
// Specific params handling
if (!+this.params.autoplay)
$$.forEach($$("video"), function(v){ v.controls = true });
}
Dz.onkeydown = function(aEvent) {
// Don't intercept keyboard shortcuts
if (aEvent.altKey
|| aEvent.ctrlKey
|| aEvent.metaKey
|| aEvent.shiftKey) {
return;
}
if ( aEvent.keyCode == 37 // left arrow
|| aEvent.keyCode == 38 // up arrow
|| aEvent.keyCode == 33 // page up
) {
aEvent.preventDefault();
this.back();
}
if ( aEvent.keyCode == 39 // right arrow
|| aEvent.keyCode == 40 // down arrow
|| aEvent.keyCode == 34 // page down
) {
aEvent.preventDefault();
this.forward();
}
if (aEvent.keyCode == 35) { // end
aEvent.preventDefault();
this.goEnd();
}
if (aEvent.keyCode == 36) { // home
aEvent.preventDefault();
this.goStart();
}
if (aEvent.keyCode == 32) { // space
aEvent.preventDefault();
this.toggleContent();
}
if (aEvent.keyCode == 70) { // f
aEvent.preventDefault();
this.goFullscreen();
}
if (aEvent.keyCode == 79) { // o
aEvent.preventDefault();
this.toggleView();
}
}
/* Touch Events */
Dz.setupTouchEvents = function() {
var orgX, newX;
var tracking = false;
var db = document.body;
db.addEventListener("touchstart", start.bind(this), false);
db.addEventListener("touchmove", move.bind(this), false);
function start(aEvent) {
aEvent.preventDefault();
tracking = true;
orgX = aEvent.changedTouches[0].pageX;
}
function move(aEvent) {
if (!tracking) return;
newX = aEvent.changedTouches[0].pageX;
if (orgX - newX > 100) {
tracking = false;
this.forward();
} else {
if (orgX - newX < -100) {
tracking = false;
this.back();
}
}
}
}
Dz.setupView = function() {
document.body.addEventListener("click", function ( e ) {
if (!Dz.html.classList.contains("view")) return;
if (!e.target || e.target.nodeName != "SECTION") return;
Dz.html.classList.remove("view");
Dz.setCursor(Dz.slides.indexOf(e.target) + 1);
}, false);
}
/* Adapt the size of the slides to the window */
Dz.onresize = function() {
var db = document.body;
var sx = db.clientWidth / window.innerWidth;
var sy = db.clientHeight / window.innerHeight;
var transform = "scale(" + (1/Math.max(sx, sy)) + ")";
db.style.MozTransform = transform;
db.style.WebkitTransform = transform;
db.style.OTransform = transform;
db.style.msTransform = transform;
db.style.transform = transform;
}
Dz.getNotes = function(aIdx) {
var s = $("section:nth-of-type(" + aIdx + ")");
var d = s.$("[role='note']");
return d ? d.innerHTML : "";
}
Dz.onmessage = function(aEvent) {
var argv = aEvent.data.split(" "), argc = argv.length;
argv.forEach(function(e, i, a) { a[i] = decodeURIComponent(e) });
var win = aEvent.source;
if (argv[0] === "REGISTER" && argc === 1) {
this.remoteWindows.push(win);
this.postMsg(win, "REGISTERED", document.title, this.slides.length);
this.postMsg(win, "CURSOR", this.idx + "." + this.step);
return;
}
if (argv[0] === "BACK" && argc === 1)
this.back();
if (argv[0] === "FORWARD" && argc === 1)
this.forward();
if (argv[0] === "START" && argc === 1)
this.goStart();
if (argv[0] === "END" && argc === 1)
this.goEnd();
if (argv[0] === "TOGGLE_CONTENT" && argc === 1)
this.toggleContent();
if (argv[0] === "SET_CURSOR" && argc === 2)
window.location.hash = "#" + argv[1];
if (argv[0] === "GET_CURSOR" && argc === 1)
this.postMsg(win, "CURSOR", this.idx + "." + this.step);
if (argv[0] === "GET_NOTES" && argc === 1)
this.postMsg(win, "NOTES", this.getNotes(this.idx));
}
Dz.toggleContent = function() {
// If a Video is present in this new slide, play it.
// If a Video is present in the previous slide, stop it.
var s = $("section[aria-selected]");
if (s) {
var video = s.$("video");
if (video) {
if (video.ended || video.paused) {
video.play();
} else {
video.pause();
}
}
}
}
Dz.setCursor = function(aIdx, aStep) {
// If the user change the slide number in the URL bar, jump
// to this slide.
aStep = (aStep != 0 && typeof aStep !== "undefined") ? "." + aStep : ".0";
window.location.hash = "#" + aIdx + aStep;
}
Dz.onhashchange = function() {
var cursor = window.location.hash.split("#"),
newidx = 1,
newstep = 0;
if (cursor.length == 2) {
newidx = ~~cursor[1].split(".")[0];
newstep = ~~cursor[1].split(".")[1];
if (newstep > Dz.slides[newidx - 1].$$('.incremental > *').length) {
newstep = 0;
newidx++;
}
}
this.setProgress(newidx, newstep);
if (newidx != this.idx) {
this.setSlide(newidx);
}
if (newstep != this.step) {
this.setIncremental(newstep);
}
for (var i = 0; i < this.remoteWindows.length; i++) {
this.postMsg(this.remoteWindows[i], "CURSOR", this.idx + "." + this.step);
}
}
Dz.back = function() {
if (this.idx == 1 && this.step == 0) {
return;
}
if (this.step == 0) {
this.setCursor(this.idx - 1,
this.slides[this.idx - 2].$$('.incremental > *').length);
} else {
this.setCursor(this.idx, this.step - 1);
}
}
Dz.forward = function() {
if (this.idx >= this.slides.length &&
this.step >= this.slides[this.idx - 1].$$('.incremental > *').length) {
return;
}
if (this.step >= this.slides[this.idx - 1].$$('.incremental > *').length) {
this.setCursor(this.idx + 1, 0);
} else {
this.setCursor(this.idx, this.step + 1);
}
}
Dz.goStart = function() {
this.setCursor(1, 0);
}
Dz.goEnd = function() {
var lastIdx = this.slides.length;
var lastStep = this.slides[lastIdx - 1].$$('.incremental > *').length;
this.setCursor(lastIdx, lastStep);
}
Dz.toggleView = function() {
this.html.classList.toggle("view");
if (this.html.classList.contains("view")) {
$("section[aria-selected]").scrollIntoView(true);
}
}
Dz.setSlide = function(aIdx) {
this.idx = aIdx;
var old = $("section[aria-selected]");
var next = $("section:nth-of-type("+ this.idx +")");
if (old) {
old.removeAttribute("aria-selected");
var video = old.$("video");
if (video) {
video.pause();
}
}
if (next) {
next.setAttribute("aria-selected", "true");
if (this.html.classList.contains("view")) {
next.scrollIntoView();
}
var video = next.$("video");
if (video && !!+this.params.autoplay) {
video.play();
}
} else {
// That should not happen
this.idx = -1;
// console.warn("Slide doesn't exist.");
}
}
Dz.setIncremental = function(aStep) {
this.step = aStep;
var old = this.slides[this.idx - 1].$('.incremental > *[aria-selected]');
if (old) {
old.removeAttribute('aria-selected');
}
var incrementals = $$('.incremental');
if (this.step <= 0) {
$$.forEach(incrementals, function(aNode) {
aNode.removeAttribute('active');
});
return;
}
var next = this.slides[this.idx - 1].$$('.incremental > *')[this.step - 1];
if (next) {
next.setAttribute('aria-selected', true);
next.parentNode.setAttribute('active', true);
var found = false;
$$.forEach(incrementals, function(aNode) {
if (aNode != next.parentNode)
if (found)
aNode.removeAttribute('active');
else
aNode.setAttribute('active', true);
else
found = true;
});
} else {
setCursor(this.idx, 0);
}
return next;
}
Dz.goFullscreen = function() {
var html = $('html'),
requestFullscreen = html.requestFullscreen || html.requestFullScreen || html.mozRequestFullScreen || html.webkitRequestFullScreen;
if (requestFullscreen) {
requestFullscreen.apply(html);
}
}
Dz.setProgress = function(aIdx, aStep) {
var slide = $("section:nth-of-type("+ aIdx +")");
if (!slide)
return;
var steps = slide.$$('.incremental > *').length + 1,
slideSize = 100 / (this.slides.length - 1),
stepSize = slideSize / steps;
this.progressBar.style.width = ((aIdx - 1) * slideSize + aStep * stepSize) + '%';
}
Dz.postMsg = function(aWin, aMsg) { // [arg0, [arg1...]]
aMsg = [aMsg];
for (var i = 2; i < arguments.length; i++)
aMsg.push(encodeURIComponent(arguments[i]));
aWin.postMessage(aMsg.join(" "), "*");
}
function init() {
Dz.init();
window.onkeydown = Dz.onkeydown.bind(Dz);
window.onresize = Dz.onresize.bind(Dz);
window.onhashchange = Dz.onhashchange.bind(Dz);
window.onmessage = Dz.onmessage.bind(Dz);
}
window.onload = init;
</script>
<script> // Helpers
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
// closest thing possible to the ECMAScript 5 internal IsCallable
// function
if (typeof this !== "function")
throw new TypeError(
"Function.prototype.bind - what is trying to be fBound is not callable"
);
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply( this instanceof fNOP ? this : oThis || window,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
var $ = (HTMLElement.prototype.$ = function(aQuery) {
return this.querySelector(aQuery);
}).bind(document);
var $$ = (HTMLElement.prototype.$$ = function(aQuery) {
return this.querySelectorAll(aQuery);
}).bind(document);
$$.forEach = function(nodeList, fun) {
Array.prototype.forEach.call(nodeList, fun);
}
</script>
<!-- vim: set fdm=marker: }}} -->