mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
fix(unpacker): validate executable entry transforms
This commit is contained in:
@@ -831,14 +831,17 @@ impl<'a> Unpacker<'a> {
|
|||||||
// (i.e., the 4 bytes immediately after the last instance of that pattern).
|
// (i.e., the 4 bytes immediately after the last instance of that pattern).
|
||||||
let v6 = find_v_after_pad(&u.decompressed, stage4_field, stage4_dlen)
|
let v6 = find_v_after_pad(&u.decompressed, stage4_field, stage4_dlen)
|
||||||
.unwrap_or_else(|| idb_pos.wrapping_sub(24));
|
.unwrap_or_else(|| idb_pos.wrapping_sub(24));
|
||||||
let mut accum2 = get_u32(&u.decompressed, v6);
|
let accum2_seed = get_u32(&u.decompressed, v6);
|
||||||
for m in 0..3u32 {
|
let mut accum2 = accum2_seed;
|
||||||
|
let mut accum2_candidates = vec![(0u32, accum2_seed)];
|
||||||
|
for m in 0..8u32 {
|
||||||
let bound = (m + 1).wrapping_mul(25) << 2;
|
let bound = (m + 1).wrapping_mul(25) << 2;
|
||||||
let mut i: u32 = 1;
|
let mut i: u32 = 1;
|
||||||
while i <= bound {
|
while i <= bound {
|
||||||
accum2 = accum2.wrapping_add(i);
|
accum2 = accum2.wrapping_add(i);
|
||||||
i = i.wrapping_add(1);
|
i = i.wrapping_add(1);
|
||||||
}
|
}
|
||||||
|
accum2_candidates.push((m + 1, accum2));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ops1 = match generate(&u.decompressed, data_offset) {
|
let ops1 = match generate(&u.decompressed, data_offset) {
|
||||||
@@ -852,19 +855,58 @@ impl<'a> Unpacker<'a> {
|
|||||||
|
|
||||||
let at4 = stage1.wrapping_add(stage2_off.wrapping_add(216));
|
let at4 = stage1.wrapping_add(stage2_off.wrapping_add(216));
|
||||||
let stage5_field = get_u32(&u.decompressed, at4);
|
let stage5_field = get_u32(&u.decompressed, at4);
|
||||||
// at4 is a (src, src_len, dest, dest_len) quad; only src and dest_len
|
let stage5_slen = get_u32(&u.decompressed, at4.wrapping_add(4));
|
||||||
// are needed here, the other two are consumed by the decrypt below.
|
let stage5_dest = get_u32(&u.decompressed, at4.wrapping_add(8));
|
||||||
let stage5_dlen = get_u32(&u.decompressed, at4.wrapping_add(12));
|
let stage5_dlen = get_u32(&u.decompressed, at4.wrapping_add(12));
|
||||||
if verbose {
|
if verbose {
|
||||||
println!(" stage5 = 0x{:08X}", stage5_field);
|
println!(" stage5 = 0x{:08X}", stage5_field);
|
||||||
|
println!(
|
||||||
|
" stage5 descriptor = [0x{stage5_field:08X}, 0x{stage5_slen:08X}, 0x{stage5_dest:08X}, 0x{stage5_dlen:08X}]"
|
||||||
|
);
|
||||||
|
println!(" stage5 bytecode = 0x{data_offset:08X}");
|
||||||
|
println!(" stage5 accumulator seed = 0x{accum2_seed:08X} at 0x{v6:08X}");
|
||||||
}
|
}
|
||||||
if let Err(reason) =
|
let stage5_lo = stage5_field.min(stage5_dest) as usize;
|
||||||
u.decrypt_and_decompress_data(at4, xor_acc ^ chk4 ^ chk5 ^ accum2, Some(&ops1))
|
let stage5_hi = stage5_field
|
||||||
{
|
.checked_add(stage5_slen)
|
||||||
|
.zip(stage5_dest.checked_add(stage5_dlen))
|
||||||
|
.map(|(source_end, dest_end)| source_end.max(dest_end) as usize)
|
||||||
|
.filter(|&end| stage5_lo <= end && end <= u.decompressed.len())
|
||||||
|
.ok_or(UnpackError::BufferRangeOutOfBounds {
|
||||||
|
operation: BufferOperation::Read,
|
||||||
|
offset: stage5_lo,
|
||||||
|
size: stage5_slen.max(stage5_dlen) as usize,
|
||||||
|
buffer_len: u.decompressed.len(),
|
||||||
|
})?;
|
||||||
|
let stage5_backup = u.decompressed[stage5_lo..stage5_hi].to_vec();
|
||||||
|
let mut first_failure = None;
|
||||||
|
let mut selected_rounds = None;
|
||||||
|
for rounds in std::iter::once(3u32).chain((0..=8).filter(|&rounds| rounds != 3)) {
|
||||||
|
u.decompressed[stage5_lo..stage5_hi].copy_from_slice(&stage5_backup);
|
||||||
|
let candidate_accum = accum2_candidates[rounds as usize].1;
|
||||||
|
match u.decrypt_and_decompress_data(
|
||||||
|
at4,
|
||||||
|
xor_acc ^ chk4 ^ chk5 ^ candidate_accum,
|
||||||
|
Some(&ops1),
|
||||||
|
) {
|
||||||
|
Ok(()) => {
|
||||||
|
selected_rounds = Some(rounds);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(reason) => {
|
||||||
|
first_failure.get_or_insert(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(selected_rounds) = selected_rounds else {
|
||||||
|
u.decompressed[stage5_lo..stage5_hi].copy_from_slice(&stage5_backup);
|
||||||
return Err(UnpackError::StageDecompressionFailed {
|
return Err(UnpackError::StageDecompressionFailed {
|
||||||
stage: DecompressionStage::ExeStage5,
|
stage: DecompressionStage::ExeStage5,
|
||||||
reason,
|
reason: first_failure.expect("at least one Stage5 candidate was tried"),
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
if verbose {
|
||||||
|
println!(" selected stage5 accumulator rounds = {selected_rounds}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inside stage5, the loader stores a table of (ptr, size) pairs at a
|
// Inside stage5, the loader stores a table of (ptr, size) pairs at a
|
||||||
|
|||||||
@@ -77,6 +77,49 @@ fn rva_to_off(secs: &[Section], file_len: usize, rva: u32, need: u32) -> Option<
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_executable_rva(secs: &[Section], rva: u32) -> bool {
|
||||||
|
secs.iter().any(|section| {
|
||||||
|
let span = section.vsize.max(section.raw_size);
|
||||||
|
rva >= section.va
|
||||||
|
&& rva < section.va.wrapping_add(span)
|
||||||
|
&& (section.chars & 0x2000_0000) != 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_common_entry_branches(
|
||||||
|
stub: &[u8],
|
||||||
|
ep: u32,
|
||||||
|
secs: &[Section],
|
||||||
|
report: &mut IntegrityReport,
|
||||||
|
) {
|
||||||
|
if stub.len() < 18
|
||||||
|
|| stub[0..3] != [0x48, 0x83, 0xEC]
|
||||||
|
|| stub[4] != 0xE8
|
||||||
|
|| stub[9..12] != [0x48, 0x83, 0xC4]
|
||||||
|
|| stub[12] != stub[3]
|
||||||
|
|| stub[13] != 0xE9
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (name, rel_off, instruction_len) in [("call", 5usize, 9i64), ("jump", 14usize, 18i64)] {
|
||||||
|
let rel = i32::from_le_bytes([
|
||||||
|
stub[rel_off],
|
||||||
|
stub[rel_off + 1],
|
||||||
|
stub[rel_off + 2],
|
||||||
|
stub[rel_off + 3],
|
||||||
|
]) as i64;
|
||||||
|
let target = i64::from(ep) + instruction_len + rel;
|
||||||
|
let valid = u32::try_from(target)
|
||||||
|
.ok()
|
||||||
|
.is_some_and(|rva| is_executable_rva(secs, rva));
|
||||||
|
if !valid {
|
||||||
|
report.issues.push(format!(
|
||||||
|
"entry point {name} target 0x{target:X} is outside executable sections (DD8 selection is likely wrong)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Inspect an unpacked PE image and report any defect that would make the OS
|
/// Inspect an unpacked PE image and report any defect that would make the OS
|
||||||
/// loader fault at runtime. `out` is the bytes the unpacker produced.
|
/// loader fault at runtime. `out` is the bytes the unpacker produced.
|
||||||
pub fn check(out: &[u8]) -> IntegrityReport {
|
pub fn check(out: &[u8]) -> IntegrityReport {
|
||||||
@@ -253,6 +296,9 @@ pub fn check(out: &[u8]) -> IntegrityReport {
|
|||||||
"entry point RVA 0x{ep:X} is not in an executable section"
|
"entry point RVA 0x{ep:X} is not in an executable section"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if let Some(entry_stub) = out.get(off as usize..off as usize + 18) {
|
||||||
|
check_common_entry_branches(entry_stub, ep, &secs, &mut r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,3 +409,40 @@ fn looks_like_dll_name(d: &[u8], off: u32) -> bool {
|
|||||||
}
|
}
|
||||||
d[start..end].iter().all(|&b| (0x20..0x7F).contains(&b))
|
d[start..end].iter().all(|&b| (0x20..0x7F).contains(&b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn executable_text() -> Vec<Section> {
|
||||||
|
vec![Section {
|
||||||
|
va: 0x1000,
|
||||||
|
vsize: 0x4000,
|
||||||
|
raw_ptr: 0x1000,
|
||||||
|
raw_size: 0x4000,
|
||||||
|
chars: 0x6000_0020,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn common_entry_stub_rejects_out_of_image_branches() {
|
||||||
|
let stub = [
|
||||||
|
0x48, 0x83, 0xEC, 0x28, 0xE8, 0x5B, 0x02, 0x41, 0x00, 0x48, 0x83, 0xC4, 0x28, 0xE9,
|
||||||
|
0x7A, 0xFE, 0x54, 0xFF,
|
||||||
|
];
|
||||||
|
let mut report = IntegrityReport::default();
|
||||||
|
check_common_entry_branches(&stub, 0x1264, &executable_text(), &mut report);
|
||||||
|
assert_eq!(report.issues.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn common_entry_stub_accepts_executable_branches() {
|
||||||
|
let stub = [
|
||||||
|
0x48, 0x83, 0xEC, 0x28, 0xE8, 0x5B, 0x02, 0x00, 0x00, 0x48, 0x83, 0xC4, 0x28, 0xE9,
|
||||||
|
0x7A, 0xFE, 0xFF, 0xFF,
|
||||||
|
];
|
||||||
|
let mut report = IntegrityReport::default();
|
||||||
|
check_common_entry_branches(&stub, 0x1264, &executable_text(), &mut report);
|
||||||
|
assert!(report.ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
//! Validation-driven selection for per-page text transforms.
|
//! Validation-driven selection for per-page text transforms.
|
||||||
|
|
||||||
|
use super::discovery::trial_decrypt5_u32;
|
||||||
|
|
||||||
/// PE32 `.text` dd8 key-formula selection with a skip decision. The packer keys
|
/// PE32 `.text` dd8 key-formula selection with a skip decision. The packer keys
|
||||||
/// the per-page XOR either with `page+1` or `0x8000*(page+1)`; the formula is
|
/// the per-page XOR either with `page+1` or `0x8000*(page+1)`; the formula is
|
||||||
/// not recorded. Replays the dd8 page pass on a scratch copy of sample pages
|
/// not recorded. Replays the dd8 page pass on a scratch copy of sample pages
|
||||||
@@ -118,26 +120,29 @@ pub fn select_dd8_formula_pe32(data: &[u8], text_off: u32, text_size: u32) -> Op
|
|||||||
//
|
//
|
||||||
// The packer scrambles ~1 byte per 16-byte block of .text via decrypt_data8,
|
// The packer scrambles ~1 byte per 16-byte block of .text via decrypt_data8,
|
||||||
// keyed by `page_idx << shift` (absolute page index = text_va >> 12). Observed
|
// keyed by `page_idx << shift` (absolute page index = text_va >> 12). Observed
|
||||||
// shifts are 0 and 15. The shift is NOT stored in any header/config field:
|
// shifts are 0 and 15. The shift is NOT stored in any header/config field, so
|
||||||
// two otherwise-unrelated builds can carry byte-identical config-version stamps
|
// the decision must be validated against the resulting .text content.
|
||||||
// (0x40327253) yet require different shifts, so the only reliable discriminator
|
|
||||||
// is the .text content itself.
|
|
||||||
//
|
//
|
||||||
// Detection scoring formula: for each candidate shift, replay decrypt_data8
|
// A recognised CRT entry stub is the strongest oracle: decode skip/0/15 and
|
||||||
// across a few sample pages (25/50/75% of .text) and count how many of the 255
|
// require both of its direct rel32 branches to land in executable .text. This
|
||||||
// mutated positions become 0xCC — the MSVC int3 padding byte. The correct shift
|
// includes the call/jump displacement bytes themselves; an older entry oracle
|
||||||
// hits int3 pads disproportionately often (~3-10x the baseline), so the
|
// wildcarded those bytes and could accept a stub whose opcodes looked right but
|
||||||
// highest-scoring shift wins. If neither shift clears 2x the baseline, .text
|
// whose branch targets were outside the image.
|
||||||
// is already plaintext → skip (return 99).
|
|
||||||
//
|
//
|
||||||
// This replaces an earlier entry-stub oracle that matched the 14 fixed CRT-stub
|
// Other entry shapes fall back to padding statistics: replay each shift across
|
||||||
// bytes at the AEP. That oracle false-positived on a newer EXE-64 build: dd8
|
// sample pages and count positions restored to the MSVC int3 padding byte. A
|
||||||
// corrupted only the call rel32 (bytes 5-8, the wildcard region), so the stub
|
// clear gain selects the shift; otherwise .text is treated as already plain.
|
||||||
// matched under BOTH shifts and the selector defaulted to 0 when the truth was
|
|
||||||
// 15. The 0xCC statistic samples hundreds of positions per page and is not
|
|
||||||
// fooled by a stub whose fixed bytes happen to survive.
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
pub fn select_dd8_shift(data: &[u8], text_va: u32, text_size: u32, _info3: u32) -> u32 {
|
pub fn select_dd8_shift(data: &[u8], text_va: u32, text_size: u32, info3: u32) -> u32 {
|
||||||
|
if let Some((shift, scores)) = select_dd8_by_entry_stub(data, text_va, text_size, info3) {
|
||||||
|
if std::env::var("SEL_DIAG").is_ok() {
|
||||||
|
eprintln!(
|
||||||
|
"SEL dd8 entry best_shift={} none={} s0={} s15={}",
|
||||||
|
shift, scores[0], scores[1], scores[2]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return shift;
|
||||||
|
}
|
||||||
if text_size < 0x1000 {
|
if text_size < 0x1000 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -192,6 +197,123 @@ pub fn select_dd8_shift(data: &[u8], text_va: u32, text_size: u32, _info3: u32)
|
|||||||
best_shift
|
best_shift
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Select DD8 from the common CRT entry stub when its direct call and jump
|
||||||
|
/// provide a stronger oracle than sparse padding statistics. The candidate is
|
||||||
|
/// accepted only when it is the sole one whose two branch targets stay inside
|
||||||
|
/// `.text`; unrecognised entry code falls through to the padding selector.
|
||||||
|
fn select_dd8_by_entry_stub(
|
||||||
|
data: &[u8],
|
||||||
|
text_va: u32,
|
||||||
|
text_size: u32,
|
||||||
|
info3: u32,
|
||||||
|
) -> Option<(u32, [u8; 3])> {
|
||||||
|
for entry in entry_candidates(data, text_va, text_size, info3) {
|
||||||
|
let [Some(none), Some(s0), Some(s15)] = [None, Some(0), Some(15)]
|
||||||
|
.map(|shift| entry_stub_branch_score(data, text_va, text_size, entry, shift))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let scores = [none, s0, s15];
|
||||||
|
let best = scores.iter().copied().max()?;
|
||||||
|
if best == 2 && scores.iter().filter(|&&score| score == best).count() == 1 {
|
||||||
|
let index = scores.iter().position(|&score| score == best)?;
|
||||||
|
return Some(([99, 0, 15][index], scores));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_candidates(data: &[u8], text_va: u32, text_size: u32, info3: u32) -> Vec<u32> {
|
||||||
|
let text_end = text_va.saturating_add(text_size);
|
||||||
|
let mut entries = Vec::with_capacity(3);
|
||||||
|
if let Some(pe) = read_u32(data, 0x3C)
|
||||||
|
&& let Some(entry) = pe.checked_add(40).and_then(|offset| read_u32(data, offset))
|
||||||
|
&& (text_va..text_end).contains(&entry)
|
||||||
|
{
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
for metadata_off in [32u32, 64] {
|
||||||
|
let Some(end) = info3
|
||||||
|
.checked_add(metadata_off)
|
||||||
|
.and_then(|offset| offset.checked_add(8))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if end as usize > data.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let entry = trial_decrypt5_u32(data, info3 + metadata_off);
|
||||||
|
let image_base = trial_decrypt5_u32(data, info3 + metadata_off + 4);
|
||||||
|
if image_base == info3 && (text_va..text_end).contains(&entry) && !entries.contains(&entry)
|
||||||
|
{
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_stub_branch_score(
|
||||||
|
data: &[u8],
|
||||||
|
text_va: u32,
|
||||||
|
text_size: u32,
|
||||||
|
entry: u32,
|
||||||
|
shift: Option<u32>,
|
||||||
|
) -> Option<u8> {
|
||||||
|
let text_end = text_va.checked_add(text_size)?;
|
||||||
|
if entry < text_va || entry.checked_add(18)? > text_end {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stub = [0u8; 18];
|
||||||
|
for (offset, byte) in stub.iter_mut().enumerate() {
|
||||||
|
*byte = dd8_candidate_byte(data, entry + offset as u32, shift)?;
|
||||||
|
}
|
||||||
|
if stub[0..3] != [0x48, 0x83, 0xEC]
|
||||||
|
|| stub[4] != 0xE8
|
||||||
|
|| stub[9..12] != [0x48, 0x83, 0xC4]
|
||||||
|
|| stub[12] != stub[3]
|
||||||
|
|| stub[13] != 0xE9
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let call_rel = i32::from_le_bytes(stub[5..9].try_into().ok()?) as i64;
|
||||||
|
let jump_rel = i32::from_le_bytes(stub[14..18].try_into().ok()?) as i64;
|
||||||
|
let call_target = i64::from(entry) + 9 + call_rel;
|
||||||
|
let jump_target = i64::from(entry) + 18 + jump_rel;
|
||||||
|
let in_text = |target: i64| target >= i64::from(text_va) && target < i64::from(text_end);
|
||||||
|
Some(u8::from(in_text(call_target)) + u8::from(in_text(jump_target)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dd8_candidate_byte(data: &[u8], rva: u32, shift: Option<u32>) -> Option<u8> {
|
||||||
|
let mut byte = *data.get(rva as usize)?;
|
||||||
|
let Some(shift) = shift else {
|
||||||
|
return Some(byte);
|
||||||
|
};
|
||||||
|
let page = rva >> 12;
|
||||||
|
let block = (rva & 0xFFF) >> 4;
|
||||||
|
let mut key = page << shift;
|
||||||
|
for index in 0..=block {
|
||||||
|
let mixed = key.rotate_right(15).wrapping_add(index);
|
||||||
|
key = mixed.wrapping_add(index);
|
||||||
|
if index != 0 {
|
||||||
|
let target = (page << 12)
|
||||||
|
.wrapping_add(index << 4)
|
||||||
|
.wrapping_add(mixed & 0xF);
|
||||||
|
if target == rva {
|
||||||
|
byte ^= key as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(data: &[u8], offset: u32) -> Option<u32> {
|
||||||
|
let start = offset as usize;
|
||||||
|
let bytes = data.get(start..start.checked_add(4)?)?;
|
||||||
|
Some(u32::from_le_bytes(bytes.try_into().ok()?))
|
||||||
|
}
|
||||||
|
|
||||||
// Baseline: count int3 pads already present at the first byte of each 16-byte
|
// Baseline: count int3 pads already present at the first byte of each 16-byte
|
||||||
// block, i.e. the positions dd8 would target if its in-block offset were 0.
|
// block, i.e. the positions dd8 would target if its in-block offset were 0.
|
||||||
fn score_dd8_baseline(data: &[u8], text_off: usize, sample_pages: &[u32]) -> u32 {
|
fn score_dd8_baseline(data: &[u8], text_off: usize, sample_pages: &[u32]) -> u32 {
|
||||||
@@ -250,6 +372,42 @@ fn score_dd8_shift(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn entry_stub_fixture() -> Vec<u8> {
|
||||||
|
let mut data = vec![0u8; 0x5000];
|
||||||
|
data[0x3C..0x40].copy_from_slice(&0x100u32.to_le_bytes());
|
||||||
|
data[0x128..0x12C].copy_from_slice(&0x1264u32.to_le_bytes());
|
||||||
|
data[0x1264..0x1276].copy_from_slice(&[
|
||||||
|
0x48, 0x83, 0xEC, 0x28, 0xE8, 0x5B, 0x02, 0x00, 0x00, 0x48, 0x83, 0xC4, 0x28, 0xE9,
|
||||||
|
0x7A, 0xFE, 0xFF, 0xFF,
|
||||||
|
]);
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_dd8_page(data: &mut [u8], page_rva: u32, shift: u32) {
|
||||||
|
let mut key = (page_rva >> 12) << shift;
|
||||||
|
for index in 0..256u32 {
|
||||||
|
let mixed = key.rotate_right(15).wrapping_add(index);
|
||||||
|
key = mixed.wrapping_add(index);
|
||||||
|
if index == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let target = page_rva.wrapping_add(index << 4).wrapping_add(mixed & 0xF) as usize;
|
||||||
|
data[target] ^= key as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn entry_stub_selects_plaintext_and_both_dd8_shifts() {
|
||||||
|
let plain = entry_stub_fixture();
|
||||||
|
assert_eq!(select_dd8_shift(&plain, 0x1000, 0x4000, 0), 99);
|
||||||
|
|
||||||
|
for expected in [0u32, 15] {
|
||||||
|
let mut encrypted = plain.clone();
|
||||||
|
apply_dd8_page(&mut encrypted, 0x1000, expected);
|
||||||
|
assert_eq!(select_dd8_shift(&encrypted, 0x1000, 0x4000, 0), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Seed the first `count` dd8-targeted positions of each sampled page with
|
/// Seed the first `count` dd8-targeted positions of each sampled page with
|
||||||
/// the byte that decodes to `0xCC` under the `page+1` formula — i.e. an
|
/// the byte that decodes to `0xCC` under the `page+1` formula — i.e. an
|
||||||
/// encrypted `.text` whose plaintext is int3 padding. Positions whose key
|
/// encrypted `.text` whose plaintext is int3 padding. Positions whose key
|
||||||
|
|||||||
Reference in New Issue
Block a user