skipfish/report.c

780 lines
21 KiB
C

/*
skipfish - post-processing and reporting
----------------------------------------
Author: Michal Zalewski <lcamtuf@google.com>
Copyright 2009, 2010 by Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <dirent.h>
#include <sys/fcntl.h>
#include "debug.h"
#include "config.h"
#include "types.h"
#include "http_client.h"
#include "database.h"
#include "crawler.h"
#include "analysis.h"
/* Pivot and issue signature data. */
struct p_sig_desc {
u8 type; /* Pivot type */
struct http_sig* res_sig; /* Response signature */
u32 issue_sig; /* Issues fingerprint */
u32 child_sig; /* Children fingerprint */
};
static struct p_sig_desc* p_sig;
static u32 p_sig_cnt;
u8 suppress_dupes;
/* Response, issue sample data. */
struct mime_sample_desc {
u8* det_mime;
struct http_request** req;
struct http_response** res;
u32 sample_cnt;
};
struct issue_sample_desc {
u32 type;
struct issue_desc** i;
u32 sample_cnt;
};
static struct mime_sample_desc* m_samp;
static struct issue_sample_desc* i_samp;
static u32 m_samp_cnt, i_samp_cnt;
/* qsort() helper for sort_annotate_pivot(). */
static int pivot_compar(const void* par1, const void* par2) {
const struct pivot_desc *p1 = *(struct pivot_desc**)par1,
*p2 = *(struct pivot_desc**)par2;
return strcasecmp((char*)p1->name, (char*)p2->name);
}
static int issue_compar(const void* par1, const void* par2) {
const struct issue_desc *i1 = par1, *i2 = par2;
return i2->type - i1->type;
}
/* Recursively annotates and sorts pivots. */
static void sort_annotate_pivot(struct pivot_desc* pv) {
u32 i, path_child = 0;
static u32 proc_cnt;
u8 *q1, *q2;
/* Add notes to all non-dir nodes with dir or file children... */
for (i=0;i<pv->child_cnt;i++) {
if (pv->child[i]->type == PIVOT_FILE || pv->child[i]->type == PIVOT_DIR) path_child = 1;
sort_annotate_pivot(pv->child[i]);
}
if (pv->type != PIVOT_DIR && pv->type != PIVOT_SERV &&
pv->type != PIVOT_ROOT && path_child)
problem(PROB_NOT_DIR, pv->req, pv->res, 0, pv, 0);
/* Non-parametric nodes with digits in the name were not brute-forced,
but the user might be interested in doing so. Skip images here. */
if (pv->fuzz_par == -1 && pv->res &&
(pv->res->sniff_mime_id < MIME_IMG_JPEG ||
pv->res->sniff_mime_id > MIME_AV_WMEDIA) &&
(pv->type == PIVOT_DIR || pv->type == PIVOT_FILE ||
pv->type == PIVOT_PATHINFO) && !pv->missing) {
i = strlen((char*)pv->name);
while (i--)
if (isdigit(pv->name[i])) {
problem(PROB_FUZZ_DIGIT, pv->req, pv->res, 0, pv, 0);
break;
}
}
/* Parametric nodes that seem to contain queries in parameters, and are not
marked as bogus_par, should be marked as dangerous. */
if (pv->fuzz_par != -1 && !pv->bogus_par &&
(((q1 = (u8*)strchr((char*)pv->req->par.v[pv->fuzz_par], '(')) &&
(q2 = (u8*)strchr((char*)pv->req->par.v[pv->fuzz_par], ')')) && q1 < q2)
||
((inl_strcasestr(pv->req->par.v[pv->fuzz_par], (u8*)"SELECT ") ||
inl_strcasestr(pv->req->par.v[pv->fuzz_par], (u8*)"DELETE ") ) &&
inl_strcasestr(pv->req->par.v[pv->fuzz_par], (u8*)" FROM ")) ||
(inl_strcasestr(pv->req->par.v[pv->fuzz_par], (u8*)"UPDATE ") ||
inl_strcasestr(pv->req->par.v[pv->fuzz_par], (u8*)" WHERE ")) ||
inl_strcasestr(pv->req->par.v[pv->fuzz_par], (u8*)"DROP TABLE ") ||
inl_strcasestr(pv->req->par.v[pv->fuzz_par], (u8*)" ORDER BY ")))
problem(PROB_SQL_PARAM, pv->req, pv->res, 0, pv, 0);
/* Sort children nodes and issues as appropriate. */
if (pv->child_cnt > 1)
qsort(pv->child, pv->child_cnt, sizeof(struct pivot_desc*), pivot_compar);
if (pv->issue_cnt > 1)
qsort(pv->issue, pv->issue_cnt, sizeof(struct issue_desc), issue_compar);
if ((!(proc_cnt++ % 50)) || pv->type == PIVOT_ROOT) {
SAY(cLGN "\r[+] " cNOR "Sorting and annotating crawl nodes: %u", proc_cnt);
fflush(0);
}
}
/* Issue extra hashing helper. */
static inline u32 hash_extra(u8* str) {
register u32 ret = 0;
register u8 cur;
if (str)
while ((cur=*str)) {
ret = ~ret ^ (cur) ^
(cur << 5) ^ (~cur >> 5) ^
(cur << 10) ^ (~cur << 15) ^
(cur << 20) ^ (~cur << 25) ^
(cur << 30);
str++;
}
return ret;
}
/* Registers a new pivot signature, or updates an existing one. */
static void maybe_add_sig(struct pivot_desc* pv) {
u32 i, issue_sig = ~pv->issue_cnt,
child_sig = ~pv->child_cnt;
if (!pv->res) return;
/* Compute a rough children node signature based on children types. */
for (i=0;i<pv->child_cnt;i++)
child_sig ^= (hash_extra(pv->child[i]->name) ^
pv->child[i]->type) << (i % 16);
/* Do the same for all recorded issues. */
for (i=0;i<pv->issue_cnt;i++)
issue_sig ^= (hash_extra(pv->issue[i].extra) ^
pv->issue[i].type) << (i % 16);
/* Assign a simplified signature to the pivot. */
pv->pv_sig = (pv->type << 16) ^ ~child_sig ^ issue_sig;
/* See if a matching signature already exists. */
for (i=0;i<p_sig_cnt;i++)
if (p_sig[i].type == pv->type && p_sig[i].issue_sig == issue_sig &&
p_sig[i].child_sig == child_sig &&
same_page(p_sig[i].res_sig, &pv->res->sig)) {
pv->dupe = 1;
return;
}
/* No match - create a new one. */
p_sig = ck_realloc(p_sig, (p_sig_cnt + 1) * sizeof(struct p_sig_desc));
p_sig[p_sig_cnt].type = pv->type;
p_sig[p_sig_cnt].res_sig = &pv->res->sig;
p_sig[p_sig_cnt].issue_sig = issue_sig;
p_sig[p_sig_cnt].child_sig = child_sig;
p_sig_cnt++;
}
/* Recursively collects unique signatures for pivots. */
static void collect_signatures(struct pivot_desc* pv) {
u32 i;
static u32 proc_cnt;
maybe_add_sig(pv);
for (i=0;i<pv->child_cnt;i++) collect_signatures(pv->child[i]);
if ((!(proc_cnt++ % 50)) || pv->type == PIVOT_ROOT) {
SAY(cLGN "\r[+] " cNOR "Looking for duplicate entries: %u", proc_cnt);
fflush(0);
}
}
/* Destroys signature data (for memory profiling purposes). */
void destroy_signatures(void) {
u32 i;
ck_free(p_sig);
for (i=0;i<m_samp_cnt;i++) {
ck_free(m_samp[i].req);
ck_free(m_samp[i].res);
}
for (i=0;i<i_samp_cnt;i++)
ck_free(i_samp[i].i);
ck_free(m_samp);
ck_free(i_samp);
}
/* Prepares issue, pivot stats, backtracing through all children.
Do not count nodes that seem duplicate. */
static void compute_counts(struct pivot_desc* pv) {
u32 i;
struct pivot_desc* tmp = pv->parent;
static u32 proc_cnt;
for (i=0;i<pv->child_cnt;i++) compute_counts(pv->child[i]);
if (pv->dupe) return;
while (tmp) {
tmp->total_child_cnt++;
tmp = tmp->parent;
}
for (i=0;i<pv->issue_cnt;i++) {
u8 sev = PSEV(pv->issue[i].type);
tmp = pv;
while (tmp) {
tmp->total_issues[sev]++;
tmp = tmp->parent;
}
}
if ((!(proc_cnt++ % 50)) || pv->type == PIVOT_ROOT) {
SAY(cLGN "\r[+] " cNOR "Counting unique issues: %u", proc_cnt);
fflush(0);
}
}
/* Helper to JS-escape data. Static buffer, will be destroyed on
subsequent calls. */
static inline u8* js_escape(u8* str) {
u32 len;
static u8* ret;
u8* opos;
if (!str) return (u8*)"[none]";
len = strlen((char*)str);
if (ret) free(ret);
opos = ret = __DFL_ck_alloc(len * 4 + 1);
while (len--) {
if (*str > 0x1f && *str < 0x80 && !strchr("<>\\'\"", *str)) {
*(opos++) = *(str++);
} else {
sprintf((char*)opos, "\\x%02x", *(str++));
opos += 4;
}
}
*opos = 0;
return ret;
}
static void output_scan_info(u64 scan_time, u32 seed) {
FILE* f;
time_t t = time(NULL);
u8* ct = (u8*)ctime(&t);
if (isspace(ct[strlen((char*)ct)-1]))
ct[strlen((char*)ct)-1] = 0;
f = fopen("summary.js", "w");
if (!f) PFATAL("Cannot open 'summary.js'");
fprintf(f, "var sf_version = '%s';\n", VERSION);
fprintf(f, "var scan_date = '%s';\n", js_escape(ct));
fprintf(f, "var scan_seed = '0x%08x';\n", seed);
fprintf(f, "var scan_ms = %llu;\n", (long long)scan_time);
fclose(f);
}
/* Helper to save request, response data. */
static void describe_res(FILE* f, struct http_response* res) {
if (!res) {
fprintf(f, "'fetched': false, 'error': 'Content not fetched'");
return;
}
switch (res->state) {
case 0 ... STATE_OK - 1:
fprintf(f, "'fetched': false, 'error': '(Reported while fetch in progress)'");
break;
case STATE_OK:
fprintf(f, "'fetched': true, 'code': %u, 'len': %u, 'decl_mime': '%s', ",
res->code, res->pay_len,
js_escape(res->header_mime));
fprintf(f, "'sniff_mime': '%s', 'cset': '%s'",
res->sniffed_mime ? res->sniffed_mime : (u8*)"[none]",
js_escape(res->header_charset ? res->header_charset
: res->meta_charset));
break;
case STATE_DNSERR:
fprintf(f, "'fetched': false, 'error': 'DNS error'");
break;
case STATE_LOCALERR:
fprintf(f, "'fetched': false, 'error': 'Local network error'");
break;
case STATE_CONNERR:
fprintf(f, "'fetched': false, 'error': 'Connection error'");
break;
case STATE_RESPERR:
fprintf(f, "'fetched': false, 'error': 'Malformed HTTP response'");
break;
case STATE_SUPPRESS:
fprintf(f, "'fetched': false, 'error': 'Limits exceeded'");
break;
default:
fprintf(f, "'fetched': false, 'error': 'Unknown error'");
}
}
/* Helper to save request, response data. */
static void save_req_res(struct http_request* req, struct http_response* res, u8 sample) {
FILE* f;
if (req) {
u8* rd = build_request_data(req);
f = fopen("request.dat", "w");
if (!f) PFATAL("Cannot create 'request.dat'");
fwrite(rd, strlen((char*)rd), 1, f);
fclose(f);
ck_free(rd);
}
if (res && res->state == STATE_OK) {
u32 i;
f = fopen("response.dat", "w");
if (!f) PFATAL("Cannot create 'response.dat'");
fprintf(f, "HTTP/1.1 %u %s\n", res->code, res->msg);
for (i=0;i<res->hdr.c;i++)
if (res->hdr.t[i] == PARAM_HEADER)
fprintf(f, "%s: %s\n", res->hdr.n[i], res->hdr.v[i]);
else
fprintf(f, "Set-Cookie: %s=%s\n", res->hdr.n[i], res->hdr.v[i]);
fprintf(f, "\n");
fwrite(res->payload, res->pay_len, 1, f);
fclose(f);
/* Also collect MIME samples at this point. */
if (!req->pivot->dupe && res->sniffed_mime && sample) {
for (i=0;i<m_samp_cnt;i++)
if (!strcmp((char*)m_samp[i].det_mime, (char*)res->sniffed_mime)) break;
if (i == m_samp_cnt) {
m_samp = ck_realloc(m_samp, (i + 1) * sizeof(struct mime_sample_desc));
m_samp[i].det_mime = res->sniffed_mime;
m_samp_cnt++;
} else {
u32 c;
/* If we already have something that looks very much the same on the
list, don't bother reporting it again. */
for (c=0;c<m_samp[i].sample_cnt;c++)
if (same_page(&m_samp[i].res[c]->sig, &res->sig)) return;
}
m_samp[i].req = ck_realloc(m_samp[i].req, (m_samp[i].sample_cnt + 1) *
sizeof(struct http_request*));
m_samp[i].res = ck_realloc(m_samp[i].res, (m_samp[i].sample_cnt + 1) *
sizeof(struct http_response*));
m_samp[i].req[m_samp[i].sample_cnt] = req;
m_samp[i].res[m_samp[i].sample_cnt] = res;
m_samp[i].sample_cnt++;
}
}
}
/* Dumps the actual crawl data. */
static void output_crawl_tree(struct pivot_desc* pv) {
u32 i;
FILE* f;
static u32 proc_cnt;
/* Save request, response. */
save_req_res(pv->req, pv->res, 1);
/* Write children information. Don't crawl children just yet,
because we could run out of file descriptors on a particularly
deep tree if we keep one open and recurse. */
f = fopen("child_index.js", "w");
if (!f) PFATAL("Cannot create 'child_index.js'.");
fprintf(f, "var child = [\n");
for (i=0;i<pv->child_cnt;i++) {
u8 tmp[32];
u8* p;
if (suppress_dupes && pv->child[i]->dupe &&
!pv->child[i]->total_child_cnt) continue;
/* Also completely suppress nodes that seem identical to the
previous one, and have a common prefix (as this implies
a mod_rewrite or htaccess filter). */
if (i && pv->child[i-1]->pv_sig == pv->child[i]->pv_sig) {
u8 *pn = pv->child[i-1]->name, *cn = pv->child[i]->name;
u32 pnd = strcspn((char*)pn, ".");
if (!strncasecmp((char*)pn, (char*)cn, pnd)) continue;
}
sprintf((char*)tmp, "c%u", i);
fprintf(f, " { 'dupe': %s, 'type': %u, 'name': '%s%s",
pv->child[i]->dupe ? "true" : "false",
pv->child[i]->type, js_escape(pv->child[i]->name),
(pv->child[i]->fuzz_par == -1 || pv->child[i]->type == PIVOT_VALUE)
? (u8*)"" : (u8*)"=");
fprintf(f, "%s', 'dir': '%s', 'linked': %d, ",
(pv->child[i]->fuzz_par == -1 || pv->child[i]->type == PIVOT_VALUE)
? (u8*)"" :
js_escape(pv->child[i]->req->par.v[pv->child[i]->fuzz_par]),
tmp, pv->child[i]->linked);
p = serialize_path(pv->child[i]->req, 1, 1);
fprintf(f, "'url': '%s', ", js_escape(p));
ck_free(p);
describe_res(f, pv->child[i]->res);
fprintf(f,", 'missing': %s, 'csens': %s, 'child_cnt': %u, "
"'issue_cnt': [ %u, %u, %u, %u, %u ] }%s\n",
pv->child[i]->missing ? "true" : "false",
pv->child[i]->csens ? "true" : "false",
pv->child[i]->total_child_cnt, pv->child[i]->total_issues[1],
pv->child[i]->total_issues[2], pv->child[i]->total_issues[3],
pv->child[i]->total_issues[4], pv->child[i]->total_issues[5],
(i == pv->child_cnt - 1) ? "" : ",");
}
fprintf(f, "];\n");
fclose(f);
/* Write issue index, issue dumps. */
f = fopen("issue_index.js", "w");
if (!f) PFATAL("Cannot create 'issue_index.js'.");
fprintf(f, "var issue = [\n");
for (i=0;i<pv->issue_cnt;i++) {
u8 tmp[32];
sprintf((char*)tmp, "i%u", i);
fprintf(f, " { 'severity': %u, 'type': %u, 'extra': '%s', ",
PSEV(pv->issue[i].type) - 1, pv->issue[i].type,
pv->issue[i].extra ? js_escape(pv->issue[i].extra) : (u8*)"");
describe_res(f, pv->issue[i].res);
fprintf(f, ", 'dir': '%s' }%s\n",
tmp, (i == pv->issue_cnt - 1) ? "" : ",");
if (mkdir((char*)tmp, 0755)) PFATAL("Cannot create '%s'.", tmp);
chdir((char*)tmp);
save_req_res(pv->issue[i].req, pv->issue[i].res, 1);
chdir((char*)"..");
/* Issue samples next! */
if (!pv->dupe) {
u32 c;
for (c=0;c<i_samp_cnt;c++)
if (i_samp[c].type == pv->issue[i].type) break;
if (c == i_samp_cnt) {
i_samp = ck_realloc(i_samp, (c + 1) * sizeof(struct issue_sample_desc));
i_samp_cnt++;
i_samp[c].type = pv->issue[i].type;
}
i_samp[c].i = ck_realloc(i_samp[c].i, (i_samp[c].sample_cnt + 1) *
sizeof(struct issue_desc*));
i_samp[c].i[i_samp[c].sample_cnt] = &pv->issue[i];
i_samp[c].sample_cnt++;
}
}
fprintf(f, "];\n");
fclose(f);
/* Actually crawl children. */
for (i=0;i<pv->child_cnt;i++) {
u8 tmp[32];
sprintf((char*)tmp, "c%u", i);
if (mkdir((char*)tmp, 0755)) PFATAL("Cannot create '%s'.", tmp);
chdir((char*)tmp);
output_crawl_tree(pv->child[i]);
chdir((char*)"..");
}
if ((!(proc_cnt++ % 50)) || pv->type == PIVOT_ROOT) {
SAY(cLGN "\r[+] " cNOR "Counting unique issues: %u", proc_cnt);
fflush(0);
}
}
/* Writes previews of MIME types, issues. */
static int m_samp_qsort(const void* ptr1, const void* ptr2) {
const struct mime_sample_desc *p1 = ptr1, *p2 = ptr2;
return strcasecmp((char*)p1->det_mime, (char*)p2->det_mime);
}
static int i_samp_qsort(const void* ptr1, const void* ptr2) {
const struct issue_sample_desc *p1 = ptr1, *p2 = ptr2;
return p2->type - p1->type;
}
static void output_summary_views() {
u32 i;
FILE* f;
f = fopen("samples.js", "w");
if (!f) PFATAL("Cannot create 'samples.js'.");
qsort(m_samp, m_samp_cnt, sizeof(struct mime_sample_desc), m_samp_qsort);
qsort(i_samp, i_samp_cnt, sizeof(struct issue_sample_desc), i_samp_qsort);
fprintf(f, "var mime_samples = [\n");
for (i=0;i<m_samp_cnt;i++) {
u32 c;
u8 tmp[32];
u32 use_samp = (m_samp[i].sample_cnt > MAX_SAMPLES ? MAX_SAMPLES :
m_samp[i].sample_cnt);
sprintf((char*)tmp, "_m%u", i);
if (mkdir((char*)tmp, 0755)) PFATAL("Cannot create '%s'.", tmp);
chdir((char*)tmp);
fprintf(f, " { 'mime': '%s', 'samples': [\n", m_samp[i].det_mime);
for (c=0;c<use_samp;c++) {
u8 tmp2[32];
u8* p = serialize_path(m_samp[i].req[c], 1, 0);
sprintf((char*)tmp2, "%u", c);
if (mkdir((char*)tmp2, 0755)) PFATAL("Cannot create '%s'.", tmp2);
chdir((char*)tmp2);
save_req_res(m_samp[i].req[c], m_samp[i].res[c], 0);
chdir("..");
fprintf(f, " { 'url': '%s', 'dir': '%s/%s', 'linked': %d, 'len': %d"
" }%s\n", js_escape(p), tmp, tmp2,
m_samp[i].req[c]->pivot->linked, m_samp[i].res[c]->pay_len,
(c == use_samp - 1) ? " ]" : ",");
ck_free(p);
}
fprintf(f, " }%s\n", (i == m_samp_cnt - 1) ? "" : ",");
chdir("..");
}
fprintf(f, "];\n\n");
fprintf(f, "var issue_samples = [\n");
for (i=0;i<i_samp_cnt;i++) {
u32 c;
u8 tmp[32];
u32 use_samp = (i_samp[i].sample_cnt > MAX_SAMPLES ? MAX_SAMPLES :
i_samp[i].sample_cnt);
sprintf((char*)tmp, "_i%u", i);
if (mkdir((char*)tmp, 0755)) PFATAL("Cannot create '%s'.", tmp);
chdir((char*)tmp);
fprintf(f, " { 'severity': %d, 'type': %d, 'samples': [\n",
PSEV(i_samp[i].type) - 1, i_samp[i].type);
for (c=0;c<use_samp;c++) {
u8 tmp2[32];
u8* p = serialize_path(i_samp[i].i[c]->req, 1, 0);
sprintf((char*)tmp2, "%u", c);
if (mkdir((char*)tmp2, 0755)) PFATAL("Cannot create '%s'.", tmp2);
chdir((char*)tmp2);
save_req_res(i_samp[i].i[c]->req, i_samp[i].i[c]->res, 0);
chdir("..");
fprintf(f, " { 'url': '%s', ", js_escape(p));
fprintf(f, "'extra': '%s', 'dir': '%s/%s' }%s\n",
i_samp[i].i[c]->extra ? js_escape(i_samp[i].i[c]->extra) :
(u8*)"", tmp, tmp2,
(c == use_samp - 1) ? " ]" : ",");
ck_free(p);
}
fprintf(f, " }%s\n", (i == i_samp_cnt - 1) ? "" : ",");
chdir("..");
}
fprintf(f, "];\n\n");
fclose(f);
}
/* Copies over assets/... to target directory. */
static u8* ca_out_dir;
static int copy_asset(const struct dirent* d) {
u8 *itmp, *otmp, buf[1024];
s32 i, o;
if (d->d_name[0] == '.' || !strcmp(d->d_name, "COPYING")) return 0;
itmp = ck_alloc(6 + strlen(d->d_name) + 2);
sprintf((char*)itmp, "assets/%s", d->d_name);
i = open((char*)itmp, O_RDONLY);
otmp = ck_alloc(strlen((char*)ca_out_dir) + strlen(d->d_name) + 2);
sprintf((char*)otmp, "%s/%s", ca_out_dir, d->d_name);
o = open((char*)otmp, O_WRONLY | O_CREAT | O_EXCL, 0644);
if (i >= 0 && o >= 0) {
s32 c;
while ((c = read(i, buf, 1024)) > 0) write(o, buf, c);
}
close(i);
close(o);
ck_free(itmp);
ck_free(otmp);
return 0;
}
static void copy_static_code(u8* out_dir) {
struct dirent** d;
ca_out_dir = out_dir;
scandir("assets", &d, copy_asset, NULL);
}
/* Writes report to index.html in the current directory. Will create
subdirectories, helper files, etc. */
void write_report(u8* out_dir, u64 scan_time, u32 seed) {
SAY(cLGN "[+] " cNOR "Copying static resources...\n");
copy_static_code(out_dir);
if (chdir((char*)out_dir)) PFATAL("Cannot chdir to '%s'", out_dir);
sort_annotate_pivot(&root_pivot);
SAY("\n");
collect_signatures(&root_pivot);
SAY("\n");
compute_counts(&root_pivot);
SAY("\n");
SAY(cLGN "[+] " cNOR "Writing scan description...\n");
output_scan_info(scan_time, seed);
output_crawl_tree(&root_pivot);
SAY("\n");
SAY(cLGN "[+] " cNOR "Generating summary views...\n");
output_summary_views();
SAY(cLGN "[+] " cNOR "Report saved to '" cLBL "%s/index.html" cNOR "' ["
cLBL "0x%08x" cNOR "].\n", out_dir, seed);
}