댓글 검색 목록

[Nodejs] 비밀 요원이 된 기분 : 스테가노그래피로 이미지에 숨겨진 메시지 ?️?️‍♀️

페이지 정보

작성자 운영자 작성일 21-06-23 10:45 조회 1,651 댓글 0

James Bond, Ethan Hunt, Napoleon Solo-위장에서 일하는 비밀 요원, 고용주 및 기타 요원에게 비밀 메시지를 보냅니다. 솔직히 말해서 비밀 요원은 멋지다.

적어도 영화와 책에서. 그들은 멋진 도구를 얻고, 악당을 사냥하고, 멋진 옷을 입고 멋진 클럽을 방문합니다. 그리고 하루가 끝나면 그들은 세상을 구합니다. 어렸을 때 비밀 요원이 되고 싶었을 것입니다.


이 게시물에서는 비밀 요원이 다른 이미지 내에서 이미지를 숨기기 위해 사용할 수 있는 기술인 스테가노그래피를 보여 드리겠습니다.


하지만 먼저 : 스테가노그래피란 무엇입니까? 


스테가노그래피는 "제임스 본드"영화에서 MI6의 유명한 엔지니어 Q가 발명한 것일 수 있지만 실제로는 훨씬 더 오래되었습니다! 보이지 않아야 할 메시지나 이미지를 눈에서 숨기는 것은 이미 고대부터 일이었습니다.


Wikipedia에 따르면 기원전 440 년에 고대 그리스 작가 인 Herodotus는 자신의 가장 충성스러운 하인 중 한 사람의 머리를 깎아 대머리에 메시지를 쓰고 머리카락이 다시 자라면 받는 사람에게 하인을 보냈습니다.


오늘은 누구도 면도하지 않을 것입니다. 서로의 머리에 메시지를 숨기는 것은 말할 것도 없습니다. 대신 다른 이미지에 이미지를 숨기고 있습니다.


이를 위해 한 이미지의 색상에서 중요하지 않은 부분을 제거하고 다른 이미지의 색상에서 중요한 부분으로 대체합니다.


무엇을 기다립니다? 중요하지 않습니까? 


이것이 의미하는 바를 이해하려면 먼저 PNG와 같이 색상이 어떻게 작동하는지 알아야 합니다. 웹 개발자는 #f60053 또는 #16ee8a와 같은 16 진수 색상 표기법에 익숙할 수 있습니다. 16 진수 색상은 네 가지 부분으로 구성됩니다.


  • 접두사로 #
  • 빨간색의 두 16 진수
  • 녹색의 경우 두 16 진수
  • 파란색의 경우 두 16 진수

값은 각 색상에 대해 00에서 FF까지 갈 수 있기 때문에 이것은 십진수로 0에서 255까지라는 것을 의미합니다. 바이너리에서는 00000000에서 11111111로 이동합니다.


이진법은 십진수와 매우 유사하게 작동합니다. 한 자리가 더 왼쪽 일수록 값이 높아집니다. 따라서 비트의 "의미"는 증가하고 더 왼쪽에 있습니다.


예 : 11111111은 01111111의 거의 두 배이고 반면에 11111110은 약간 더 작습니다. 인간의 눈은 #FFFFFF와 #FEFEFE의 차이를 인식하지 못할 가능성이 높습니다. 하지만 #FFFFFF와 # 7F7F7F의 차이점을 알 수 있습니다.


JS로 이미지를 숨기자 


이 스톡 이미지를 숨기겠습니다.


A stock image of a computer with some CLI output 


이 고양이 이미지에서 :


A fluffy orange cat 


다른 이미지를 숨기는 작은 Node 스크립트를 작성하겠습니다. 즉, 내 스크립트는 세 가지 인수를 받아야 합니다.


  • 메인 이미지
  • 숨겨진 이미지
  • 목적지

먼저 이것을 코딩 해 보겠습니다.


const args = process.argv.slice(2)

const mainImagePath = args[0]
const hiddenImagePath = args[1]
const targetImagePath = args[2]

// Usage:
// node hide-image.js ./cat.png ./hidden.png ./target.png


여태까지는 그런대로 잘됐다. 이제 메인 이미지의 크기를 가져 오기 위해 image-size를 설치하고 노드가 이미지를 검사하고 새 이미지를 생성 할 수 있도록 캔버스를 설치합니다.


먼저 메인 이미지와 비밀 이미지의 크기를 알아 내고 둘 모두에 대한 캔버스를 생성 해 보겠습니다. 출력 이미지를 위한 캔버스도 생성합니다.


const imageSize = require('image-size')
const { createCanvas, loadImage } = require('canvas')

const args = process.argv.slice(2)

const mainImagePath = args[0]
const hiddenImagePath = args[1]
const targetImagePath = args[2]

const sizeMain = imageSize(mainImagePath)
const sizeHidden = imageSize(hiddenImagePath)

