Byte-at-a-time ECB decryption

This commit is contained in:
Armin Friedl 2025-02-15 19:13:31 +01:00
parent 54a4cef76e
commit 1625f9561c
4 changed files with 184 additions and 1 deletions

View file

@ -3,6 +3,7 @@ const nettle = @cImport({
@cInclude("nettle/aes.h"); @cInclude("nettle/aes.h");
}); });
const xor = @import("xor.zig"); const xor = @import("xor.zig");
const base64 = @import("base64.zig");
pub const Mode = enum { ECB, CBC }; pub const Mode = enum { ECB, CBC };
@ -195,6 +196,31 @@ pub const ECB_CBC_Oracle = struct {
} }
}; };
pub const ByteATime_ECB_Oracle = struct {
const key = [_]u8{ 0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11, 0x12 };
const secret =
"Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg" ++
"aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq" ++
"dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg" ++
"YnkK";
pub fn oracleFn(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
const secret_decoded = try base64.decode(allocator, secret);
defer allocator.free(secret_decoded);
var clear = try allocator.alloc(u8, (input.len + secret_decoded.len));
defer allocator.free(clear);
std.mem.copyForwards(u8, clear[0..input.len], input);
std.mem.copyForwards(u8, clear[input.len..], secret_decoded);
var cipher = AES.init_ecb(allocator, &key);
defer cipher.deinit();
return try cipher.encrypt(allocator, clear);
}
};
test "ecb" { test "ecb" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
@ -232,3 +258,34 @@ test "cbc" {
try std.testing.expectEqualStrings("YELLOW SUBMARINEYELLOW SUBMARINEYELLOW SUBMARINE", dec); try std.testing.expectEqualStrings("YELLOW SUBMARINEYELLOW SUBMARINEYELLOW SUBMARINE", dec);
} }
} }
test "bat_ecb_oracle" {
const allocator = std.testing.allocator;
const data = [_]u8{ 0x50, 0x68, 0x12, 0xA4, 0x5F, 0x08, 0xC8, 0x89, 0xB9, 0x7F, 0x59, 0x80, 0x03, 0x8B, 0x83, 0x59 };
const key = [_]u8{ 0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11, 0x12 };
const secret =
"Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg" ++
"aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq" ++
"dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg" ++
"YnkK";
const secret_decoded = try base64.decode(allocator, secret);
defer allocator.free(secret_decoded);
const clear = try allocator.alloc(u8, (16 + secret_decoded.len));
defer allocator.free(clear);
std.mem.copyForwards(u8, clear[0..16], &data);
std.mem.copyForwards(u8, clear[16..], secret_decoded);
const bat = ByteATime_ECB_Oracle;
const result = try bat.oracleFn(allocator, &data);
defer allocator.free(result);
var aes = AES.init(allocator, &key);
defer aes.deinit();
const res = try aes.encrypt(allocator, clear);
defer allocator.free(res);
try std.testing.expectEqualSlices(u8, res, result);
}

View file

