隐写术后第一个字节的位差一

First Byte's Bit Off-By-One after Steganography

目前正在研究隐写术项目,给定一条以字节为单位的消息和每个字节要修改的位数,将消息隐藏在任意字节数组中。

在结果消息的第一个解码字节中,该值的第一个(最左边)位设置为“1”而不是“0”。例如,当使用消息 "Foo".getBytes()maxBits = 1 时,结果是“Æoo”,而不是 "Foo"(0b01000110 变为 0b11000110)。消息 "Æoo".getBytes()maxBits = 1 结果是“Æoo”,这意味着据我所知该位没有翻转。

只有某些消息字节的 maxBits 的某些值会导致此错误,例如 "Foo"maxBits 等于 1、5 和 6 时遇到此问题,而 "Test"maxBits 等于 1、3 和 5 时遇到此问题。只有结果的第一个字符以其第一位设置结束,并且此问题仅发生在 this.maxBits 相关的指定值处到初始数据。

编码和解码方法:

public byte[] encodeMessage(byte[] data, byte[] message) {
    byte[] encoded = data;
    boolean[] messageBits = byteArrToBoolArr(message);
    int index = 0;
    for (int x = 0; x < messageBits.length; x++) {
        encoded[index] = messageBits[x] ? setBit(encoded[index], x % this.maxBits) : unsetBit(encoded[index], x % this.maxBits);
        if (x % this.maxBits == 0 && x != 0)
            index++;
    }
    return encoded;
}

public byte[] decodeMessage(byte[] data) {
    boolean[] messageBits = new boolean[data.length * this.maxBits];
    int index = 0;
    for (int x = 0; x < messageBits.length; x++) {
        messageBits[x] = getBit(data[index], x % this.maxBits);
        if (x % this.maxBits == 0 && x != 0)
            index++;
    }
    return boolArrToByteArr(messageBits);
}

取消设置、设置和获取方法:

public byte unsetBit(byte data, int pos) {
    return (byte) (data & ~((1 << pos)));
}

public byte setBit(byte data, int pos) {
    return (byte) (data | ((1 << pos)));
}

public boolean getBit(byte data, int pos) {
    return ((data >>> pos) & 0x01) == 1;
}

转换方法:

public boolean[] byteArrToBoolArr(byte[] b) {
    boolean bool[] = new boolean[b.length * 8];
    for (int x = 0; x < bool.length; x++) {
        bool[x] = false;
        if ((b[x / 8] & (1 << (7 - (x % 8)))) > 0)
            bool[x] = true;
    }
    return bool;
}

public byte[] boolArrToByteArr(boolean[] bool) {
    byte[] b = new byte[bool.length / 8];
    for (int x = 0; x < b.length; x++) {
        for (int y = 0; y < 8; y++) {
            if (bool[x * 8 + y]) {
                b[x] |= (128 >>> y);
            }
        }
    }
    return b;
}

示例代码和输出:

    test("Foo", 1);//Æoo
    test("Foo", 2);//Foo
    test("Foo", 3);//Foo
    test("Foo", 4);//Foo
    test("Foo", 5);//Æoo
    test("Foo", 6);//Æoo
    test("Foo", 7);//Foo
    test("Foo", 8);//Foo

    test("Test", 1);//Ôest
    test("Test", 2);//Test
    test("Test", 3);//Ôest
    test("Test", 4);//Test
    test("Test", 5);//Ôest
    test("Test", 6);//Test
    test("Test", 7);//Test
    test("Test", 8);//Test

    private static void test(String s, int x) {
        BinaryModifier bm = null;
        try {
            bm = new BinaryModifier(x);//Takes maxBits as constructor param
        } catch (BinaryException e) {
            e.printStackTrace();
        }
        System.out.println(new String(bm.decodeMessage(bm.encodeMessage(new byte[1024], s.getBytes()))));
        return;
    }

您可能会 运行 遇到的一件事是字符编码的性质。

当您调用 s.getBytes() 时,您使用 JVM 的默认编码将字符串转换为字节。然后修改字节并使用默认编码再次从修改后的字节创建一个新字符串。

