Merakit Opcode True, False, dan NOP untuk x86 & x64.
Panduan merakit machine code mentah di C# untuk memodifikasi fungsi native C++ saat runtime, memahami calling convention, dan sinkronisasi CPU cache.
Tulisan ini adalah versi panjang dari materi yang sama. Slide harus memotong setiap ide jadi satu baris; di sini tidak ada batas itu, jadi setiap bagian dibahas sampai kamu benar-benar bisa mengerjakannya sendiri.
Kamu tidak harus membacanya berurutan. Daftar isi di bawah memetakan seluruh isi tulisan, dan tiap bagian berdiri sendiri kalau kamu cuma butuh satu hal.
Daftar isi
- Mengapa C# Butuh Native Byte Array? — Pada modding Unity (IL2CPP) dan injeksi memori, C# sering dipakai untuk mengendalikan binary native C++.
- Merakit 'Return True' (x86 & x64) — Dalam ABI C/C++, nilai kembalian boolean disimpan di register akumulator terendah (AL).
- Merakit 'Return False' (x86 & x64) — Melakukan XOR register dengan dirinya sendiri adalah standar tercepat dan terbersih untuk me-reset nilai.
- Perbandingan Teknik 'Return False' — Memilih antara instruksi XOR dan MOV berdasarkan arsitektur dan tipe data C++.
- Merakit Payload NOP (0x90) — NOP memerintahkan CPU untuk memajukan Instruction Pointer tanpa mengubah register atau flag apapun.
- Kapan dan Cara Memakai NOP di C++ — NOP lazim digunakan untuk menonaktifkan percabangan (jump), validasi keamanan, dan fungsi tertentu.
- Jebakan Calling Convention (x86 vs x64) — Aturan pembersihan stack menentukan apakah 'ret' biasa (0xC3) aman atau membuat game crash.
- Langkah 1: Membuka Proteksi Memori — Section kode executable (.text) bersifat read-only. Menulis tanpa izin memicu Access Violation.
- Langkah 2: Menulis Byte Array ke Memori — Menyalin data byte[] C# ke unmanaged pointer C++ dengan aman menggunakan Marshal.Copy.
- Langkah 3: Flush CPU Instruction Cache — Penyebab patch native gagal secara misterius: Inkoherensi Cache Arsitektur Harvard.
- Class Native Patching Siap Pakai — Membungkus status patch, backup byte otomatis, dan fitur toggle on/off dalam C#.
- Cheat Sheet Opcode Patching C# ke C++ — Rangkuman praktis opcode mesin dan array byte C# untuk patching fungsi native C++.
1. Mengapa C# Butuh Native Byte Array?
Mari kita bedah bagian Mengapa C# Butuh Native Byte Array?. Pada modding Unity (IL2CPP) dan injeksi memori, C# sering dipakai untuk mengendalikan binary native C++. Aku akan mulai dari gambaran besarnya, baru turun ke detail yang biasanya bikin orang tersandung.
Istilah di bagian ini
| Istilah | Artinya |
|---|
| IL2CPP & Native DLL | Kode C++ dikompilasi langsung ke machine code tanpa metadata CLR/MSIL. |
| In-Memory Patching | Mengubah byte instruksi di section .text secara live saat program berjalan. |
| Byte Array Payload | Array byte[] C# merepresentasikan instruksi CPU x86/x64 yang disuntikkan ke RAM. |
Istilah IL2CPP & Native DLL dipakai untuk kode C++ dikompilasi langsung ke machine code tanpa metadata CLR/MSIL. Kalau kamu menemukannya di dokumentasi lain dengan arti yang terasa berbeda, kemungkinan besar konteksnya yang berbeda.
In-Memory Patching berarti mengubah byte instruksi di section .text secara live saat program berjalan. Istilah ini sering muncul, jadi ada baiknya kamu hafal bentuk persisnya — salah menyebut istilah biasanya berarti salah membayangkan konsepnya juga.
Yang dimaksud Byte Array Payload adalah array byte[] C# merepresentasikan instruksi CPU x86/x64 yang disuntikkan ke RAM. Perhatikan bahwa definisinya sempit; banyak kebingungan muncul karena orang memakai istilah ini lebih luas dari artinya.
Catatan: Berbeda dari Harmony yang memodifikasi bytecode MSIL, native patching langsung menimpa instruksi mesin CPU!
Catatan di atas kelihatan kecil tapi sering jadi penyebab satu sesi debugging penuh. Perlakukan sebagai langkah wajib, bukan saran.
Sampai di sini bagian Mengapa C# Butuh Native Byte Array? seharusnya sudah terasa masuk akal. Kalau belum, baca ulang poin pertamanya — biasanya di situ letak yang belum nyantol.
2. Merakit 'Return True' (x86 & x64)
Bagian ini membahas Merakit 'Return True' (x86 & x64). Dalam ABI C/C++, nilai kembalian boolean disimpan di register akumulator terendah (AL). Kelihatannya sederhana kalau ditulis satu kalimat, tapi justru di sinilah beda antara orang yang hafal langkahnya dengan orang yang paham kenapa langkahnya begitu.
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (PatchTrue.cs):
<span class="token comment">// Assembly: mov al, 1; ret</span>
<span class="token comment">// 0xB0 0x01 -> mov al, 1 (Isi register AL = 1)</span>
<span class="token comment">// 0xC3 -> ret (Langsung return ke pemanggil)</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> PatchTrue <span class="token operator">=</span>
<span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">0xB0</span><span class="token punctuation">,</span> <span class="token number">0x01</span><span class="token punctuation">,</span> <span class="token number">0xC3</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
Membaca kodenya baris demi baris
- Baris 1 adalah komentar — Assembly: mov al, 1; ret
- Baris 2 adalah komentar — 0xB0 0x01 -> mov al, 1 (Isi register AL = 1)
- Baris 3 adalah komentar — 0xC3 -> ret (Langsung return ke pemanggil)
- Baris 4 mendefinisikan PatchTrue; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 5 mengerjakan langkah berikutnya dalam urutan tadi.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Angka-angkanya
| Angka | Keterangan |
|---|
| 3 Byte | Ukuran Payload |
| AL = 1 | State Register |
| x86 / x64 | Arsitektur Universal |
3 Byte pada ukuran Payload memberi gambaran seberapa besar bedanya. Ukur ulang di kasusmu sendiri sebelum memakainya sebagai dasar keputusan.
Perhatikan angka AL = 1 di baris state Register. Angka seperti ini berguna untuk membandingkan dua pendekatan, bukan untuk menjanjikan hasil yang sama di tempat lain.
Soal arsitektur Universal: x86 / x64. Yang penting bukan angkanya, tapi fakta bahwa bedanya cukup besar untuk terasa dalam pemakaian sehari-hari.
Sebelum lanjut, pastikan kamu sudah nyaman dengan bagian Merakit 'Return True' (x86 & x64). Bagian berikutnya dibangun langsung di atasnya.
3. Merakit 'Return False' (x86 & x64)
Mari kita bedah bagian Merakit 'Return False' (x86 & x64). Melakukan XOR register dengan dirinya sendiri adalah standar tercepat dan terbersih untuk me-reset nilai. Aku akan mulai dari gambaran besarnya, baru turun ke detail yang biasanya bikin orang tersandung.
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (PatchFalse.cs):
<span class="token comment">// Assembly: xor eax, eax; ret</span>
<span class="token comment">// 0x31 0xC0 -> xor eax, eax (Reset EAX & zero-extend RAX)</span>
<span class="token comment">// 0xC3 -> ret (Return ke pemanggil)</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> PatchFalse <span class="token operator">=</span>
<span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">0x31</span><span class="token punctuation">,</span> <span class="token number">0xC0</span><span class="token punctuation">,</span> <span class="token number">0xC3</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
Membaca kodenya baris demi baris
- Baris 1 adalah komentar — Assembly: xor eax, eax; ret
- Baris 2 adalah komentar — 0x31 0xC0 -> xor eax, eax (Reset EAX & zero-extend RAX)
- Baris 3 adalah komentar — 0xC3 -> ret (Return ke pemanggil)
- Baris 4 mendefinisikan PatchFalse; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 5 mengerjakan langkah berikutnya dalam urutan tadi.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Angka-angkanya
| Angka | Keterangan |
|---|
| 3 Byte | Ukuran Payload |
| EAX / RAX | Bersih (0) |
| AL = 0 | Nilai Boolean |
Soal ukuran Payload: 3 Byte. Yang penting bukan angkanya, tapi fakta bahwa bedanya cukup besar untuk terasa dalam pemakaian sehari-hari.
Angka EAX / RAX untuk bersih (0) bukan janji, melainkan hasil pengukuran pada kondisi tertentu. Yang bisa kamu bawa dari angka ini adalah urutan besarannya, bukan digit persisnya.
AL = 0 pada nilai Boolean memberi gambaran seberapa besar bedanya. Ukur ulang di kasusmu sendiri sebelum memakainya sebagai dasar keputusan.
Sebelum lanjut, pastikan kamu sudah nyaman dengan bagian Merakit 'Return False' (x86 & x64). Bagian berikutnya dibangun langsung di atasnya.
4. Perbandingan Teknik 'Return False'
Sekarang bagian yang paling sering dilewati waktu belajar cepat: Perbandingan Teknik 'Return False'. Memilih antara instruksi XOR dan MOV berdasarkan arsitektur dan tipe data C++. Padahal begitu bagian ini jelas, sisa materinya jadi jauh lebih mudah diikuti.
Poin-poin pentingnya
0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64.
Inti poin ini ada di satu kalimat. 0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64. Tahan dulu keinginan untuk langsung praktek; pastikan kalimat itu jelas, karena sisa bagian ini mengasumsikan kamu sudah paham.
Poin ini menghemat waktu debugging. Orang yang sudah memegang "0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64" bisa langsung mempersempit pencarian, sementara yang belum akan menebak-nebak satu per satu.
Ini sumber bug klasik. Orang menyalin kode yang jalan di satu kondisi, memakainya di kondisi lain tanpa memeriksa "0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64", lalu menghabiskan berjam-jam mencari kesalahan di tempat yang salah.
Langkah praktisnya sederhana: buktikan dulu "0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64" di lingkunganmu sendiri, baru lanjut. Satu pemeriksaan kecil di depan menghemat penelusuran panjang di belakang.
Batasannya perlu jelas. Selama kamu berada di jalur yang umum, "0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64" bisa kamu pegang. Begitu masuk ke kasus khusus, perlakukan poin ini sebagai titik awal, bukan jaminan.
Ada harga yang harus dibayar. Pendekatan ini menukar kecepatan menulis dengan kepastian hasil — pilihan yang hampir selalu tepat untuk kode yang akan dipakai lagi, dan berlebihan untuk skrip sekali pakai.
Bayangkan kasus ini: semuanya benar hari ini, lalu rusak minggu depan setelah pembaruan yang kelihatannya tidak berhubungan. Penjelasannya hampir selalu sama — pembaruan tadi menggeser kondisi yang membuat "0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64" berlaku.
Cara memverifikasi "0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64": buat satu kasus kecil yang sengaja melanggarnya dan pastikan kamu bisa melihat bedanya. Kalau kamu tidak bisa membedakan yang benar dari yang salah, pemeriksaanmu belum cukup ketat.
0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte.
0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte. Kedengarannya seperti detail teknis kecil, tapi kalimat itu menentukan bagaimana seluruh bagian lain berperilaku. Salah membacanya, dan semua kesimpulan setelahnya ikut meleset.
Alasan poin ini masuk daftar: dia membedakan solusi yang kebetulan jalan dari solusi yang memang benar. Dan solusi yang benar tetap benar setelah kodenya diubah.
Risiko terbesarnya bukan kodenya gagal, tapi kodenya berhasil untuk alasan yang salah. Tanpa memastikan "0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte", kamu tidak punya cara tahu yang mana yang sedang terjadi.
Terapkan poin ini dengan mengubahnya jadi kebiasaan. Setiap kali kamu menyentuh bagian ini, tanyakan lagi apakah "0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte" masih berlaku — jawabannya tidak selalu sama.
Kapan poin ini paling relevan? Saat kamu bekerja dengan sesuatu yang berubah di luar kendalimu. Di situlah "0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte" berhenti jadi teori dan mulai menentukan hasil.
Sisi lain dari poin ini: dia menambah hal yang harus dipahami orang berikutnya yang membaca kodemu. Kalau kamu memakainya, jelaskan alasannya di tempat yang mudah ditemukan.
Skenario yang paling sering terjadi: dua orang menjalankan kode yang sama dan mendapat hasil berbeda. Bukan kodenya yang beda, tapi konteksnya — dan "0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte" adalah bagian konteks itu.
Jangan percaya pada hasil yang benar — hasil yang benar bisa muncul karena kebetulan. Ubah satu kondisi, lalu pastikan hasilnya bergeser persis seperti yang diduga oleh "0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte".
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (FalseVariants.cs):
<span class="token comment">// Variasi A: mov al, 0; ret (3 byte)</span>
<span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> patchFalseAl <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span> <span class="token number">0xB0</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0xC3</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token comment">// Variasi B: mov eax, 0; ret (6 byte - Win32 BOOL)</span>
<span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> patchFalseEax <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span>
<span class="token number">0xB8</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0x00</span><span class="token punctuation">,</span> <span class="token number">0xC3</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
Membaca kodenya baris demi baris
- Baris 1 adalah komentar — Variasi A: mov al, 0; ret (3 byte)
- Baris 2 menyimpan hasilnya ke patchFalseAl. Nilai inilah yang dipakai baris-baris berikutnya.
- Baris 4 adalah komentar — Variasi B: mov eax, 0; ret (6 byte - Win32 BOOL)
- Baris 5 menyimpan hasilnya ke patchFalseEax. Nilai inilah yang dipakai baris-baris berikutnya.
- Baris 6 mengerjakan langkah berikutnya dalam urutan tadi.
- Baris 7 mengerjakan langkah berikutnya dalam urutan tadi.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Kesalahan yang sering terjadi
- Menyimpulkan sesuatu benar hanya karena hasilnya kelihatan benar, tanpa memastikan "0x31 0xC0 (XOR): Standar compiler; hanya 2 byte dan otomatis membersihkan seluruh RAX di x64" lebih dulu.
- Menganggap "0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte" berlaku selamanya. Kondisinya bisa berubah, dan tidak ada yang memberi tahu kamu saat itu terjadi.
Cara memastikan kamu sudah benar
- Bisakah kamu menulis ulang bagian ini dari nol tanpa membuka catatan?
- Bisakah kamu menjelaskan alasan di balik "0xB0 0x00 vs 0xB8: MOV AL menyisakan bit kotor di upper RAX; MOV EAX memakan 6 byte", bukan cuma menyatakan bahwa begitu?
Sampai di sini bagian Perbandingan Teknik 'Return False' seharusnya sudah terasa masuk akal. Kalau belum, baca ulang poin pertamanya — biasanya di situ letak yang belum nyantol.
5. Merakit Payload NOP (0x90)
Sekarang bagian yang paling sering dilewati waktu belajar cepat: Merakit Payload NOP (0x90). NOP memerintahkan CPU untuk memajukan Instruction Pointer tanpa mengubah register atau flag apapun. Padahal begitu bagian ini jelas, sisa materinya jadi jauh lebih mudah diikuti.
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (NopGenerator.cs):
<span class="token comment">// Single NOP (x86 & x64): 0x90</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">readonly</span> <span class="token class-name"><span class="token keyword">byte</span></span> Nop <span class="token operator">=</span> <span class="token number">0x90</span><span class="token punctuation">;</span>
<span class="token comment">// Generator padding NOP dinamis di C#</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token function">NopArray</span><span class="token punctuation">(</span><span class="token class-name"><span class="token keyword">int</span></span> count<span class="token punctuation">)</span> <span class="token operator">=></span>
Enumerable<span class="token punctuation">.</span><span class="token function">Repeat</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token keyword">byte</span><span class="token punctuation">)</span><span class="token number">0x90</span><span class="token punctuation">,</span> count<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">ToArray</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Membaca kodenya baris demi baris
- Baris 1 adalah komentar — Single NOP (x86 & x64): 0x90
- Baris 2 mendefinisikan x90; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 4 adalah komentar — Generator padding NOP dinamis di C#
- Baris 5 mendefinisikan NopArray; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 6 memanggil Enumerable.Repeat dan bekerja dengan nilai yang dikembalikannya.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Angka-angkanya
| Angka | Keterangan |
|---|
| 0x90 | Opcode Byte |
| 1 Cycle | Latency CPU |
| Dinamis | Ukuran Array |
0x90 pada opcode Byte memberi gambaran seberapa besar bedanya. Ukur ulang di kasusmu sendiri sebelum memakainya sebagai dasar keputusan.
Perhatikan angka 1 Cycle di baris latency CPU. Angka seperti ini berguna untuk membandingkan dua pendekatan, bukan untuk menjanjikan hasil yang sama di tempat lain.
Soal ukuran Array: Dinamis. Yang penting bukan angkanya, tapi fakta bahwa bedanya cukup besar untuk terasa dalam pemakaian sehari-hari.
Ringkasnya, Merakit Payload NOP (0x90) bukan soal menghafal sintaks, tapi soal tahu apa yang sebenarnya terjadi di balik layar.
6. Kapan dan Cara Memakai NOP di C++
Bagian ini membahas Kapan dan Cara Memakai NOP di C++. NOP lazim digunakan untuk menonaktifkan percabangan (jump), validasi keamanan, dan fungsi tertentu. Kelihatannya sederhana kalau ditulis satu kalimat, tapi justru di sinilah beda antara orang yang hafal langkahnya dengan orang yang paham kenapa langkahnya begitu.
Istilah di bagian ini
| Istilah | Artinya |
|---|
| Bypass Jump (JE / JNE) | Short jump (0x74 / 0x75) berukuran 2 byte. Timpa keduanya dengan 0x90, 0x90. |
| Mematikan CALL | CALL relatif (0xE8 xx xx xx xx) berukuran 5 byte. Timpa dengan 5x 0x90. |
| Menghapus Proteksi | Menetralkan loop verifikasi memori atau heartbeat anti-cheat secara in-place. |
Bypass Jump (JE / JNE): Short jump (0x74 / 0x75) berukuran 2 byte. Timpa keduanya dengan 0x90, 0x90. Kamu akan menemuinya lagi di bagian-bagian berikutnya, biasanya tanpa penjelasan ulang.
Istilah Mematikan CALL dipakai untuk cALL relatif (0xE8 xx xx xx xx) berukuran 5 byte. Timpa dengan 5x 0x90. Kalau kamu menemukannya di dokumentasi lain dengan arti yang terasa berbeda, kemungkinan besar konteksnya yang berbeda.
Menghapus Proteksi berarti menetralkan loop verifikasi memori atau heartbeat anti-cheat secara in-place. Istilah ini sering muncul, jadi ada baiknya kamu hafal bentuk persisnya — salah menyebut istilah biasanya berarti salah membayangkan konsepnya juga.
Catatan: PENTING: Panjang NOP wajib sama persis dengan instruksi asli yang ditimpa agar CPU tidak crash!
Catatan di atas kelihatan kecil tapi sering jadi penyebab satu sesi debugging penuh. Perlakukan sebagai langkah wajib, bukan saran.
Sebelum lanjut, pastikan kamu sudah nyaman dengan bagian Kapan dan Cara Memakai NOP di C++. Bagian berikutnya dibangun langsung di atasnya.
7. Jebakan Calling Convention (x86 vs x64)
Sekarang bagian yang paling sering dilewati waktu belajar cepat: Jebakan Calling Convention (x86 vs x64). Aturan pembersihan stack menentukan apakah 'ret' biasa (0xC3) aman atau membuat game crash. Padahal begitu bagian ini jelas, sisa materinya jadi jauh lebih mudah diikuti.
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (StdcallPatch.cs):
<span class="token comment">// x86 __stdcall 1 parameter 4-byte: mov al, 1; ret 4</span>
<span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> patchStdcallTrue <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token constructor-invocation class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> <span class="token punctuation">{</span>
<span class="token number">0xB0</span><span class="token punctuation">,</span> <span class="token number">0x01</span><span class="token punctuation">,</span> <span class="token comment">// mov al, 1</span>
<span class="token number">0xC2</span><span class="token punctuation">,</span> <span class="token number">0x04</span><span class="token punctuation">,</span> <span class="token number">0x00</span> <span class="token comment">// ret 4 (pop 0x0004 byte dari stack)</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
Membaca kodenya baris demi baris
- Baris 1 adalah komentar — x86 __stdcall 1 parameter 4-byte: mov al, 1; ret 4
- Baris 2 menyimpan hasilnya ke patchStdcallTrue. Nilai inilah yang dipakai baris-baris berikutnya.
- Baris 3 mengerjakan langkah berikutnya dalam urutan tadi.
- Baris 4 mengerjakan langkah berikutnya dalam urutan tadi.
- Baris 5 mengerjakan langkah berikutnya dalam urutan tadi.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Istilah di bagian ini
| Istilah | Artinya |
|---|
| x64 (Fastcall) | Pemanggil (caller) yang membersihkan stack. 'ret' (0xC3) aman universal. |
| x86 __cdecl | Caller yang membersihkan stack. Cukup gunakan 'ret' biasa (0xC3). |
| x86 __stdcall | Fungsi (callee) wajib membersihkan stack! Butuh 'ret N' (0xC2, low, high). |
x64 (Fastcall): Pemanggil (caller) yang membersihkan stack. 'ret' (0xC3) aman universal. Kamu akan menemuinya lagi di bagian-bagian berikutnya, biasanya tanpa penjelasan ulang.
Istilah x86 __cdecl dipakai untuk caller yang membersihkan stack. Cukup gunakan 'ret' biasa (0xC3). Kalau kamu menemukannya di dokumentasi lain dengan arti yang terasa berbeda, kemungkinan besar konteksnya yang berbeda.
x86 __stdcall berarti fungsi (callee) wajib membersihkan stack! Butuh 'ret N' (0xC2, low, high). Istilah ini sering muncul, jadi ada baiknya kamu hafal bentuk persisnya — salah menyebut istilah biasanya berarti salah membayangkan konsepnya juga.
Sebelum lanjut, pastikan kamu sudah nyaman dengan bagian Jebakan Calling Convention (x86 vs x64). Bagian berikutnya dibangun langsung di atasnya.
8. Langkah 1: Membuka Proteksi Memori
Sebelum masuk ke detail, kunci dari Langkah 1: Membuka Proteksi Memori ada di satu kalimat ini: Section kode executable (.text) bersifat read-only. Menulis tanpa izin memicu Access Violation. Semua yang dibahas di bawah adalah penjelasan dari kalimat tersebut.
Poin-poin pentingnya
Section .text terproteksi; wajib diubah sementara ke PAGE_EXECUTE_READWRITE (0x40).
Kalimat "Section .text terproteksi; wajib diubah sementara ke PAGE_EXECUTE_READWRITE (0x40)" perlu dibaca pelan. Yang dimaksud bukan aturan formal yang harus dihafal, melainkan gambaran tentang apa yang benar-benar terjadi saat kodenya jalan.
Nilai praktisnya ada di sini. Begitu "Section .text terproteksi; wajib diubah sementara ke PAGE_EXECUTE_READWRITE (0x40)" sudah kamu pegang, kamu bisa memprediksi hasilnya sebelum menjalankan apa pun, dan itu jauh lebih cepat daripada trial and error.
Kalau poin ini dilewat, gejalanya biasanya tidak langsung terlihat. Kode tetap jalan, hasilnya tetap keluar, tapi salah — dan bug yang tidak bikin crash adalah bug yang paling lama ketahuan.
Cara memakainya sehari-hari: jadikan pemeriksaan "Section .text terproteksi; wajib diubah sementara ke PAGE_EXECUTE_READWRITE (0x40)" sebagai hal pertama yang kamu lakukan, bukan yang terakhir. Urutan ini penting karena pemeriksaan yang murah harus mendahului yang mahal.
Ada satu situasi yang membuat poin ini berbeda: ketika lingkungannya berubah di tengah jalan. Section .text terproteksi; wajib diubah sementara ke PAGE_EXECUTE_READWRITE (0x40). Tapi itu hanya berlaku untuk kondisi yang ada saat diukur.
Pertimbangan jujurnya, poin ini membuat kode terasa lebih bertele-tele di awal. Yang kamu beli dengan kebertele-telean itu adalah kemampuan mengubah kode nanti tanpa takut merusak sesuatu.
Contoh nyatanya mudah dikenali begitu kamu tahu polanya. Sesuatu bekerja pada percobaan pertama lalu berhenti bekerja pada percobaan kedua, dan penyebabnya bermuara pada "Section .text terproteksi; wajib diubah sementara ke PAGE_EXECUTE_READWRITE (0x40)".
Uji pemahamanmu dengan menjelaskan poin ini ke orang lain tanpa membuka catatan. Kalau kamu tersendat di bagian "kenapa", di situlah yang masih perlu dibaca ulang.
Simpan lpflOldProtect dan kembalikan proteksi asli segera setelah penulisan selesai.
Simpan lpflOldProtect dan kembalikan proteksi asli segera setelah penulisan selesai. Rumusannya sengaja dibuat sempit supaya tidak ambigu — begitu kamu melebarkannya, kamu akan menemukan kasus yang melanggarnya.
Kenapa poin ini penting? Karena "Simpan lpflOldProtect dan kembalikan proteksi asli segera setelah penulisan selesai" adalah asumsi yang dipakai diam-diam oleh hampir semua kode di sekitarnya. Selama asumsinya benar semuanya jalan mulus; begitu dilanggar, kesalahan muncul di tempat yang sama sekali tidak kamu curigai.
Melanggar poin ini jarang menghasilkan pesan error yang jelas. Yang muncul justru perilaku aneh yang kelihatan acak, padahal sebenarnya sangat konsisten begitu kamu memperhitungkan "Simpan lpflOldProtect dan kembalikan proteksi asli segera setelah penulisan selesai".
Dalam praktik, tuliskan asumsinya secara eksplisit sebelum menulis logikanya. Simpan lpflOldProtect dan kembalikan proteksi asli segera setelah penulisan selesai. Taruh kalimat itu di komentar atau di tes, jangan simpan di kepala.
Perlu dicatat, poin ini tidak berlaku universal. Dia mengasumsikan kondisi yang normal; di luar itu keadaannya bisa berubah, dan kamu harus memeriksanya ulang alih-alih mengandalkan ingatan.
Tidak ada yang gratis. Menerapkan poin ini menambah satu langkah lagi yang harus kamu jaga, dan langkah itu ikut menua saat kodenya berkembang. Bandingkan biaya itu dengan waktu debugging yang dihemat sebelum memutuskan.
Contoh konkretnya begini. Kamu punya sesuatu yang jalan sempurna di mesinmu, lalu gagal di tempat lain tanpa perubahan kode sedikit pun. Yang bergeser adalah kondisi di balik "Simpan lpflOldProtect dan kembalikan proteksi asli segera setelah penulisan selesai", dan itulah yang harus kamu cari.
Pemeriksaan tercepat: catat dulu apa yang kamu harapkan terjadi sebelum menjalankan kodenya. Kalau tebakanmu meleset, kamu baru saja menemukan bagian yang belum kamu pahami.
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (MemoryProtection.cs):
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">DllImport</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"kernel32.dll"</span><span class="token punctuation">,</span> SetLastError <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">VirtualProtect</span><span class="token punctuation">(</span>
<span class="token class-name">IntPtr</span> lpAddress<span class="token punctuation">,</span> <span class="token class-name">UIntPtr</span> dwSize<span class="token punctuation">,</span>
<span class="token class-name"><span class="token keyword">uint</span></span> flNewProtect<span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name"><span class="token keyword">uint</span></span> lpflOldProtect<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token class-name"><span class="token keyword">uint</span></span> PAGE_EXECUTE_READWRITE <span class="token operator">=</span> <span class="token number">0x40</span><span class="token punctuation">;</span>
Membaca kodenya baris demi baris
- Baris 1 memanggil DllImport dan bekerja dengan nilai yang dikembalikannya.
- Baris 2 mendefinisikan VirtualProtect; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 3 mengerjakan langkah berikutnya dalam urutan tadi.
- Baris 4 mengerjakan langkah berikutnya dalam urutan tadi.
- Baris 6 menyimpan hasilnya ke PAGE_EXECUTE_READWRITE. Nilai inilah yang dipakai baris-baris berikutnya.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Kesalahan yang sering terjadi
- Menyalin bagian ini dari contoh lain tanpa memeriksa apakah "Section .text terproteksi; wajib diubah sementara ke PAGE_EXECUTE_READWRITE (0x40)" juga benar di kasusmu. Contoh selalu ditulis untuk kondisi tertentu.
- Melewati "Simpan lpflOldProtect dan kembalikan proteksi asli segera setelah penulisan selesai" karena kelihatan sepele, lalu kembali ke sini setelah menghabiskan waktu mencari penyebab di tempat lain.
Cara memastikan kamu sudah benar
- Kalau kondisinya berubah, tahukah kamu bagian mana yang ikut berubah karena "Section .text terproteksi; wajib diubah sementara ke PAGE_EXECUTE_READWRITE (0x40)"?
- Sudahkah kamu mencoba kasus yang sengaja melanggar "Simpan lpflOldProtect dan kembalikan proteksi asli segera setelah penulisan selesai", dan melihat bedanya dengan jelas?
Ringkasnya, Langkah 1: Membuka Proteksi Memori bukan soal menghafal sintaks, tapi soal tahu apa yang sebenarnya terjadi di balik layar.
9. Langkah 2: Menulis Byte Array ke Memori
Mari kita bedah bagian Langkah 2: Menulis Byte Array ke Memori. Menyalin data byte[] C# ke unmanaged pointer C++ dengan aman menggunakan Marshal.Copy. Aku akan mulai dari gambaran besarnya, baru turun ke detail yang biasanya bikin orang tersandung.
Poin-poin pentingnya
Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD.
Inti poin ini ada di satu kalimat. Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD. Tahan dulu keinginan untuk langsung praktek; pastikan kalimat itu jelas, karena sisa bagian ini mengasumsikan kamu sudah paham.
Poin ini menghemat waktu debugging. Orang yang sudah memegang "Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD" bisa langsung mempersempit pencarian, sementara yang belum akan menebak-nebak satu per satu.
Ini sumber bug klasik. Orang menyalin kode yang jalan di satu kondisi, memakainya di kondisi lain tanpa memeriksa "Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD", lalu menghabiskan berjam-jam mencari kesalahan di tempat yang salah.
Langkah praktisnya sederhana: buktikan dulu "Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD" di lingkunganmu sendiri, baru lanjut. Satu pemeriksaan kecil di depan menghemat penelusuran panjang di belakang.
Batasannya perlu jelas. Selama kamu berada di jalur yang umum, "Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD" bisa kamu pegang. Begitu masuk ke kasus khusus, perlakukan poin ini sebagai titik awal, bukan jaminan.
Ada harga yang harus dibayar. Pendekatan ini menukar kecepatan menulis dengan kepastian hasil — pilihan yang hampir selalu tepat untuk kode yang akan dipakai lagi, dan berlebihan untuk skrip sekali pakai.
Bayangkan kasus ini: semuanya benar hari ini, lalu rusak minggu depan setelah pembaruan yang kelihatannya tidak berhubungan. Penjelasannya hampir selalu sama — pembaruan tadi menggeser kondisi yang membuat "Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD" berlaku.
Cara memverifikasi "Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD": buat satu kasus kecil yang sengaja melanggarnya dan pastikan kamu bisa melihat bedanya. Kalau kamu tidak bisa membedakan yang benar dari yang salah, pemeriksaanmu belum cukup ketat.
Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual.
Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual. Kedengarannya seperti detail teknis kecil, tapi kalimat itu menentukan bagaimana seluruh bagian lain berperilaku. Salah membacanya, dan semua kesimpulan setelahnya ikut meleset.
Alasan poin ini masuk daftar: dia membedakan solusi yang kebetulan jalan dari solusi yang memang benar. Dan solusi yang benar tetap benar setelah kodenya diubah.
Risiko terbesarnya bukan kodenya gagal, tapi kodenya berhasil untuk alasan yang salah. Tanpa memastikan "Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual", kamu tidak punya cara tahu yang mana yang sedang terjadi.
Terapkan poin ini dengan mengubahnya jadi kebiasaan. Setiap kali kamu menyentuh bagian ini, tanyakan lagi apakah "Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual" masih berlaku — jawabannya tidak selalu sama.
Kapan poin ini paling relevan? Saat kamu bekerja dengan sesuatu yang berubah di luar kendalimu. Di situlah "Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual" berhenti jadi teori dan mulai menentukan hasil.
Sisi lain dari poin ini: dia menambah hal yang harus dipahami orang berikutnya yang membaca kodemu. Kalau kamu memakainya, jelaskan alasannya di tempat yang mudah ditemukan.
Skenario yang paling sering terjadi: dua orang menjalankan kode yang sama dan mendapat hasil berbeda. Bukan kodenya yang beda, tapi konteksnya — dan "Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual" adalah bagian konteks itu.
Jangan percaya pada hasil yang benar — hasil yang benar bisa muncul karena kebetulan. Ubah satu kondisi, lalu pastikan hasilnya bergeser persis seperti yang diduga oleh "Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual".
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (WriteBytes.cs):
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">WriteNativeBytes</span><span class="token punctuation">(</span><span class="token class-name">IntPtr</span> target<span class="token punctuation">,</span> <span class="token class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> patch<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token class-name">UIntPtr</span> size <span class="token operator">=</span> <span class="token punctuation">(</span>UIntPtr<span class="token punctuation">)</span>patch<span class="token punctuation">.</span>Length<span class="token punctuation">;</span>
<span class="token comment">// 1. Berikan hak akses tulis</span>
<span class="token function">VirtualProtect</span><span class="token punctuation">(</span>target<span class="token punctuation">,</span> size<span class="token punctuation">,</span> <span class="token number">0x40</span><span class="token punctuation">,</span> <span class="token keyword">out</span> <span class="token class-name"><span class="token keyword">uint</span></span> oldProtect<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// 2. Salin byte[] managed ke native memory</span>
Marshal<span class="token punctuation">.</span><span class="token function">Copy</span><span class="token punctuation">(</span>patch<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> target<span class="token punctuation">,</span> patch<span class="token punctuation">.</span>Length<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// 3. Kembalikan proteksi awal</span>
<span class="token function">VirtualProtect</span><span class="token punctuation">(</span>target<span class="token punctuation">,</span> size<span class="token punctuation">,</span> oldProtect<span class="token punctuation">,</span> <span class="token keyword">out</span> _<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
Membaca kodenya baris demi baris
- Baris 1 mendefinisikan WriteNativeBytes; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 2 menyimpan hasilnya ke size. Nilai inilah yang dipakai baris-baris berikutnya.
- Baris 3 adalah komentar — 1. Berikan hak akses tulis
- Baris 4 memanggil VirtualProtect dan bekerja dengan nilai yang dikembalikannya.
- Baris 5 adalah komentar — 2. Salin byte[] managed ke native memory
- Baris 6 memanggil Marshal.Copy dan bekerja dengan nilai yang dikembalikannya.
- Baris 7 adalah komentar — 3. Kembalikan proteksi awal
- Baris 8 memanggil VirtualProtect dan bekerja dengan nilai yang dikembalikannya.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Kesalahan yang sering terjadi
- Melewati "Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD" karena kelihatan sepele, lalu kembali ke sini setelah menghabiskan waktu mencari penyebab di tempat lain.
- Menyimpulkan sesuatu benar hanya karena hasilnya kelihatan benar, tanpa memastikan "Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual" lebih dulu.
Cara memastikan kamu sudah benar
- Bisakah kamu menjelaskan alasan di balik "Marshal.Copy adalah API resmi .NET BCL yang sangat cepat dan teroptimasi SIMD", bukan cuma menyatakan bahwa begitu?
- Kalau kondisinya berubah, tahukah kamu bagian mana yang ikut berubah karena "Tidak membutuhkan block 'unsafe' ataupun pointer pinning (fixed) secara manual"?
Sampai di sini bagian Langkah 2: Menulis Byte Array ke Memori seharusnya sudah terasa masuk akal. Kalau belum, baca ulang poin pertamanya — biasanya di situ letak yang belum nyantol.
10. Langkah 3: Flush CPU Instruction Cache
Bagian ini membahas Langkah 3: Flush CPU Instruction Cache. Penyebab patch native gagal secara misterius: Inkoherensi Cache Arsitektur Harvard. Kelihatannya sederhana kalau ditulis satu kalimat, tapi justru di sinilah beda antara orang yang hafal langkahnya dengan orang yang paham kenapa langkahnya begitu.
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (FlushCache.cs):
<span class="token punctuation">[</span><span class="token attribute"><span class="token class-name">DllImport</span><span class="token attribute-arguments"><span class="token punctuation">(</span><span class="token string">"kernel32.dll"</span><span class="token punctuation">,</span> SetLastError <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span></span></span><span class="token punctuation">]</span>
<span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">extern</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> <span class="token function">FlushInstructionCache</span><span class="token punctuation">(</span>
<span class="token class-name">IntPtr</span> hProcess<span class="token punctuation">,</span> <span class="token class-name">IntPtr</span> lpBaseAddress<span class="token punctuation">,</span> <span class="token class-name">UIntPtr</span> dwSize<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Panggil tepat setelah memulihkan proteksi memori:</span>
<span class="token function">FlushInstructionCache</span><span class="token punctuation">(</span><span class="token function">GetCurrentProcess</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> target<span class="token punctuation">,</span> <span class="token punctuation">(</span>UIntPtr<span class="token punctuation">)</span>patch<span class="token punctuation">.</span>Length<span class="token punctuation">)</span><span class="token punctuation">;</span>
Membaca kodenya baris demi baris
- Baris 1 memanggil DllImport dan bekerja dengan nilai yang dikembalikannya.
- Baris 2 mendefinisikan FlushInstructionCache; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 3 mengerjakan langkah berikutnya dalam urutan tadi.
- Baris 5 adalah komentar — Panggil tepat setelah memulihkan proteksi memori:
- Baris 6 memanggil FlushInstructionCache dan bekerja dengan nilai yang dikembalikannya.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Istilah di bagian ini
| Istilah | Artinya |
|---|
| L1 Data Cache (D-Cache) | Tempat Marshal.Copy menuliskan data byte modifikasi. |
| L1 Instruction Cache (I-Cache) | Tempat CPU mengambil instruksi assembly untuk dieksekusi. |
| Inkoherensi Cache | Tanpa flush, core CPU mengeksekusi instruksi lama yang tersangkut di I-Cache! |
Istilah L1 Data Cache (D-Cache) dipakai untuk tempat Marshal.Copy menuliskan data byte modifikasi. Kalau kamu menemukannya di dokumentasi lain dengan arti yang terasa berbeda, kemungkinan besar konteksnya yang berbeda.
L1 Instruction Cache (I-Cache) berarti tempat CPU mengambil instruksi assembly untuk dieksekusi. Istilah ini sering muncul, jadi ada baiknya kamu hafal bentuk persisnya — salah menyebut istilah biasanya berarti salah membayangkan konsepnya juga.
Yang dimaksud Inkoherensi Cache adalah tanpa flush, core CPU mengeksekusi instruksi lama yang tersangkut di I-Cache. Perhatikan bahwa definisinya sempit; banyak kebingungan muncul karena orang memakai istilah ini lebih luas dari artinya.
Ringkasnya, Langkah 3: Flush CPU Instruction Cache bukan soal menghafal sintaks, tapi soal tahu apa yang sebenarnya terjadi di balik layar.
11. Class Native Patching Siap Pakai
Sekarang bagian yang paling sering dilewati waktu belajar cepat: Class Native Patching Siap Pakai. Membungkus status patch, backup byte otomatis, dan fitur toggle on/off dalam C#. Padahal begitu bagian ini jelas, sisa materinya jadi jauh lebih mudah diikuti.
Poin-poin pentingnya
Rollback Otomatis: Backup byte asli sebelum patch dipasang agar bisa dipulihkan 100% bersih.
Kalimat "Rollback Otomatis: Backup byte asli sebelum patch dipasang agar bisa dipulihkan 100% bersih" perlu dibaca pelan. Yang dimaksud bukan aturan formal yang harus dihafal, melainkan gambaran tentang apa yang benar-benar terjadi saat kodenya jalan.
Nilai praktisnya ada di sini. Begitu "Rollback Otomatis: Backup byte asli sebelum patch dipasang agar bisa dipulihkan 100% bersih" sudah kamu pegang, kamu bisa memprediksi hasilnya sebelum menjalankan apa pun, dan itu jauh lebih cepat daripada trial and error.
Kalau poin ini dilewat, gejalanya biasanya tidak langsung terlihat. Kode tetap jalan, hasilnya tetap keluar, tapi salah — dan bug yang tidak bikin crash adalah bug yang paling lama ketahuan.
Cara memakainya sehari-hari: jadikan pemeriksaan "Rollback Otomatis: Backup byte asli sebelum patch dipasang agar bisa dipulihkan 100% bersih" sebagai hal pertama yang kamu lakukan, bukan yang terakhir. Urutan ini penting karena pemeriksaan yang murah harus mendahului yang mahal.
Ada satu situasi yang membuat poin ini berbeda: ketika lingkungannya berubah di tengah jalan. Rollback Otomatis: Backup byte asli sebelum patch dipasang agar bisa dipulihkan 100% bersih. Tapi itu hanya berlaku untuk kondisi yang ada saat diukur.
Pertimbangan jujurnya, poin ini membuat kode terasa lebih bertele-tele di awal. Yang kamu beli dengan kebertele-telean itu adalah kemampuan mengubah kode nanti tanpa takut merusak sesuatu.
Contoh nyatanya mudah dikenali begitu kamu tahu polanya. Sesuatu bekerja pada percobaan pertama lalu berhenti bekerja pada percobaan kedua, dan penyebabnya bermuara pada "Rollback Otomatis: Backup byte asli sebelum patch dipasang agar bisa dipulihkan 100% bersih".
Uji pemahamanmu dengan menjelaskan poin ini ke orang lain tanpa membuka catatan. Kalau kamu tersendat di bagian "kenapa", di situlah yang masih perlu dibaca ulang.
Runtime Toggle: Bisa dinyalakan/dimatikan kapan saja lewat hotkey tanpa restart game.
Runtime Toggle: Bisa dinyalakan/dimatikan kapan saja lewat hotkey tanpa restart game. Rumusannya sengaja dibuat sempit supaya tidak ambigu — begitu kamu melebarkannya, kamu akan menemukan kasus yang melanggarnya.
Kenapa poin ini penting? Karena "Runtime Toggle: Bisa dinyalakan/dimatikan kapan saja lewat hotkey tanpa restart game" adalah asumsi yang dipakai diam-diam oleh hampir semua kode di sekitarnya. Selama asumsinya benar semuanya jalan mulus; begitu dilanggar, kesalahan muncul di tempat yang sama sekali tidak kamu curigai.
Melanggar poin ini jarang menghasilkan pesan error yang jelas. Yang muncul justru perilaku aneh yang kelihatan acak, padahal sebenarnya sangat konsisten begitu kamu memperhitungkan "Runtime Toggle: Bisa dinyalakan/dimatikan kapan saja lewat hotkey tanpa restart game".
Dalam praktik, tuliskan asumsinya secara eksplisit sebelum menulis logikanya. Runtime Toggle: Bisa dinyalakan/dimatikan kapan saja lewat hotkey tanpa restart game. Taruh kalimat itu di komentar atau di tes, jangan simpan di kepala.
Perlu dicatat, poin ini tidak berlaku universal. Dia mengasumsikan kondisi yang normal; di luar itu keadaannya bisa berubah, dan kamu harus memeriksanya ulang alih-alih mengandalkan ingatan.
Tidak ada yang gratis. Menerapkan poin ini menambah satu langkah lagi yang harus kamu jaga, dan langkah itu ikut menua saat kodenya berkembang. Bandingkan biaya itu dengan waktu debugging yang dihemat sebelum memutuskan.
Contoh konkretnya begini. Kamu punya sesuatu yang jalan sempurna di mesinmu, lalu gagal di tempat lain tanpa perubahan kode sedikit pun. Yang bergeser adalah kondisi di balik "Runtime Toggle: Bisa dinyalakan/dimatikan kapan saja lewat hotkey tanpa restart game", dan itulah yang harus kamu cari.
Pemeriksaan tercepat: catat dulu apa yang kamu harapkan terjadi sebelum menjalankan kodenya. Kalau tebakanmu meleset, kamu baru saja menemukan bagian yang belum kamu pahami.
Berikut bentuk paling sederhana dari apa yang baru saja dijelaskan. Baca dulu utuh, baru lihat pembahasan tiap barisnya.
Kode untuk bagian ini (NativeMemoryPatch.cs):
<span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">NativeMemoryPatch</span> <span class="token punctuation">{</span>
<span class="token keyword">public</span> <span class="token return-type class-name">IntPtr</span> Address <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> Original <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">byte</span><span class="token punctuation">[</span><span class="token punctuation">]</span></span> Patch <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">bool</span></span> IsApplied <span class="token punctuation">{</span> <span class="token keyword">get</span><span class="token punctuation">;</span> <span class="token keyword">private</span> <span class="token keyword">set</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Enable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">Apply</span><span class="token punctuation">(</span>Patch<span class="token punctuation">)</span><span class="token punctuation">;</span> IsApplied <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">public</span> <span class="token return-type class-name"><span class="token keyword">void</span></span> <span class="token function">Disable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">Apply</span><span class="token punctuation">(</span>Original<span class="token punctuation">)</span><span class="token punctuation">;</span> IsApplied <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
Membaca kodenya baris demi baris
- Baris 1 mendefinisikan NativeMemoryPatch; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 2 mendefinisikan get; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 3 mendefinisikan get; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 4 mendefinisikan get; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 5 mendefinisikan set; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 6 mendefinisikan Enable; ini bagian yang akan kamu panggil dari tempat lain.
- Baris 7 mendefinisikan Disable; ini bagian yang akan kamu panggil dari tempat lain.
Coba ubah satu nilai di potongan ini dan tebak dulu hasilnya sebelum menjalankannya. Itu cara tercepat mengubah bacaan jadi pemahaman.
Kesalahan yang sering terjadi
- Menyalin bagian ini dari contoh lain tanpa memeriksa apakah "Rollback Otomatis: Backup byte asli sebelum patch dipasang agar bisa dipulihkan 100% bersih" juga benar di kasusmu. Contoh selalu ditulis untuk kondisi tertentu.
- Melewati "Runtime Toggle: Bisa dinyalakan/dimatikan kapan saja lewat hotkey tanpa restart game" karena kelihatan sepele, lalu kembali ke sini setelah menghabiskan waktu mencari penyebab di tempat lain.
Cara memastikan kamu sudah benar
- Sudahkah kamu mencoba kasus yang sengaja melanggar "Rollback Otomatis: Backup byte asli sebelum patch dipasang agar bisa dipulihkan 100% bersih", dan melihat bedanya dengan jelas?
- Bisakah kamu menulis ulang bagian ini dari nol tanpa membuka catatan?
Ringkasnya, Class Native Patching Siap Pakai bukan soal menghafal sintaks, tapi soal tahu apa yang sebenarnya terjadi di balik layar.
12. Cheat Sheet Opcode Patching C# ke C++
Bagian ini membahas Cheat Sheet Opcode Patching C# ke C++. Rangkuman praktis opcode mesin dan array byte C# untuk patching fungsi native C++. Kelihatannya sederhana kalau ditulis satu kalimat, tapi justru di sinilah beda antara orang yang hafal langkahnya dengan orang yang paham kenapa langkahnya begitu.
Istilah di bagian ini
| Istilah | Artinya |
|---|
| Return True | new byte[] { 0xB0, 0x01, 0xC3 } // mov al, 1; ret (3b). |
| Return False | new byte[] { 0x31, 0xC0, 0xC3 } // xor eax, eax; ret (3b). |
| NOP Padding | new byte[] { 0x90 } // Menimpa jump/call (1b). |
| Pipeline Inti | VirtualProtect -> Marshal.Copy -> Restore -> FlushCache. |
Return True: new byte[] { 0xB0, 0x01, 0xC3 } // mov al, 1; ret (3b). Kamu akan menemuinya lagi di bagian-bagian berikutnya, biasanya tanpa penjelasan ulang.
Istilah Return False dipakai untuk new byte[] { 0x31, 0xC0, 0xC3 } // xor eax, eax; ret (3b). Kalau kamu menemukannya di dokumentasi lain dengan arti yang terasa berbeda, kemungkinan besar konteksnya yang berbeda.
NOP Padding berarti new byte[] { 0x90 } // Menimpa jump/call (1b). Istilah ini sering muncul, jadi ada baiknya kamu hafal bentuk persisnya — salah menyebut istilah biasanya berarti salah membayangkan konsepnya juga.
Yang dimaksud Pipeline Inti adalah virtualProtect -> Marshal.Copy -> Restore -> FlushCache. Perhatikan bahwa definisinya sempit; banyak kebingungan muncul karena orang memakai istilah ini lebih luas dari artinya.
Catatan: Selalu perhatikan calling convention dan wajib panggil FlushInstructionCache agar stabil!
Catatan di atas kelihatan kecil tapi sering jadi penyebab satu sesi debugging penuh. Perlakukan sebagai langkah wajib, bukan saran.
Ringkasnya, Cheat Sheet Opcode Patching C# ke C++ bukan soal menghafal sintaks, tapi soal tahu apa yang sebenarnya terjadi di balik layar.
Daftar istilah
| Istilah | Artinya |
|---|
| IL2CPP & Native DLL | Kode C++ dikompilasi langsung ke machine code tanpa metadata CLR/MSIL. |
| In-Memory Patching | Mengubah byte instruksi di section .text secara live saat program berjalan. |
| Byte Array Payload | Array byte[] C# merepresentasikan instruksi CPU x86/x64 yang disuntikkan ke RAM. |
| Bypass Jump (JE / JNE) | Short jump (0x74 / 0x75) berukuran 2 byte. Timpa keduanya dengan 0x90, 0x90. |
| Mematikan CALL | CALL relatif (0xE8 xx xx xx xx) berukuran 5 byte. Timpa dengan 5x 0x90. |
| Menghapus Proteksi | Menetralkan loop verifikasi memori atau heartbeat anti-cheat secara in-place. |
| x64 (Fastcall) | Pemanggil (caller) yang membersihkan stack. 'ret' (0xC3) aman universal. |
| x86 __cdecl | Caller yang membersihkan stack. Cukup gunakan 'ret' biasa (0xC3). |
| x86 __stdcall | Fungsi (callee) wajib membersihkan stack! Butuh 'ret N' (0xC2, low, high). |
| L1 Data Cache (D-Cache) | Tempat Marshal.Copy menuliskan data byte modifikasi. |
| L1 Instruction Cache (I-Cache) | Tempat CPU mengambil instruksi assembly untuk dieksekusi. |
| Inkoherensi Cache | Tanpa flush, core CPU mengeksekusi instruksi lama yang tersangkut di I-Cache! |
| Return True | new byte[] { 0xB0, 0x01, 0xC3 } // mov al, 1; ret (3b). |
| Return False | new byte[] { 0x31, 0xC0, 0xC3 } // xor eax, eax; ret (3b). |
| NOP Padding | new byte[] { 0x90 } // Menimpa jump/call (1b). |
| Pipeline Inti | VirtualProtect -> Marshal.Copy -> Restore -> FlushCache. |
Pertanyaan yang sering muncul
Mengapa C# Butuh Native Byte Array?
Singkatnya: Pada modding Unity (IL2CPP) dan injeksi memori, C# sering dipakai untuk mengendalikan binary native C++. Bagian Mengapa C# Butuh Native Byte Array? di atas membahasnya lengkap, termasuk kasus-kasus yang bikin jawabannya berubah.
Kenapa Merakit 'Return True' (x86 & x64) penting?
Jawaban pendeknya ada di bagian Merakit 'Return True' (x86 & x64). Dalam ABI C/C++, nilai kembalian boolean disimpan di register akumulator terendah (AL). Yang panjang butuh sedikit konteks, dan konteks itu ada di poin-poin bagian tersebut.
Bagaimana cara mulai dengan Merakit 'Return False' (x86 & x64)?
Melakukan XOR register dengan dirinya sendiri adalah standar tercepat dan terbersih untuk me-reset nilai. Itu jawaban yang berlaku untuk kebanyakan kasus; bagian Merakit 'Return False' (x86 & x64) menjelaskan kapan jawabannya perlu disesuaikan.
Apa sebenarnya maksud dari Perbandingan Teknik 'Return False'?
Ini pertanyaan yang wajar, dan jawabannya bukan satu kalimat. Memilih antara instruksi XOR dan MOV berdasarkan arsitektur dan tipe data C++. Baca bagian Perbandingan Teknik 'Return False' untuk versi utuhnya.
Kenapa Merakit Payload NOP (0x90) penting?
Singkatnya: NOP memerintahkan CPU untuk memajukan Instruction Pointer tanpa mengubah register atau flag apapun. Bagian Merakit Payload NOP (0x90) di atas membahasnya lengkap, termasuk kasus-kasus yang bikin jawabannya berubah.
Kapan dan Cara Memakai NOP di C++?
Jawaban pendeknya ada di bagian Kapan dan Cara Memakai NOP di C++. NOP lazim digunakan untuk menonaktifkan percabangan (jump), validasi keamanan, dan fungsi tertentu. Yang panjang butuh sedikit konteks, dan konteks itu ada di poin-poin bagian tersebut.
Apa sebenarnya maksud dari Jebakan Calling Convention (x86 vs x64)?
Aturan pembersihan stack menentukan apakah 'ret' biasa (0xC3) aman atau membuat game crash. Itu jawaban yang berlaku untuk kebanyakan kasus; bagian Jebakan Calling Convention (x86 vs x64) menjelaskan kapan jawabannya perlu disesuaikan.
Kenapa Langkah 1: Membuka Proteksi Memori penting?
Ini pertanyaan yang wajar, dan jawabannya bukan satu kalimat. Section kode executable (.text) bersifat read-only. Menulis tanpa izin memicu Access Violation. Baca bagian Langkah 1: Membuka Proteksi Memori untuk versi utuhnya.
Bagaimana cara mulai dengan Langkah 2: Menulis Byte Array ke Memori?
Singkatnya: Menyalin data byte[] C# ke unmanaged pointer C++ dengan aman menggunakan Marshal.Copy. Bagian Langkah 2: Menulis Byte Array ke Memori di atas membahasnya lengkap, termasuk kasus-kasus yang bikin jawabannya berubah.
Apa sebenarnya maksud dari Langkah 3: Flush CPU Instruction Cache?
Jawaban pendeknya ada di bagian Langkah 3: Flush CPU Instruction Cache. Penyebab patch native gagal secara misterius: Inkoherensi Cache Arsitektur Harvard. Yang panjang butuh sedikit konteks, dan konteks itu ada di poin-poin bagian tersebut.
Kenapa Class Native Patching Siap Pakai penting?
Membungkus status patch, backup byte otomatis, dan fitur toggle on/off dalam C#. Itu jawaban yang berlaku untuk kebanyakan kasus; bagian Class Native Patching Siap Pakai menjelaskan kapan jawabannya perlu disesuaikan.
Bagaimana cara mulai dengan Cheat Sheet Opcode Patching C# ke C++?
Ini pertanyaan yang wajar, dan jawabannya bukan satu kalimat. Rangkuman praktis opcode mesin dan array byte C# untuk patching fungsi native C++. Baca bagian Cheat Sheet Opcode Patching C# ke C++ untuk versi utuhnya.
Latihan
- Latihan untuk Mengapa C# Butuh Native Byte Array? — jelaskan bagian ini ke orang yang belum pernah mendengarnya. Bagian yang paling susah dijelaskan adalah bagian yang paling belum kamu kuasai.
- Coba terapkan Merakit 'Return True' (x86 & x64) pada kasusmu sendiri, bukan pada contoh di tulisan ini. Kasus sendiri selalu memunculkan detail yang contoh tidak punya.
- Ambil bagian Merakit 'Return False' (x86 & x64) dan kerjakan ulang dari nol tanpa melihat tulisan ini. Berhenti di titik pertama yang bikin kamu ragu — di situ yang perlu dibaca ulang.
- Untuk Perbandingan Teknik 'Return False': buat satu contoh terkecil yang bisa jalan, lalu rusak dengan sengaja. Perhatikan gejalanya, karena gejala itu yang akan kamu kenali nanti.
- Latihan untuk Merakit Payload NOP (0x90) — jelaskan bagian ini ke orang yang belum pernah mendengarnya. Bagian yang paling susah dijelaskan adalah bagian yang paling belum kamu kuasai.
- Coba terapkan Kapan dan Cara Memakai NOP di C++ pada kasusmu sendiri, bukan pada contoh di tulisan ini. Kasus sendiri selalu memunculkan detail yang contoh tidak punya.
- Ambil bagian Jebakan Calling Convention (x86 vs x64) dan kerjakan ulang dari nol tanpa melihat tulisan ini. Berhenti di titik pertama yang bikin kamu ragu — di situ yang perlu dibaca ulang.
- Untuk Langkah 1: Membuka Proteksi Memori: buat satu contoh terkecil yang bisa jalan, lalu rusak dengan sengaja. Perhatikan gejalanya, karena gejala itu yang akan kamu kenali nanti.
- Latihan untuk Langkah 2: Menulis Byte Array ke Memori — jelaskan bagian ini ke orang yang belum pernah mendengarnya. Bagian yang paling susah dijelaskan adalah bagian yang paling belum kamu kuasai.
- Coba terapkan Langkah 3: Flush CPU Instruction Cache pada kasusmu sendiri, bukan pada contoh di tulisan ini. Kasus sendiri selalu memunculkan detail yang contoh tidak punya.
- Ambil bagian Class Native Patching Siap Pakai dan kerjakan ulang dari nol tanpa melihat tulisan ini. Berhenti di titik pertama yang bikin kamu ragu — di situ yang perlu dibaca ulang.
- Untuk Cheat Sheet Opcode Patching C# ke C++: buat satu contoh terkecil yang bisa jalan, lalu rusak dengan sengaja. Perhatikan gejalanya, karena gejala itu yang akan kamu kenali nanti.
Ringkasan
- Mengapa C# Butuh Native Byte Array? — Pada modding Unity (IL2CPP) dan injeksi memori, C# sering dipakai untuk mengendalikan binary native C++.
- Merakit 'Return True' (x86 & x64): Dalam ABI C/C++, nilai kembalian boolean disimpan di register akumulator terendah (AL).
- Merakit 'Return False' (x86 & x64) — Melakukan XOR register dengan dirinya sendiri adalah standar tercepat dan terbersih untuk me-reset nilai.
- Perbandingan Teknik 'Return False': Memilih antara instruksi XOR dan MOV berdasarkan arsitektur dan tipe data C++.
- Merakit Payload NOP (0x90) — NOP memerintahkan CPU untuk memajukan Instruction Pointer tanpa mengubah register atau flag apapun.
- Kapan dan Cara Memakai NOP di C++: NOP lazim digunakan untuk menonaktifkan percabangan (jump), validasi keamanan, dan fungsi tertentu.
- Jebakan Calling Convention (x86 vs x64) — Aturan pembersihan stack menentukan apakah 'ret' biasa (0xC3) aman atau membuat game crash.
- Langkah 1: Membuka Proteksi Memori: Section kode executable (.text) bersifat read-only. Menulis tanpa izin memicu Access Violation.
- Langkah 2: Menulis Byte Array ke Memori — Menyalin data byte[] C# ke unmanaged pointer C++ dengan aman menggunakan Marshal.Copy.
- Langkah 3: Flush CPU Instruction Cache: Penyebab patch native gagal secara misterius: Inkoherensi Cache Arsitektur Harvard.
- Class Native Patching Siap Pakai — Membungkus status patch, backup byte otomatis, dan fitur toggle on/off dalam C#.
- Cheat Sheet Opcode Patching C# ke C++: Rangkuman praktis opcode mesin dan array byte C# untuk patching fungsi native C++.
Yang perlu dibawa pulang
- Pahami konsepnya dulu sebelum menyalin kode; kode yang tidak kamu mengerti akan sulit kamu perbaiki saat rusak.
- Uji satu perubahan dalam satu waktu supaya jelas bagian mana yang bikin hasilnya berubah.
- Catat langkah yang berhasil — catatan kecil hari ini menghemat berjam-jam debugging bulan depan.
- Kalau sebuah bagian belum masuk akal, jangan lompat; bagian berikutnya biasanya dibangun di atasnya.
Penutup
Kalau tulisan ini membantu, simpan dulu biar gampang dibuka lagi saat kamu butuh. Ada bagian yang masih bikin bingung? Tulis di kolom komentar — pertanyaan yang sering muncul biasanya jadi tulisan berikutnya.
Ikuti @unreliablecode untuk pembahasan lain seputar reverse engineering, coding, dan tools yang dipakai sehari-hari.
Дыскусія (0)