<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Code Obfuscation - appunti pratici</title>
<meta name="description" content="Appunti pratici sull'offuscamento del codice: limiti, tecniche utili, esempi e criteri di valutazione." />
<style>
:root {
--bg: #fbfaf7;
--fg: #171717;
--muted: #5f5a52;
--line: #ded8ce;
--panel: #f1eee8;
--code: #ece7dd;
--accent: #8a4b24;
--accent-soft: #ead8c8;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
line-height: 1.65;
}
.page {
max-width: 920px;
margin: 0 auto;
padding: 48px 22px 72px;
}
header {
display: grid;
gap: 22px;
padding-bottom: 30px;
border-bottom: 1px solid var(--line);
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.eyebrow {
margin: 0;
color: var(--accent);
font: 700 0.78rem/1.2 ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
letter-spacing: .08em;
text-transform: uppercase;
}
h1, h2, h3 { line-height: 1.18; }
h1 {
max-width: 760px;
margin: 0;
font-size: clamp(2.2rem, 6vw, 4.8rem);
font-weight: 500;
letter-spacing: -0.04em;
}
h2 {
margin: 56px 0 14px;
font-size: clamp(1.55rem, 3vw, 2.2rem);
font-weight: 500;
letter-spacing: -0.025em;
}
h3 {
margin: 34px 0 10px;
font-size: 1.15rem;
font-weight: 700;
}
p { margin: 0 0 14px; }
.lead {
max-width: 760px;
margin: 0;
color: var(--muted);
font-size: clamp(1.15rem, 2vw, 1.45rem);
}
.note {
margin: 24px 0;
padding: 18px 20px;
background: var(--panel);
border-left: 3px solid var(--accent);
color: #342d28;
}
.muted { color: var(--muted); }
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin: 22px 0;
}
.card {
padding: 18px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 14px;
}
.card strong {
display: block;
margin-bottom: 6px;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
font-size: .9rem;
}
.card p {
margin: 0;
color: var(--muted);
font-size: .96rem;
}
pre {
margin: 18px 0 8px;
padding: 18px;
overflow-x: auto;
background: var(--code);
border: 1px solid var(--line);
border-radius: 14px;
}
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
font-size: .88rem;
}
p code, li code {
background: var(--code);
border-radius: 5px;
padding: .12em .35em;
}
ul {
padding-left: 1.2rem;
color: var(--muted);
}
li + li { margin-top: 7px; }
.langbar {
display: flex;
gap: 8px;
align-items: center;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
}
.langbtn {
border: 1px solid var(--line);
background: transparent;
color: var(--muted);
padding: 7px 11px;
border-radius: 999px;
cursor: pointer;
font-weight: 650;
}
.langbtn[aria-pressed="true"],
.langbtn:hover {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.footer {
margin-top: 56px;
padding-top: 22px;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: .95rem;
}
a { color: var(--accent); text-underline-offset: 3px; }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
noscript {
display: block;
margin: 20px 0;
padding: 14px 16px;
background: var(--accent-soft);
border-radius: 10px;
}
@media (max-width: 760px) {
.page { padding-top: 32px; }
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main class="page">
<header>
<div class="topbar">
<p class="eyebrow" data-i18n="eyebrow">Reverse engineering / software protection</p>
<nav class="langbar" aria-label="Selettore lingua">
<span class="sr-only" id="langStatus" aria-live="polite"></span>
<button class="langbtn" data-lang="it" aria-pressed="true">IT</button>
<button class="langbtn" data-lang="en" aria-pressed="false">EN</button>
<button class="langbtn" data-lang="pl" aria-pressed="false">PL</button>
</nav>
</div>
<h1 data-i18n="title">Offuscare codice non significa renderlo inviolabile.</h1>
<p class="lead" data-i18n="lead">Significa aumentare il costo dell’analisi. A volte basta far perdere tempo. A volte serve proteggere una parte precisa del flusso. In ogni caso, va progettato con un modello di minaccia reale, non come decorazione.</p>
</header>
<noscript data-i18n="noscript">Il cambio lingua richiede JavaScript. La pagina resta leggibile in italiano.</noscript>
<section>
<h2 data-i18n="what_h2">Il punto di partenza</h2>
<p class="muted" data-i18n="what_p1">Un obfuscator prende un programma e ne produce uno equivalente dal punto di vista osservabile, ma più difficile da capire, modificare o ricostruire. Questo non crea sicurezza assoluta: se un segreto deve comparire in memoria, un attaccante abbastanza motivato può arrivarci.</p>
<p class="muted" data-i18n="what_p2">Il valore pratico sta nel compromesso: rendere costosa l’analisi statica, disturbare quella dinamica e ridurre la probabilità che un attaccante scelga proprio quel binario come bersaglio facile.</p>
<div class="note" data-i18n="note1">Una buona offuscazione non si misura da quanto “strano” sembra il codice, ma da quanto tempo richiede estrarre ciò che volevi proteggere.</div>
</section>
<section>
<h2 data-i18n="threat_h2">Prima domanda: contro chi?</h2>
<div class="grid">
<article class="card">
<strong data-i18n="threat1_h">Solo file binario</strong>
<p data-i18n="threat1_p">L’attaccante analizza il file con strumenti statici. Qui contano nomi, stringhe, grafo di controllo e pattern riconoscibili.</p>
</article>
<article class="card">
<strong data-i18n="threat2_h">Runtime libero</strong>
<p data-i18n="threat2_p">L’attaccante può eseguire, debuggare, tracciare, usare hook o emulatori. L’offuscamento diventa solo uno strato.</p>
</article>
<article class="card">
<strong data-i18n="threat3_h">Tempo limitato</strong>
<p data-i18n="threat3_p">È lo scenario più comune: non devi fermare tutti, devi rendere poco conveniente l’analisi rispetto al valore ottenuto.</p>
</article>
</div>
</section>
<section>
<h2 data-i18n="tech_h2">Tecniche che hanno senso</h2>
<h3 data-i18n="t1_h3">1. Stringhe e dati immediatamente riconoscibili</h3>
<p class="muted" data-i18n="t1_p">URL, nomi di endpoint, messaggi interni e token non dovrebbero stare in chiaro nel binario. Codificarli e decodificarli solo al bisogno è una protezione semplice, ma utile contro analisi superficiali.</p>
<pre><code class="language-rust">fn decrypt_xor(data: &[u8], key: u8) -> String {
let bytes: Vec<u8> = data.iter().map(|b| b ^ key).collect();
String::from_utf8_lossy(&bytes).into_owned()
}
fn main() {
let encoded = [0x2a, 0x27, 0x30, 0x30];
let value = decrypt_xor(&encoded, 0x42);
println!("{}", value);
}</code></pre>
<p class="muted" data-i18n="t1_note">Da sola non basta: una chiave statica si recupera. Meglio derivare chiavi a runtime, spezzare dati sensibili e ridurre il tempo in cui restano in chiaro.</p>
<h3 data-i18n="t2_h3">2. Control-flow flattening</h3>
<p class="muted" data-i18n="t2_p">Il flattening rende meno leggibile il grafo di controllo, trasformando una logica lineare in un dispatcher. È utile contro decompilatori e lettura rapida, ma può peggiorare performance e manutenzione.</p>
<h3 data-i18n="t3_h3">3. Virtualizzazione</h3>
<p class="muted" data-i18n="t3_p">La virtualizzazione traduce porzioni di programma in bytecode custom, eseguito da un interprete interno. È più pesante, ma spesso più efficace sulle funzioni davvero sensibili.</p>
<pre><code class="language-rust">fn exec(code: &[u8]) -> i64 {
let mut pc = 0usize;
let mut stack = Vec::new();
while pc < code.len() {
match code[pc] {
0x01 => { pc += 1; stack.push(code[pc] as i64); }
0x02 => {
let b = stack.pop().unwrap_or(0);
let a = stack.pop().unwrap_or(0);
stack.push(a + b);
}
0xff => break,
_ => break,
}
pc += 1;
}
stack.pop().unwrap_or(0)
}</code></pre>
<h3 data-i18n="t4_h3">4. Controlli anti-debug e anti-tamper</h3>
<p class="muted" data-i18n="t4_p">Controlli su debugger, timing, checksum delle sezioni eseguibili o integrità dei file possono rallentare l’analisi dinamica. Non vanno trattati come muri, ma come attrito.</p>
<pre><code class="language-rust">fn being_traced() -> bool {
let Ok(status) = std::fs::read_to_string("/proc/self/status") else {
return false;
};
status
.lines()
.find(|line| line.starts_with("TracerPid:"))
.and_then(|line| line.split_whitespace().last())
.is_some_and(|pid| pid != "0")
}</code></pre>
</section>
<section>
<h2 data-i18n="metrics_h2">Come capire se serve davvero</h2>
<ul>
<li data-i18n="m1">Misura il tempo necessario a recuperare una stringa, una chiave o una funzione critica.</li>
<li data-i18n="m2">Confronta output di Ghidra, IDA o altri decompilatori prima e dopo la trasformazione.</li>
<li data-i18n="m3">Controlla l’impatto su performance, dimensione binaria, crash rate e debugging interno.</li>
</ul>
<p class="muted" data-i18n="metrics_p">La baseline non offuscata deve restare disponibile per audit, test e manutenzione. L’offuscamento va applicato alla release, non al modo in cui il team ragiona sul codice.</p>
</section>
<section>
<h2 data-i18n="best_h2">Regole pratiche</h2>
<ul>
<li data-i18n="b1">Proteggi poche parti ad alto valore, non tutto il programma senza criterio.</li>
<li data-i18n="b2">Non spedire segreti statici quando puoi derivarli o recuperarli da un canale controllato.</li>
<li data-i18n="b3">Automatizza test di equivalenza dopo ogni trasformazione.</li>
<li data-i18n="b4">Prevedi rotazione, revoca e aggiornamento: prima o poi qualcosa verrà estratto.</li>
</ul>
</section>
<section class="note">
<strong data-i18n="summary_h">In sintesi</strong>
<p data-i18n="summary_p">L’offuscamento è utile quando è mirato, misurabile e inserito in una strategia più ampia. Non sostituisce sicurezza, design corretto o gestione seria dei segreti. Aggiunge costo. E in molti casi è esattamente quello che serve.</p>
</section>
<footer class="footer">
<p>- <a href="https://www.linkedin.com/in/gianfranco-iaculo" rel="me">Gianfranco Iaculo</a></p>
</footer>
</main>
<script>
const I18N = {
it: {
eyebrow: "Reverse engineering / software protection",
title: "Offuscare codice non significa renderlo inviolabile.",
lead: "Significa aumentare il costo dell’analisi. A volte basta far perdere tempo. A volte serve proteggere una parte precisa del flusso. In ogni caso, va progettato con un modello di minaccia reale, non come decorazione.",
noscript: "Il cambio lingua richiede JavaScript. La pagina resta leggibile in italiano.",
what_h2: "Il punto di partenza",
what_p1: "Un obfuscator prende un programma e ne produce uno equivalente dal punto di vista osservabile, ma più difficile da capire, modificare o ricostruire. Questo non crea sicurezza assoluta: se un segreto deve comparire in memoria, un attaccante abbastanza motivato può arrivarci.",
what_p2: "Il valore pratico sta nel compromesso: rendere costosa l’analisi statica, disturbare quella dinamica e ridurre la probabilità che un attaccante scelga proprio quel binario come bersaglio facile.",
note1: "Una buona offuscazione non si misura da quanto “strano” sembra il codice, ma da quanto tempo richiede estrarre ciò che volevi proteggere.",
threat_h2: "Prima domanda: contro chi?",
threat1_h: "Solo file binario",
threat1_p: "L’attaccante analizza il file con strumenti statici. Qui contano nomi, stringhe, grafo di controllo e pattern riconoscibili.",
threat2_h: "Runtime libero",
threat2_p: "L’attaccante può eseguire, debuggare, tracciare, usare hook o emulatori. L’offuscamento diventa solo uno strato.",
threat3_h: "Tempo limitato",
threat3_p: "È lo scenario più comune: non devi fermare tutti, devi rendere poco conveniente l’analisi rispetto al valore ottenuto.",
tech_h2: "Tecniche che hanno senso",
t1_h3: "1. Stringhe e dati immediatamente riconoscibili",
t1_p: "URL, nomi di endpoint, messaggi interni e token non dovrebbero stare in chiaro nel binario. Codificarli e decodificarli solo al bisogno è una protezione semplice, ma utile contro analisi superficiali.",
t1_note: "Da sola non basta: una chiave statica si recupera. Meglio derivare chiavi a runtime, spezzare dati sensibili e ridurre il tempo in cui restano in chiaro.",
t2_h3: "2. Control-flow flattening",
t2_p: "Il flattening rende meno leggibile il grafo di controllo, trasformando una logica lineare in un dispatcher. È utile contro decompilatori e lettura rapida, ma può peggiorare performance e manutenzione.",
t3_h3: "3. Virtualizzazione",
t3_p: "La virtualizzazione traduce porzioni di programma in bytecode custom, eseguito da un interprete interno. È più pesante, ma spesso più efficace sulle funzioni davvero sensibili.",
t4_h3: "4. Controlli anti-debug e anti-tamper",
t4_p: "Controlli su debugger, timing, checksum delle sezioni eseguibili o integrità dei file possono rallentare l’analisi dinamica. Non vanno trattati come muri, ma come attrito.",
metrics_h2: "Come capire se serve davvero",
m1: "Misura il tempo necessario a recuperare una stringa, una chiave o una funzione critica.",
m2: "Confronta output di Ghidra, IDA o altri decompilatori prima e dopo la trasformazione.",
m3: "Controlla l’impatto su performance, dimensione binaria, crash rate e debugging interno.",
metrics_p: "La baseline non offuscata deve restare disponibile per audit, test e manutenzione. L’offuscamento va applicato alla release, non al modo in cui il team ragiona sul codice.",
best_h2: "Regole pratiche",
b1: "Proteggi poche parti ad alto valore, non tutto il programma senza criterio.",
b2: "Non spedire segreti statici quando puoi derivarli o recuperarli da un canale controllato.",
b3: "Automatizza test di equivalenza dopo ogni trasformazione.",
b4: "Prevedi rotazione, revoca e aggiornamento: prima o poi qualcosa verrà estratto.",
summary_h: "In sintesi",
summary_p: "L’offuscamento è utile quando è mirato, misurabile e inserito in una strategia più ampia. Non sostituisce sicurezza, design corretto o gestione seria dei segreti. Aggiunge costo. E in molti casi è esattamente quello che serve."
},
en: {
eyebrow: "Reverse engineering / software protection",
title: "Obfuscating code does not make it unbreakable.",
lead: "It raises the cost of analysis. Sometimes that means wasting an attacker’s time. Sometimes it means protecting one specific part of the flow. Either way, it needs a real threat model, not decorative complexity.",
noscript: "Language switching requires JavaScript. The page remains readable in Italian.",
what_h2: "The starting point",
what_p1: "An obfuscator takes a program and produces an observably equivalent one that is harder to understand, modify, or reconstruct. This does not create absolute security: if a secret must appear in memory, a motivated attacker can eventually reach it.",
what_p2: "The practical value is the tradeoff: make static analysis expensive, disrupt dynamic analysis, and reduce the chance that your binary is treated as an easy target.",
note1: "Good obfuscation is not measured by how strange the code looks, but by how long it takes to extract the thing you wanted to protect.",
threat_h2: "First question: against whom?",
threat1_h: "Binary-only access",
threat1_p: "The attacker analyzes the file with static tools. Names, strings, control-flow graphs and recognizable patterns matter here.",
threat2_h: "Free runtime access",
threat2_p: "The attacker can run, debug, trace, hook or emulate the program. Obfuscation becomes one layer, not the whole defense.",
threat3_h: "Limited time",
threat3_p: "This is the common case: you do not need to stop everyone, you need to make analysis less convenient than the value it returns.",
tech_h2: "Techniques that make sense",
t1_h3: "1. Strings and obvious data",
t1_p: "URLs, endpoint names, internal messages and tokens should not sit plainly in the binary. Encoding them and decoding only when needed is simple, but useful against shallow analysis.",
t1_note: "On its own, this is not enough: a static key can be recovered. Runtime derivation, splitting sensitive data, and reducing cleartext lifetime are better.",
t2_h3: "2. Control-flow flattening",
t2_p: "Flattening makes the control-flow graph harder to read by turning linear logic into a dispatcher. It helps against decompilers and quick reading, but can hurt performance and maintenance.",
t3_h3: "3. Virtualization",
t3_p: "Virtualization translates selected program parts into custom bytecode executed by an internal interpreter. It is heavier, but often more effective for genuinely sensitive functions.",
t4_h3: "4. Anti-debug and anti-tamper checks",
t4_p: "Checks for debuggers, timing anomalies, executable section checksums or file integrity can slow dynamic analysis. Treat them as friction, not walls.",
metrics_h2: "How to know whether it is useful",
m1: "Measure the time needed to recover a string, key, or critical function.",
m2: "Compare Ghidra, IDA, or other decompiler output before and after the transform.",
m3: "Track the impact on performance, binary size, crash rate, and internal debugging.",
metrics_p: "The unobfuscated baseline should remain available for audits, tests, and maintenance. Obfuscation belongs in the release pipeline, not in the way the team reasons about the code.",
best_h2: "Practical rules",
b1: "Protect a few high-value areas, not the whole program blindly.",
b2: "Do not ship static secrets when you can derive them or fetch them through a controlled channel.",
b3: "Automate equivalence tests after every transform.",
b4: "Plan rotation, revocation, and updates: eventually something will be extracted.",
summary_h: "In short",
summary_p: "Obfuscation is useful when it is targeted, measurable, and part of a wider strategy. It does not replace security, sound design, or serious secret management. It adds cost. In many cases, that is exactly what you need."
},
pl: {
eyebrow: "Reverse engineering / ochrona oprogramowania",
title: "Zaciemnianie kodu nie czyni go niezniszczalnym.",
lead: "Podnosi koszt analizy. Czasem wystarczy zabrać atakującemu czas. Czasem trzeba chronić konkretny fragment przepływu. W każdym przypadku potrzebny jest realny model zagrożeń, a nie dekoracyjna złożoność.",
noscript: "Przełączanie języka wymaga JavaScript. Strona pozostaje czytelna po włosku.",
what_h2: "Punkt wyjścia",
what_p1: "Obfuscator bierze program i tworzy jego obserwowalnie równoważną wersję, trudniejszą do zrozumienia, zmiany lub odtworzenia. To nie daje absolutnego bezpieczeństwa: jeśli sekret musi pojawić się w pamięci, zdeterminowany atakujący w końcu do niego dotrze.",
what_p2: "Wartość praktyczna polega na kompromisie: utrudnić analizę statyczną, zakłócić analizę dynamiczną i zmniejszyć szansę, że binarka zostanie uznana za łatwy cel.",
note1: "Dobre zaciemnianie mierzy się nie tym, jak dziwnie wygląda kod, lecz tym, ile czasu zajmuje wydobycie chronionej informacji.",
threat_h2: "Pierwsze pytanie: przeciw komu?",
threat1_h: "Tylko plik binarny",
threat1_p: "Atakujący analizuje plik narzędziami statycznymi. Liczą się nazwy, stringi, graf przepływu sterowania i rozpoznawalne wzorce.",
threat2_h: "Swobodny runtime",
threat2_p: "Atakujący może uruchamiać, debugować, śledzić, hookować albo emulować program. Zaciemnianie jest wtedy tylko jedną warstwą.",
threat3_h: "Ograniczony czas",
threat3_p: "To najczęstszy przypadek: nie trzeba zatrzymać wszystkich, tylko sprawić, że analiza przestanie się opłacać.",
tech_h2: "Techniki, które mają sens",
t1_h3: "1. Stringi i oczywiste dane",
t1_p: "URL-e, nazwy endpointów, komunikaty wewnętrzne i tokeny nie powinny leżeć jawnie w binarce. Kodowanie ich i dekodowanie tylko w razie potrzeby jest proste, ale użyteczne przeciw płytkiej analizie.",
t1_note: "Samo to nie wystarczy: statyczny klucz da się odzyskać. Lepsze jest wyprowadzanie kluczy w runtime, dzielenie danych i skracanie czasu życia tekstu jawnego.",
t2_h3: "2. Spłaszczanie przepływu sterowania",
t2_p: "Flattening utrudnia czytanie grafu przepływu, zamieniając liniową logikę w dispatcher. Pomaga przeciw dekompilatorom i szybkiej lekturze, ale może pogorszyć wydajność i utrzymanie.",
t3_h3: "3. Wirtualizacja",
t3_p: "Wirtualizacja tłumaczy wybrane części programu na własny bytecode wykonywany przez wewnętrzny interpreter. Jest cięższa, ale często skuteczniejsza dla naprawdę wrażliwych funkcji.",
t4_h3: "4. Anti-debug i anti-tamper",
t4_p: "Kontrole debuggerów, anomalii czasu, sum kontrolnych sekcji wykonywalnych lub integralności plików mogą spowolnić analizę dynamiczną. Traktuj je jako tarcie, nie mur.",
metrics_h2: "Jak sprawdzić, czy to działa",
m1: "Zmierz czas potrzebny na odzyskanie stringa, klucza lub krytycznej funkcji.",
m2: "Porównaj wynik Ghidry, IDA lub innych dekompilatorów przed i po transformacji.",
m3: "Śledź wpływ na wydajność, rozmiar binarki, crash rate i debugowanie wewnętrzne.",
metrics_p: "Wersja bez zaciemniania powinna pozostać dostępna do audytów, testów i utrzymania. Zaciemnianie należy do pipeline’u release, nie do sposobu myślenia zespołu o kodzie.",
best_h2: "Praktyczne zasady",
b1: "Chroń kilka miejsc o wysokiej wartości, nie cały program bez kryterium.",
b2: "Nie dostarczaj statycznych sekretów, jeśli możesz je wyprowadzać albo pobierać kontrolowanym kanałem.",
b3: "Automatyzuj testy równoważności po każdej transformacji.",
b4: "Zaplanuj rotację, unieważnianie i aktualizacje: prędzej czy później coś zostanie wyciągnięte.",
summary_h: "W skrócie",
summary_p: "Zaciemnianie jest użyteczne, gdy jest celowane, mierzalne i częścią szerszej strategii. Nie zastępuje bezpieczeństwa, dobrego projektu ani poważnego zarządzania sekretami. Dodaje koszt. W wielu przypadkach właśnie o to chodzi."
}
};
const buttons = document.querySelectorAll('.langbtn');
const status = document.getElementById('langStatus');
function setLang(lang) {
const dict = I18N[lang] || I18N.it;
document.documentElement.lang = lang;
buttons.forEach(button => {
button.setAttribute('aria-pressed', String(button.dataset.lang === lang));
});
document.querySelectorAll('[data-i18n]').forEach(node => {
const key = node.dataset.i18n;
node.textContent = dict[key] || I18N.it[key] || '';
});
status.textContent = {
it: 'Lingua impostata su italiano',
en: 'Language set to English',
pl: 'Język ustawiono na polski'
}[lang] || '';
try { localStorage.setItem('lang', lang); } catch (_) {}
}
buttons.forEach(button => {
button.addEventListener('click', () => setLang(button.dataset.lang));
});
const saved = (() => {
try { return localStorage.getItem('lang'); } catch (_) { return null; }
})();
const browser = (navigator.language || 'it').slice(0, 2);
setLang(saved && I18N[saved] ? saved : (I18N[browser] ? browser : 'it'));
</script>
</body>
</html>