所以问题是编码是什么以及它是如何工作的。例如,在某些情况下,编码可能只查看与字符相关的字节的低 7 位,那么您对最高位的设置不会对从修改后的字节创建的字符串产生任何影响。

如果您真的想知道您的代码是否正常工作,请通过直接检查编码和解码方法生成的 byte[] 来进行测试,而不是将修改后的字节转换为字符串并查看字符串。

你递增的逻辑 index 有两个缺陷,它们覆盖了第一个字母的第一位。显然,当覆盖位与第一位不同时,表示存在错误。

if (x % this.maxBits == 0 && x != 0)
    index++;

第一个问题与每个字节只嵌入一位有关,即 maxBits = 1。嵌入第一位并达到上述条件后,x 仍为 0,因为它将在循环结束时递增。此时您应该递增 index,但 x != 0 阻止您这样做。因此,第二位也将嵌入到第一个字节中,有效地覆盖了第一位。由于这个逻辑也存在于decode方法中,所以你从第一个字节开始读取前两位。

更具体地说,如果您嵌入 0011,就可以了。但是 01 将被读取为 11,而 10 将被读取为 00,即第二位为任何值。如果第一个字母的 ascii 码小于或等于 63(00xxxxxx),或大于或等于 192(11xxxxxx),它就会出来。例如:

# -> # : 00100011 (35) -> 00100011 (35)
F -> Æ : 01000110 (70) -> 11000110 (198)

第二个问题与 x % this.maxBits == 0 部分有关。考虑我们每个字节嵌入 3 位的情况。在第 3 位之后,当我们达到条件时,我们仍然有 x = 2,因此模运算将为 return false。在我们嵌入第 4 位之后,我们确实有 x = 3 并且我们可以继续下一个字节。但是,这个额外的第 4 位将写入第一个字节的第 0 个位置,因为 x % this.maxBits 将是 3 % 3。因此,我们再次覆盖了我们的第一部分。然而,在第一个周期之后,模运算将正确地写入每字节 3 位,因此我们消息的其余部分将不受影响。

考虑 "F" 的二进制,即 01000110。通过每个字节嵌入 N 位,我们有效地将以下组嵌入前几个字节。

1 bit  01 0 0 0 1 1 0
2 bits 010 00 11 0x
3 bits 0100 011 0xx
4 bits 01000 110x
5 bits 010001 10xxxx
6 bits 0100011 0xxxxx
7 bits 01000110
8 bits 01000110x

如你所见,对于5位和6位的分组,第一组的最后一位是1,这将覆盖我们最初的0位。对于所有其他情况,覆盖不会影响任何东西。请注意,对于 8 位,我们最终使用第二个字母的第一位。如果那恰好有大于或等于 128 的 ascii 码,它会再次覆盖最前面的 0 位。

要解决所有问题,请使用

for (int x = 0; x < messageBits.length; x++) {
    // code in the between
    if ((x + 1) % this.maxBits == 0)
        index++;
}

for (int x = 0; x < messageBits.length; ) {
    // code in the between
    x++;
    if (x % this.maxBits == 0)
        index++;
}

你的代码还有一个潜在的问题没有被表达出来。如果您的 data 数组的大小为 1024,但您只嵌入了 3 个字母,那么您只会影响前几个字节,具体取决于 maxBits 的值。但是,对于提取,您将数组的大小定义为 data.length * this.maxBits。所以你最终会从 data 数组的所有字节中读取位。这目前没有问题,因为您的数组由 0 填充,这些 0 被转换为空字符串。但是,如果您的数组有实际数字,您最终会读取超过嵌入数据点的大量垃圾。

有两种通用的方法可以解决这个问题。你要么

  • 在您的消息(标记)末尾附加一个唯一的位序列,以便在遇到该序列时终止提取,例如八个 0,或
  • 您在嵌入实际数据之前添加一些位 (header),这将告诉您如何提取数据,例如,要读取多少字节以及每个字节要读取多少位。