@ -55,6 +55,113 @@ pub fn detect_ecb_cbc(allocator: std.mem.Allocator, oracle: anytype) !Mode {
return if (ecb) Mode.ECB else Mode.CBC; return if (ecb) Mode.ECB else Mode.CBC;
} }
pub fn crack_bat_ecb(allocator: std.mem.Allocator, oracle: anytype) ![]u8 {
// * Detect ECB and block size *
var block_size: usize = 0;
// aes is always 128 bit, while Rijndael may be 128-256 bits. In
// any case, a 512 bit buffer should be plenty enough.
var probe = [_]u8{'A'} ** 64;
for (1..probe.len) |i| {
const result = try oracle.oracleFn(allocator, probe[0..i]);
defer allocator.free(result);
// We cheat a little here. As soon as we detect an 16-byte
// repeated block we take this as if we found twice the block
// size (we need a minimum of 2 identical blocks to detect
// ECB) and abort.
//
// This should even work with >16-byte ECB since even then we
// only find a 16-byte repeated block if we have the right
// block size and finding a 16-byte repeated block with the
// wrong block size is unlikely. This holds true only if the
// oracle does not have padding or other sheenigans.
//
// At the end of the day the real reason is that that's what
// `detect` already does and frankly it's too boring to
// implement this in a more exact way.
const ecb = try detect(allocator, result);
if (ecb) {
block_size = i / 2;
std.log.info("Detected block size {d}", .{block_size});
break;
}
}
if (block_size == 0) return error.UnknownBlocksize;
// * Crack secret *
// get the size of the secret
const secret_encrypted = try oracle.oracleFn(allocator, probe[0..block_size]);
defer allocator.free(secret_encrypted);
const secret_size = secret_encrypted.len - block_size;
std.log.info("Secret length: {d}", .{secret_size});
// allocate a large enough crack probe to crack the whole secret,
// an integer multiple of block size
const crack_size = ((secret_size / block_size) + 1) * block_size;
var crack_probe = try allocator.alloc(u8, crack_size);
defer allocator.free(crack_probe);
@memset(crack_probe, 'A');
// allocate a buffer holding the cracked clear text
var clear = try allocator.alloc(u8, secret_size);
@memset(clear, 'A');
for (0..secret_size) |i| {
// let (i+1) characters at the end fill with secret text, at
// this point we cracked the previous i characters already
const secret_byte = try oracle.oracleFn(allocator, crack_probe[0..(crack_probe.len - (i + 1))]);
defer allocator.free(secret_byte);
// This is difficult to parse. What we do is essentially fill
// the probe with the already cracked bytes at the end + one
// character left at the end which is the one we currently
// crack. That is, we build a probe AAAAAbbbc where A are the
// filler bytes, b is the bytes cracked to far (from `clear`
// buffer) and c is the next byte we are about to crack.
std.mem.copyForwards(u8, crack_probe[(crack_probe.len - (i + 1))..crack_probe.len], clear[0..(i + 1)]);
crk: for (0..256) |j| {
// we replace the last character of the probe with our
// current guess
const byte: u8 = @intCast(j);
crack_probe[crack_probe.len - 1] = byte;
const crack_byte = try oracle.oracleFn(allocator, crack_probe);
defer allocator.free(crack_byte);
// If we guessed right, we compare
//
// secret_byte=aes(AAAAbbbc)
// where `bbbc` is coming from the secret key
// and `AAAA` are fillers from our probe
// crack_byte=aes(AAAAbbbc)
// where `bbb` is the already cracked bytes in `clear`
// and `c` is the byte of our current guess
// and `AAAA` are fillers from our probe
//
// i.e. they are the same if our current guess is equal to
// the cleartext `c` from the secret.
//
// len(`AAAAbbbc`) is of course exactly aligned with the
// block size of the cipher, this is just an example.
const eql = std.mem.eql(u8, secret_byte[0..(crack_probe.len)], crack_byte[0..crack_probe.len]);
if (eql) {
std.log.info("Cracked byte 0x{x:0<2} at pos {d}", .{ byte, i });
clear[i] = byte;
break :crk;
}
}
}
return clear;
}
test "detect_no" { test "detect_no" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;

View file

@ -119,7 +119,10 @@ fn to_bin(char: u8) !u8 {
'+' => 62, '+' => 62,
'/' => 63, '/' => 63,
'=' => 0xff, '=' => 0xff,
else => error.InvalidBase64Character, else => blk: {
std.log.err("Invalid char 0x{x:0>2}", .{char});
break :blk error.InvalidBase64Character;
},
}; };
} }

View file

@ -7,6 +7,10 @@ const aes = @import("aes.zig");
const aes_crack = @import("aes_crack.zig"); const aes_crack = @import("aes_crack.zig");
const padding = @import("padding.zig"); const padding = @import("padding.zig");
pub const std_options: std.Options = .{
.log_level = .debug,
};
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator(); const allocator = gpa.allocator();
@ -59,6 +63,10 @@ pub fn main() !void {
if (std.mem.eql(u8, args[1], "s2c11")) { if (std.mem.eql(u8, args[1], "s2c11")) {
try s2c11(allocator, stdout); try s2c11(allocator, stdout);
} }
if (std.mem.eql(u8, args[1], "s2c12")) {
try s2c12(allocator, stdout);
}
} }
fn s1c1(allocator: std.mem.Allocator, stdout: anytype) !void { fn s1c1(allocator: std.mem.Allocator, stdout: anytype) !void {
@ -339,3 +347,11 @@ fn s2c11(allocator: std.mem.Allocator, stdout: anytype) !void {
try stdout.print("Mode: {}, Detected: {}\n", .{ mode, result }); try stdout.print("Mode: {}, Detected: {}\n", .{ mode, result });
} }
} }
fn s2c12(allocator: std.mem.Allocator, stdout: anytype) !void {
const oracle = aes.ByteATime_ECB_Oracle;
const result = try aes_crack.crack_bat_ecb(allocator, oracle);
defer allocator.free(result);
try stdout.print("{s}", .{result});
}