Membaca jejak LSB: Chi-Square Attack dan RS Analysis di browser
Cara kerja dua algoritma steganalysis klasik di Steganalysis Web App, dari histogram pasangan nilai sampai kelompok piksel reguler dan singular, lengkap dengan hasil uji pada dua citra contoh.
- steganalysis
- forensik-digital
- javascript
Steganografi LSB menyembunyikan pesan di bit paling rendah (least significant bit) setiap nilai piksel. Mengubah nilai 8-bit sebesar satu tingkat, misalnya 154 menjadi 155, tidak terlihat oleh mata, sehingga gambar yang membawa pesan tampak sama persis dengan aslinya.
Steganalysis menjawab pertanyaan kebalikannya: apakah sebuah gambar kemungkinan besar menyimpan pesan tersembunyi? Tulisan ini membahas dua algoritma klasik yang dipakai Steganalysis Web App, cara keduanya diterjemahkan ke JavaScript agar berjalan di browser, dan hasilnya pada dua citra contoh.
Ide dasarnya
Saat pesan disisipkan, bit terakhir nilai piksel diganti dengan bit pesan. Pesan yang sudah dienkripsi atau dikompresi terlihat acak, sehingga setiap nilai piksel punya peluang setengah untuk berpindah ke "pasangannya", yaitu nilai yang hanya berbeda di bit terakhir (2k dan 2k+1). Perubahan sekecil ini tidak terlihat, tetapi meninggalkan jejak statistik. Kedua algoritma di bawah mencari jejak itu dengan cara yang berbeda.
Chi-Square Attack: pasangan nilai yang terlalu rapi
Chi-Square Attack (Westfeld & Pfitzmann) melihat histogram nilai piksel sebagai 128 pair of values: (0, 1), (2, 3), …, (254, 255). Pada foto alami, jumlah piksel bernilai 2k dan 2k+1 biasanya berbeda. Penyisipan bit acak meratakan keduanya, karena piksel terus berpindah di dalam pasangannya.
Untuk tiap pasangan, nilai harapannya adalah rata-rata kedua jumlah. Statistik χ² mengukur seberapa jauh histogram dari kondisi rata itu:
for (var k = 0; k < 128; k++) {
var even = hist[2 * k], odd = hist[2 * k + 1];
var expected = (even + odd) / 2.0;
if (expected > 0) {
chi2 += ((even - expected) * (even - expected)) / expected;
valid++;
}
}
var dof = valid - 1;
var pValue = dof > 0 ? 1.0 - chi2Cdf(chi2, dof) : 0.0;Kuncinya ada di cara membaca hasilnya: p-value mendekati 1 berarti pasangan-pasangan itu terlalu seimbang untuk gambar alami, tanda kuat adanya penyisipan. Fungsi chi2Cdf dihitung lewat fungsi gamma tak lengkap (metode Numerical Recipes), jadi tidak butuh pustaka statistik.
Histogram tidak dihitung sekali untuk seluruh gambar. Kode menambah piksel secara kumulatif, yaitu 5%, 10%, 15%, dan seterusnya dari awal gambar, lalu menghitung p-value di setiap titik. Karena pesan LSB sederhana disisipkan berurutan dari piksel pertama, kurvanya membentuk "dataran tinggi" selama masih berada di area pesan, lalu jatuh setelah pesan berakhir. Dari kurva itu aplikasi menyimpulkan dua hal:
- mencurigakan bila p-value tertinggi di 20% awal lebih dari 0,5;
- perkiraan panjang pesan adalah titik sampel terakhir yang berturut-turut masih di atas 0,5.
RS Analysis: mengukur kehalusan kelompok piksel
RS Analysis (Fridrich, Goljan & Du, 2001) tidak bergantung pada urutan penyisipan. Gambar dibagi menjadi kelompok 4 piksel berurutan, lalu "kekasaran" tiap kelompok diukur dengan fungsi diskriminasi f(G) = Σ |xᵢ − xᵢ₊₁|. Makin besar nilainya, makin banyak noise di kelompok tersebut.
Dua fungsi pembalik bit kemudian diterapkan sesuai mask M = [1, 0, 1, 0]:
- F₁ menukar 2k ↔ 2k+1 (
x ^ 1), persis seperti efek penyisipan LSB; - F₋₁ menukar 2k−1 ↔ 2k, yaitu "bayangan" F₁ yang digeser satu tingkat.
Kelompok disebut reguler (R) bila f naik setelah dibalik dan singular (S) bila f turun. Pada gambar alami, kedua mask memberi proporsi yang hampir sama: RM ≈ R−M dan SM ≈ S−M. Penyisipan LSB merusak keseimbangan itu: selisih RM − SM menyempit, sedangkan R−M − S−M justru melebar.
for (var p = 1; p < groupSize; p++) {
var o = g[p], pv = applyMask(o, mask[p]), nv = applyMask(o, -mask[p]);
fOrig += Math.abs(o - prevO);
fPos += Math.abs(pv - prevP);
fNeg += Math.abs(nv - prevN);
prevO = o; prevP = pv; prevN = nv;
}
if (fPos > fOrig) rM++; else if (fPos < fOrig) sM++;
if (fNeg > fOrig) rNM++; else if (fNeg < fOrig) sNM++;Perhitungan yang sama diulang pada salinan gambar yang seluruh LSB-nya dibalik. Dari kedua titik itu, aplikasi memperkirakan proporsi piksel yang membawa pesan:
var d0 = c[0] - c[1], d1 = c[2] - c[3]; // gambar asli
var d0Bar = cb[0] - cb[1], d1Bar = cb[2] - cb[3]; // seluruh LSB dibalik
var x = (d0 - d1) / ((d0Bar - d1Bar) + (d0 - d1));
var pEstimate = Math.max(0.0, Math.min(1.0, Math.abs(2.0 * x)));Paper aslinya menurunkan persamaan kuadrat dari keempat kurva R dan S; implementasi ini memakai bentuk linear yang lebih sederhana. Gambar dianggap mencurigakan bila estimasinya di atas 3%.
Grayscale sebagai acuan, RGB sebagai pendukung
Kedua algoritma dijalankan terpisah pada channel Grayscale, Red, Green, dan Blue. Grayscale dihitung dengan rumus luminance yang sama seperti Image.convert("L") di Pillow (ITU-R 601-2, versi fixed-point), sehingga versi browser dan versi Python membaca piksel yang sama:
gray[i] = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16;Verdict akhir diambil dari Grayscale: Tinggi bila kedua metode mencurigakan, Sedang bila hanya satu, dan Rendah bila tidak ada. Channel warna hanya berperan sebagai pendukung. Verdict Sedang naik menjadi Tinggi bila minimal dua channel R/G/B menunjukkan indikasi kuat, sedangkan indikasi di channel warna saat Grayscale bersih dicatat sebagai kemungkinan noise sensor atau demosaicing.
Hasil pada dua citra contoh
Live demo menyediakan dua citra uji berukuran 720×480: citra asli dan salinannya yang berisi pesan LSB. Hasilnya dari engine.js:
| Citra | p-value Chi-Square (sampel 5–50%) | Estimasi Chi-Square | Estimasi RS | Verdict |
|---|---|---|---|---|
contoh-asli.png | 0 | 0% | 0,136% | Tidak terindikasi (Rendah) |
contoh-stego.png | 0,899–1,000 | 55% | 48,219% | Terindikasi kuat (Tinggi) |
Pada contoh-stego.png, p-value bertahan di atas 0,89 dari sampel 5% sampai 55%, lalu jatuh ke 0,0069 di 60% dan 0 setelahnya. Artinya pesan menempati kira-kira 55–60% piksel pertama. RS Analysis, yang sama sekali tidak memakai urutan piksel, memperkirakan 48,2%. Dua metode yang bekerja dengan cara berbeda sampai pada kesimpulan yang sama: sekitar separuh gambar membawa pesan.
Angka RS-nya juga memperlihatkan teori di atas. Pada citra asli RM = 55,9% dan R−M = 55,8% (hampir sama), sedangkan pada citra berisi pesan RM turun ke 53,2% dan R−M naik ke 58,6%. Citra contoh ini grayscale, sehingga keempat channel memberi angka yang identik.
Hasil ini bisa diulang sendiri di live demo lewat tombol "Citra asli" dan "Citra berisi pesan LSB".
Berjalan sepenuhnya di browser
Versi aslinya adalah aplikasi Flask: gambar diunggah lalu dianalisis di server. engine.js adalah port JavaScript dari modul Python-nya dan menghasilkan JSON dengan format yang sama seperti endpoint /analyze, sehingga antarmukanya tidak perlu diubah. Piksel diambil lewat <canvas> dan getImageData() (alpha diabaikan, seperti Image.convert("RGB")), lalu analisis dijalankan di Web Worker agar halaman tetap responsif untuk gambar besar. Gambar tidak pernah meninggalkan perangkat.
Batasan
- Kedua metode dirancang untuk LSB replacement. Teknik LSB matching, yang menambah atau mengurangi 1 secara acak, tidak menyeimbangkan pasangan nilai (2k, 2k+1) dengan cara yang sama, sehingga jauh lebih sulit dideteksi dengan pendekatan ini.
- Ambang 0,5 dan 3% bersifat heuristik. Perlakukan hasilnya sebagai indikasi, bukan bukti.
- Steganografi LSB di domain spasial tidak bertahan dari kompresi JPEG yang lossy, dan dekoder JPEG yang berbeda bisa menghasilkan nilai piksel yang sedikit berbeda. Hasilnya paling bermakna untuk format lossless seperti PNG dan BMP.