From 1625f9561ca43233fd47e8a04fe0b0c98ade7a8c Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sat, 15 Feb 2025 19:13:31 +0100 Subject: [PATCH] Byte-at-a-time ECB decryption --- src/aes.zig | 57 ++++++++++++++++++++++++ src/aes_crack.zig | 107 ++++++++++++++++++++++++++++++++++++++++++++++ src/base64.zig | 5 ++- src/main.zig | 16 +++++++ 4 files changed, 184 insertions(+), 1 deletion(-) diff --git a/src/aes.zig b/src/aes.zig index 4d20b5b..e7d4bd0 100644 --- a/src/aes.zig +++ b/src/aes.zig @@ -3,6 +3,7 @@ const nettle = @cImport({ @cInclude("nettle/aes.h"); }); const xor = @import("xor.zig"); +const base64 = @import("base64.zig"); 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" { const allocator = std.testing.allocator; @@ -232,3 +258,34 @@ test "cbc" { 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); +} diff --git a/src/aes_crack.zig b/src/aes_crack.zig index 4592589..b09a248 100644 --- a/src/aes_crack.zig +++ b/src/aes_crack.zig @@ -55,6 +55,113 @@ pub fn detect_ecb_cbc(allocator: std.mem.Allocator, oracle: anytype) !Mode { 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" { const allocator = std.testing.allocator; diff --git a/src/base64.zig b/src/base64.zig index 42727dc..dae5559 100644 --- a/src/base64.zig +++ b/src/base64.zig @@ -119,7 +119,10 @@ fn to_bin(char: u8) !u8 { '+' => 62, '/' => 63, '=' => 0xff, - else => error.InvalidBase64Character, + else => blk: { + std.log.err("Invalid char 0x{x:0>2}", .{char}); + break :blk error.InvalidBase64Character; + }, }; } diff --git a/src/main.zig b/src/main.zig index a998969..e94b19f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,6 +7,10 @@ const aes = @import("aes.zig"); const aes_crack = @import("aes_crack.zig"); const padding = @import("padding.zig"); +pub const std_options: std.Options = .{ + .log_level = .debug, +}; + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); @@ -59,6 +63,10 @@ pub fn main() !void { if (std.mem.eql(u8, args[1], "s2c11")) { 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 { @@ -339,3 +347,11 @@ fn s2c11(allocator: std.mem.Allocator, stdout: anytype) !void { 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}); +}