Byte-at-a-time ECB decryption
This commit is contained in:
parent
54a4cef76e
commit
1625f9561c
4 changed files with 184 additions and 1 deletions
57
src/aes.zig
57
src/aes.zig
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
src/main.zig
16
src/main.zig
|
@ -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});
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue