【问题标题】:First Byte's Bit Off-By-One after Steganography隐写术后第一个字节的位
【发布时间】:2016-02-19 03:55:37
【问题描述】:

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

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

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

  • 为什么对于 maxBits 的某些值,是 生成的解码消息总是 1?
  • 为什么不同的输入对maxBits 有不同的值? 工作正常,而其他工作不正常?
  • 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;
    }

【问题讨论】:

    标签: java binary bit-manipulation byte off-by-one


    【解决方案1】:

    您递增index 的逻辑有两个缺陷,即覆盖了第一个字母的第一位。显然,当覆盖位与第一位不同时,就表示了该错误。

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

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

    更具体地说,如果您嵌入0011,就可以了。但是01 将被读取为1110 将被读取为00,即第二位是什么值。如果第一个字母的 ascii 代码小于或等于 63 (00xxxxxx),或大于或等于 192 (11xxxxxx),它会正常显示。例如:

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

    第二个问题与x % this.maxBits == 0 部分有关。考虑我们每个字节嵌入 3 位的情况。在第 3 位之后,当我们达到条件时,我们仍然有 x = 2,所以模运算将返回 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,或
    • 您在嵌入实际数据(标头)之前添加了一些位,这将告诉您如何提取数据,例如,要读取多少字节以及每个字节要读取多少位。

    【讨论】:

    • 感谢您的解释和解决方案。更改索引值增量主义者的情况在逻辑上解决了这个问题。对于解码数据数组的潜在问题,我已经为实际字节数组中的数据头和消息序列的结尾设置了一个方法(为了使这个问题更简洁,我没有包含它)。
    • @MrPublic 我忘了补充 x++; if (x % this.maxBits == 0) 相当于 if (++x % this.maxBits == 0)
    【解决方案2】:

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

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

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

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

    【讨论】:

      猜你喜欢
      • 2011-09-27
      • 1970-01-01
      • 2012-08-28
      • 2023-04-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-11
      相关资源
      最近更新 更多