const canvasMain = createCanvas(sizeMain.width, sizeMain.height)
const canvasHidden = createCanvas(sizeHidden.width, sizeHidden.height)
const canvasTarget = createCanvas(sizeMain.width, sizeMain.height)

const contextMain = canvasMain.getContext('2d')
const contextHidden = canvasHidden.getContext('2d')
const contextTarget = canvasTarget.getContext('2d')


다음으로 두 이미지를 각각의 캔버스에 로드 해야 합니다. 이 메서드는 promise를 반환하기 때문에 async / await를 허용하는 즉시 호출되는 함수 표현식에 나머지 코드를 넣습니다.


;(async () => {
  const mainImage = await loadImage(mainImagePath)
  contextMain.drawImage(mainImage, 0, 0, sizeMain.width, sizeMain.height)

  const hiddenImage = await loadImage(hiddenImagePath)
  contextHidden.drawImage(hiddenImage, 0, 0, sizeHidden.width, sizeHidden.height)
})()


다음으로 이미지의 모든 픽셀을 반복하고 색상 값을 얻습니다.


  for (let x = 0; x < sizeHidden.width; x++) {
    for (let y = 0; y < sizeHidden.height; y++) {
      const colorMain = Array.from(contextMain.getImageData(x, y, 1, 1).data)
      const colorHidden = Array.from(contextHidden.getImageData(x, y, 1, 1).data)
    }
  }


이 값을 사용하여 이제 대상 이미지에 그릴 모든 픽셀의 "결합 된"색상을 계산할 수 있습니다.


새로운 색상 계산 


앞서 중요한 부분에 대해 말씀 드렸습니다. 실제로 색을 계산하기 위해 이것을 조금 더 설명하겠습니다.


색상 A와 B의 빨간색 부분을 결합하고 싶다고 가정 해 보겠습니다. 다음과 같이 해당 비트 (8 비트)를 나타냅니다.


A7 A6 A5 A4 A3 A2 A1 A0 (color A)
B7 B6 B5 B4 B3 B2 B1 B0 (color B)



색상 A에서 B 색상을 숨기려면 A의 첫 번째 (가장 오른쪽)를 B의 마지막 (가장 왼쪽) 비트로 교체합니다. 결과 비트 패턴은 다음과 같습니다.


A7 A6 A5 A4 A3 B7 B6 B5


즉, 두 색상의 정보가 일부 손실되지만 결합 된 색상은 색상 B 자체와 크게 다르지 않습니다.


이 코드를 작성해 보겠습니다.


const combineColors = (a, b) => {
  const aBinary = a.toString(2).padStart(8, '0')
  const bBinary = b.toString(2).padStart(8, '0')

  return parseInt('' +
    aBinary[0] +
    aBinary[1] +
    aBinary[2] +
    aBinary[3] +
    aBinary[4] +
    bBinary[0] +
    bBinary[1] +
    bBinary[2], 
  2)
}


이제 픽셀 루프에서 해당 함수를 사용할 수 있습니다.


const colorMain = Array.from(contextMain.getImageData(x, y, 1, 1).data)
const colorHidden = Array.from(contextHidden.getImageData(x, y, 1, 1).data)

const combinedColor = [
  combineColors(colorMain[0], colorHidden[0]),
  combineColors(colorMain[1], colorHidden[1]),
  combineColors(colorMain[2], colorHidden[2]),
]

contextTarget.fillStyle = `rgb(${combinedColor[0]}, ${combinedColor[1]}, ${combinedColor[2]})`
contextTarget.fillRect(x, y, 1, 1)


거의 완료되었으므로 이제 결과 이미지만 저장하면 됩니다.


const buffer = canvasTarget.toBuffer('image/png')
fs.writeFileSync(targetImagePath, buffer)


결과는 다음과 같습니다


An image hidden in the cat image from above 


화면 설정에 따라 이미지 상단에 숨겨진 이미지의 패턴이 표시 될 수 있습니다. 일반적으로 숨겨진 이미지를 더 난독화하는 이미지를 사용합니다.


숨겨진 이미지를 복원하려면 어떻게 해야 합니까? 


숨겨진 이미지를 추출하기 위해 필요한 것은 각 픽셀의 마지막 3 비트를 읽고 다시 최상위 비트로 만드는 것입니다.


const extractColor = c => {
  const cBinary = c.toString(2).padStart(8, '0')

  return parseInt('' +
    cBinary[5] + 
    cBinary[6] + 
    cBinary[7] + 
    '00000',
  2)
}


모든 단일 픽셀에 대해 이 작업을 수행하면 원본 이미지 (및 몇 가지 아티팩트)를 다시 얻습니다.


Original image in lower quality 


이제 이미지를 숨기고 다른 비밀 요원에게 숨겨진 메시지를 보내 진짜 비밀 요원처럼 느껴질 수 있습니다!


https://dev.to/thormeier/feel-like-a-secret-agent-hidden-messages-in-images-with-steganography-37kh




댓글목록 0

등록된 댓글이 없습니다.

웹학교 로고

온라인 코딩학교

코리아뉴스 2001 - , All right reserved.