PIL

Introduction

A BMP image and C# bytecode file are provided. One of my teammates used a decompiler from JetBrains to recover the original C# program. See here. We have a main and 2 functions. GetNextPiDigit’s purpose is obvious, although we originally had some confusion over whether the one-million-digits.txt started with 3.14, 314, or 14.

Hide is where everything happens. Secret is converted into bytes, which are then converted into 8 bits appended into an array. The rest is pretty straightforward, although the (byte) (254U & (uint) bytes[index2]) part confused our team for a little while. It turns out that sets the least significant bit of bytes[index2] to 0. Then the next line adds a bit from the bitarray to bytes[index2], so what we have here is basically LSB steganography based on digits of pi.

Once that’s clear, we can write a program to recover the flag. This is mostly straightforward. The only issue is what the start of the pi digits file is, but you can try 3.14, 314, and 14 and see which one is right.

# my file with one million digits of pi is called pi-million since I think it's more descriptive
file = open("pi-million.txt", 'r')
# the million pi digits start with 14159, not 3
 
 
# taken from the source and converted into python
def get_next_pi_digit() -> int:
    num = ord(file.read(1))
    if num == 10:
        num = ord(file.read(1))
    return num - 48
 
 
# taken from the source and converted into python
def hide(src_path: str, dst_path: str, secret: str):
    bit_array = []
    for b in secret.encode():
        bit_array.extend([(b >> i) % 2 for i in range(8)])
 
    with open(src_path, 'rb') as f:
        src_bytes = f.read()
 
    src_bytes = bytearray(src_bytes)
    num1 = int(src_bytes[14]) + 14
    for idx1 in range(len(bit_array)):
        idx2 = num1 + get_next_pi_digit()
        num2 = int(254 & int(src_bytes[idx2]))
        src_bytes[idx2] = num2 + bit_array[idx1]
        num1 += 10
 
    with open(dst_path, 'wb') as outfile:
        outfile.write(src_bytes)
 
 
# python version of Convert.ToByte
def bits_to_int(bits: list) -> int:
    ret = 0
    for bit in reversed(bits):
        ret *= 2
        ret += bit
    return ret
 
 
# reversing Hide
def recover(out_path):
    # read bytes from result image
    with open(out_path, 'rb') as f:
        out_bytes = f.read()
    out_bytes = bytearray(out_bytes)
    # print(out_bytes)
 
    res_bits = []  # bits that will hold our flag
    num1 = int(out_bytes[14]) + 14
 
    # copy the loop from hide, but read the lsb from out_bytes[idx2] instead
    for idx1 in range(int(1e3)):  # 1e3 was a rather arbitrary number - I started with 1e4 but it took too long so here we are
        idx2 = num1 + get_next_pi_digit()
        # print(idx1, idx2)
        res_bits.append(out_bytes[idx2] % 2)
        num1 += 10
 
    # print(res_bits)
    # just to be safe, in case some other bits came in at the start, shift the bits so each combination will be printed
    for sh in range(8):
        nums = [bits_to_int(res_bits[sh+i:sh+i+8]) for i in range(0, len(res_bits), 8)]
        secret = ''.join([chr(num) for num in nums])
        # check if the flag is in the recovered secret
        if "hex" in secret.lower() or b"hex" in secret.encode("UTF-8").lower():
            print(secret.encode("UTF-8"))  # make sure to encode in UTF-8, since that's how the bitArray was created
 
 
def main():
    # hide("original.bmp", "result.bmp", "<CENSORED>")
    recover("result.bmp")
 
 
if __name__ == "__main__":
    main()
 

Flag: hexCTF{l00k_wh0_l3arned_t0_sp34k_byt3c0de}