Digital_forensic/CTF

[Dreamhack] BMP Recovery Write-up

neck392 2024. 12. 25. 02:30

1. BMP Recovery

<문제 설명>
<문제 파일>
<또 백만년만에 쓰는 롸업이다>

2. Explanation

디지털 포렌식을 공부하며 처음하였던 것이 각 파일 구조를 기반으로 바이너리 데이터를 분석하는 것이었기에 이 문제를 보고 자신있게 시작했던 기억이 난다. 아무튼 우선 chal.py를 열어서 코드를 분석한다.

<chal.py>

 

flag.bmp 파일을 열어서 data에 해당 파일의 바이너리 데이터를 저장하고

  • 1) data[:0x1C] = b'\x00' * 0x1C
    • 첫 번째 바이트부터 0x1C까지
    • 즉, 0~28바이트까지 '00'으로 저장(1C이므로 길이는 28바이트)
  • 2) data[0x22:0x36] = b'\x00' * 0x14
    • 0x22번째 바이트 부터 0x36번째 바이트까지 0x14길이의 데이터를 '00'으로 저장
    • 34번째 바이트부터 54번째 바이트까지 20바이트를 '00'으로 저장

이후에 다시 수정한 파일을 flag.bmp.broken으로 저장한다.

<flag.bmp.broken 파일 확인>

16진수 한 개의 값은 2개의 문자로 이루어져 있으며 문자 1개의 크기는 4bit이므로 16진수 한 개의 값은 4bit+4bit=1byte가 된다.

따라서 flag.bmp.broken 파일을 확인해보면 위와 같이 빨간 박스 안의 28바이트가 00으로 수정되었으며 파란 박스 안의 20바이트가 00으로 수정되었음을 알 수 있다.

우선 BitMapFile Header에 정의된 구조체에 의거하여 14바이트의 File Header 데이터를 수정해본다.

 

<File Header 수정>

우선 처음 2바이트는 .bmp 파일의 파일 시그니처인 '42 4D'를 입력한다[1].

<속성에서 파일 크기 확인>

 

다음 4바이트는 파일의 크기를 입력한다. 파일 속성에서 파일 크기 14,309,622 바이트를 확인할 수 있으며 16진수로 변환하면 "00 DA 58 F6"이 되지만 컴퓨터는 데이터를 Little Endian 방식으로 처리하기 때문에 이에 맞게 "F6 58 DA 00"를 입력해준다[2].

다음 2바이트 + 2바이트 총 4바이트는 는 예약된 영역이므로 항상 0이다. 따라서 "00"을 입력한다[3][4].

그 다음 4바이트는 픽셀 데이터의 시작 위치(오프셋)로 헤더 영역들의 length 합을 입력하며 file header의 14바이트와 info header의 40바이트를 더하여 54바이트, 16진수로 "36"을 입력한다[5]. 

 

파일명도 flag.bmp로 저장하고 작업을 편리하게 하기 위하여 나머지는 010 editer를 사용하여 분석 및 수정한다.

<010 editor 사용>

file header는 hxd editor로 수정하였기에 info(info header)를 살펴본다.

 

<가로(Width)와 세로(Height)를 제외한 나머지 값 입력>

info header의 크기는 40바이트이므로 biSize에 40을 입력한다.

biPlanes의 지원되는 값은 1로 고정되므로 1을 입력한다.

biSizeImage는 이미지 데이터의 크기이다. 전체 데이터의 크기가 14309622이므로 header 크기인 54바이트를 제외하면 14309568바이트가 된다. 밑의 나머지 데이터는 일반적으로 0이다.

<입력한 데이터>

 

Width와 Height값을 구하기 위하여 경우의 수를 생각해본다.

PNG 파일은 CRC 값을 이용하여 width와 height 값을 구할 수 있지만 BMP 파일에서는 불가능하다.

이미지 데이터의 크기가 14309568바이트(14309622 - 54)이며 픽셀에 할당된 바이트 수가 24bit(biBitCount)이므로 3바이트이다. 따라서 픽셀 데이터의 크기는 14309568 / 3을 수행하면 4,769,856바이트가 된다.

따라서 Width * Height = 4,769,856이 되는 경우의 수를 코드를 짜서 살펴본다.

def findDimensions(totalPixels):
    dimensions = set()
    for width in range(1, int(totalPixels**0.5) + 1):
        if totalPixels % width == 0:
            height = totalPixels // width
            dimensions.add((width, height))
            dimensions.add((height, width))
    return sorted(dimensions)

totalPixels = 4769856
dimensions = findDimensions(totalPixels)

for width, height in dimensions:
    print(f"Width: {width}, Height: {height}")

<모든 경우의 수 출력>

위와 같이 Width * Height = 4,769,856이 되는 모든 경우의 수를 출력해보니 경우의 수가 많으므로 직접 바이너리 데이터를 수정해가며 찾는 것 보다는 각 경우의 수에 해당하는 Width와 Height 값들을 수정 및 각각의 bmp 파일로 저장하여 명확히 복구된 이미지 파일을 찾아본다.

 

def findDimensions(totalPixels):
    dimensions = set()
    for width in range(1, int(totalPixels**0.5) + 1):
        if totalPixels % width == 0:
            height = totalPixels // width
            dimensions.add((width, height))
            dimensions.add((height, width))
    return sorted(dimensions)

def convertLittleEndianBytes(value, length):
    return value.to_bytes(length, 'little')

def saveBmpFiles(originalBmpPath, totalPixels):
    dimensions = findDimensions(totalPixels)

    with open(originalBmpPath, 'rb') as bmpFile:
        originalData = bmpFile.read()

    for width, height in dimensions:
        modifiedData = bytearray(originalData)

        widthBytes = convertLittleEndianBytes(width, 4)
        heightBytes = convertLittleEndianBytes(height, 4)

        modifiedData[18:22] = widthBytes
        modifiedData[22:26] = heightBytes

        newFileName = f"flag_{width}x{height}.bmp"
        with open(newFileName, 'wb') as newBmpFile:
            newBmpFile.write(modifiedData)

originalBmpPath = 'flag.bmp'
totalPixels = 4769856
saveBmpFiles(originalBmpPath, totalPixels)

<코드 동작 결과>

위와 같이 코드를 동작하였을 때 정상적으로 Width와 Height 길이에 대한 경우의 수에 따른 bmp 파일들이 생성되며 여기서 이미지가 명확히 보이는 파일을 찾아보면

<flag 획득>

위와 같이 flag를 찾을 수 있다.

 

3. Reference

https://velog.io/@jaeyoonheo/bmp-reader

https://blog.naver.com/lunchtime82/100055581202