천재교과서 개인정보 유출 사건 정리

2021년 4월 발생한 23,624건의 개인정보 유출
-> 시정명령, 9억여원의 과징금, 1740만원의 과태료 부과처분
아래 더보기는 사건 자세히 정리

초등 이용자의 개인정보 유출 흔적 감지(2021.4.8) 피심인(심의,의결서에서 칭함, 재판에 대해서는 원고,항소인)에 대한 개인정보 취급, 운영 실태 및 보호법 위반 여부 조사(2021.4.9~2021.9.1) 아래는 조사하며 얻은 행위 사실 가. 개인정보 수집 현황 ![개인정보 수집 현황](https://blog.kakaocdn.net/dna/cw8fbt/btsHumUE5Bx/AAAAAAAAAAAAAAAAAAAAAD2_Y6mp_QmSa3212yrOiadDIb4SsuvmaDBLATDpbdkl/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=%2BsCI7Y5RrHnFWVuHyEZWUqI1W%2Fw%3D) 나. 개인정보 유출 경위 21.4.7 17:30 DB관리자가 비정상 쿼리 발견 21.4.7 23:00 백과 웹서버에 웹셸 및 터널링 프로그램 존재 확인 21.4.8 13:50 한국인터넷진흥원에 개인정보 유출 신고 21.4.8 17:20 유출 대상자들에게 이메일, 문자로 유출 가능성 통지, 홈페이지 공지사항 게시

제1심 판결 서울행정법원 2023.1.13 선고 2022구합61564 판결
제2심 판결 서울고등법원 2023.11.2 선고 2023누34486 판결

제2심에 관해서.. 과징금부과처분 취소청구의 소

원고, 항소인: 주식회사 A, 천재교과서
피고, 피항소인: 개인정보보호위원회

청구 취지 및 항소 취지

제 1심 판결 취소
2021.10.27 의결 B로, 한 별지 1 처분내역 기재 각 처분 중 제2의 가,다,라.항 과징금납부명령 부분을 취소

아래 더보기는 2021.10.27 개인정보보호위원회 심의,의결 정리

안건번호 제2021 - 017 - 265호 주문 정리 1. 피심인에 대하여 다음과 같이 시정조치를 명한다. 2. 피심인에 대하여 다음과 같이 과징금과 과태료를 부과한다. 3. 피심인의 법 위반행위 내용 및 결과를 개인정보보호위원회 홈페이지에 공표한다. [의결서(제2021-017-264~272호).pdf](https://blog.kakaocdn.net/dna/LzrqN/btsHuO4cvM7/AAAAAAAAAAAAAAAAAAAAABTzQR_o6MX14c78UIBliviJbUE7AR0puINfkUIzbP6i/%EC%9D%98%EA%B2%B0%EC%84%9C(%EC%A0%9C2021-017-264~272%ED%98%B8).pdf?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1769871599&allow_ip=&allow_referer=&signature=7BeVwVyC%2BUprxUNdJYVl5Fic%2B6M%3D&attach=1&knm=tfile.pdf)

피고의 원고에 대한 처분 사유

  1. 개인정보처리시스템에 대한 접근통제를 소홀히 한 행위
  2. 개인정보처리시스템의 접속기록 보관 및 점검을 소홀히 한 행위
  3. 개인정보의 유출 사실을 추가 통지하지 않은 행위

과징금의 산정 근거

1) 기준 금액의 산정
가) 관련 매출액의 산정
관련 매출액
나) 중대성의 판단

  • 개인정보 보호법 제29조의 안전조치의무 근거, 피고는 원고에게 중과실 있다고 봄
  • 과징금 부과기준 제5조 제3항 본문 근거, 원고의 위반행위 ‘중대한 위반행위’
    다) 기준금액의 산출
    개인정보 보호법 시행령 및 과징금 부과기준 제3조 제1항 근거,
    관련 매출액(53,771,039,000원) X ‘중대한 위반행위’에 해당하는 부과 기분율 21/1000

2) 필수적 가중 및 감경
가) 과징금 부과기준 제6조, 제7조 근거,
장기 위반행위(2년 초과)로 인해 50% 가산(564,594,000원)
나) 원고가 최근 3년 이내 개인정보 보호법 제39조의15 제1항 행위들로 과징금 부과처분 없음으로 인해
50% 감경(564,594,000원)

3) 추가적 가중 및 감경
과징금 부과기준 제8조에 근거,

  • 원고가 조사에 적극 협력
  • 개인정보유출사실을 자진 신고한 점
    20% 감경(225,839,000원)

원고의 주장

1) 처분 사유의 부존재
가) 제1처분사유의 부존재: Any->Any 허용 규칙만으로 접근 통제 소홀히 운영했다고는 볼 수 없음
나) 제2처분사유의 부존재: 유출사고와 원고의 위반 행위 사이에 관련성 인정될 수 없음

2) 재량권의 일탈, 남용
가) 기준금액 산출 위법: 중대한 위반행위 인정할 수 없음
나) 필수적 가중 위법: 장기 위반행위 인정할 수 없음
다) 비례의 원칙 및 평등의 원칙 위반: 과징금납부명령 지나치다

인정사실

1) 원고와 E의 시스템 운영 현황
원고와 E는 각각 G와의 원격보안관제 서비스 계약, 원고가 운영한 C 서비스는 E이 운영한 F와 일부 인프라 공유
C서비스(원고)의 2차 방화벽에 F서비스(G) 접근 권한을 가진 IP도 C서비스 DB에 접속할 수 있었음 - Any->Any 허용 규칙

2) 유출사고 경위와 과정
F 웹 서버에 웹쉘 업로드 -> 웹쉘에 접속하여 터널링 프로그램 업로드 -> C서비스 DB에 직접 접속 후 개인정보 외부 전송

3) 원고의 후속조치
원고 DB 접근제어 계정 이외에 C의 DB 접근 불가능 - Any->Any 차단 규칙

판단

1) 처분사유의 존재 여부
가) 관련 규정과 법리
(1) 개인정보 보호법 제29조의 위임에 따라
개인정보 보호법 시행령 제48조의2 제1항 :
제2호 (나)목에서 ‘개인정보에 대한 불법적인 접근을 차단하기 위한 조치’
제3호에서 ‘접속기록의 위조 변조 방지를 위한 조치’

(2) 위반 여부 판단 시 고려할 점
… 당시 보편적으로 알려져 있는 정보보안의 기술 수준
… 정보통신서비스 제공자의 업종 영업규모와 정보통신서비스 제공자가 취하고 있던 저체적인 보안조치의 내용
… 정보보안에 필요한 경제적 비용 및 효용의 정도
… 해킹기술의 수준과 정보보안기술의 발전 정도에 따른 피해 발생의 회피 가능성
… 정보통신서비스 제공자가 수집한 개인정보의 내용과.. 등등

나) 제1처분사유 존재 여부: 웹셀을 통한 정보유출 충분히 방지할 수 있었음 -> 개인정보 보호법령과 보호조치 기준 제4조 제5항 위반
다) 제2처분사유 존재 여부: 관련성 충분히 인정됨

2) 재량권의 일탈, 남용 여부
가) 기준금액 산출 위법 여부: 중과실이 있다고 봄이 타당
나) 필수적 가중 위법 여부: 장기 위반행위에 해당한다고 봄이 타당
다) 비례의 원칙 및 평등의 원칙 위반 여부: 지나치게 가혹하다 보기 어렵다

소결

과징금납부명령 적법, 원고의 주장 모두 받아들일 수 없다.

[TBTL2024] Flagcheck

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // ebx
  int seed; // [rsp+4h] [rbp-6Ch]
  int i; // [rsp+8h] [rbp-68h]
  int j; // [rsp+Ch] [rbp-64h]
  char s[72]; // [rsp+10h] [rbp-60h] BYREF
  unsigned __int64 v9; // [rsp+58h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  printf("Let me check your flag: ");
  __isoc99_scanf("%s", s);
  if ( strlen(s) != 63 )
    no();
  seed = 1;
  for ( i = 0; i < strlen(s); ++i )
    seed *= s[i];
  srand(seed);
  for ( j = 0; j < strlen(s); ++j )
  {
    v3 = s[j];
    if ( ((rand() % 256) ^ v3) != target[j] )
      no();
  }
  puts("Correct!");
  return 0;
}

s 길이 63
seed 값 계산 가능..? -> random에서 쓰는 문자열(ascii) seed는 0~256

v3는 s[j]
(rand()%256)^v3값이 target[j]의 값과 같아야 함
이때 rand는 seed가 있으므로 규칙적으로 정해짐

33h, 84h, 3Dh, 3Fh, 2Ah, 93h, 7Bh, 82h, 1Ah, 0ACh, 8Eh //11
0F4h, 0B1h, 0CBh, 8Dh, 21h, 0Eh, 0B7h, 67h, 96h, 2Ch  //10
81h, 0D3h, 0BCh, 29h, 6Ch, 4Bh, 0Dh, 0EDh, 0Fh, 0Dh  //10
0EEh, 56h, 40h, 52h, 0D5h, 5, 6Dh, 90h, 3Eh, 7Ah, 1Bh //11 
69h, 23h, 1Fh, 0B6h, 1Dh, 0BCh, 98h, 0D1h, 0A6h, 83h  //10 
0E9h, 0EBh, 13h, 21h, 3Dh, 0F8h, 2Bh, 79h, 53h, 4Fh   //10 
0A1h  //1

이게 target…

#include <iostream>
#include <random>

using namespace std;

int main() {
    
    unsigned char target[63] = { 0x33, 0x84, 0x3d, 0x3f, 0x2a, 0x93, 0x7b, 0x82, 0x1a, 0xac, 0x8e,
                      0xf4, 0xb1, 0xcb, 0x8d, 0x21, 0x0e, 0xb7, 0x67, 0x96, 0x2c,
                      0x81, 0xd3, 0xbc, 0x29, 0x6c, 0x4b, 0x0d, 0x00, 0xed, 0xfd,
                      0xee, 0x56, 0x40, 0x52, 0xd5, 0x05, 0x6d, 0x90, 0x3e, 0x7a, 0x1b,
                      0x69, 0x23, 0x1f, 0xb6, 0x1d, 0xbc, 0x98, 0xd1, 0xa6, 0x83,
                      0xe9, 0xeb, 0x13, 0x21, 0x3d, 0xf8, 0x2b, 0x79, 0x53, 0x4f, 0xa1 };

    unsigned long long seedst = 0;
    unsigned long long seeden = 256;
    bool found = true;
    unsigned long long seed;
    char s[64] = { 0, };

    for (unsigned long long i = seedst; i <= seeden; ++i) {
        seed = i;
        srand(i);

        for (int j = 0; j < 63; ++j) {
            int randd = rand() % 256;
            s[j] = randd ^ target[j];

            if (s[j] < 32 || s[j] > 126) {
                found = false;
                break;
            }
        }
    }
    if (found) {
        cout << "seed:" << seed << ", string:" << s;
    }
    else
        cout << "NOT FOUND";

    return 0;
}

…안된다… 뭔가를 잘못했나 싶어서 라이트업도 참고 해봤는데 라이트업 코드도 실행이 안된다 왜일까…?

라우터 정리

라우터: data 전달하는 경로(route)를 정하는 장치
이중에서 (가정용) 공유기는 단일 경로만 다루고 있다.
스위치는 data의 통로를 선별하여 차단 또는 개방하는 문지기 역할을 수행 -> 불필요 data 필터링

라우터 기능
IP주소 사용하여 전달, 두 프로토콜이 같아야 함

  1. 경로 설정 -> 길을 검사하고 테이블링
  2. 패킷 전달

스위치, 라우터 차이점…
스위치: 중간에 있어서 경로를 스위칭 -> 데이터 링크 계층
라우터: 경로를 찾아주는 -> 네트워크 계층
라우터는 관리자의 설정으로 라우팅 테이블 생성, 통신 필요

경로 설정에는 인위적으로 등록하는 정적 경로(Static Routing)과 알고리즘에 의해 판단하는 동적 경로(Dynamic Routing)가 있다.

  1. 정적 경로 설정 (직접 지정)
  2. 동적 경로 설정 (알고리즘)

스위칭할 때
동일한 그룹 내 정보 교환은 IGP 프로토콜로…
다른 그룹 사이 정보 교환은 EGP 프로토콜로…

라우터 이미지
target: TP-link M7350 Travel WiFi Router
휴대용 라우터: 일반 라우터 + 무선 어댑터 역할
4G 대상인 거 같다
이더넷 케이블 연결 -> 라우터를 액세스 포인트로 활성화 -> 기기에서 암호로 로그인 -> 라우터 사용
ISP와 포켓 라우터 연결 -> 무선 신호 브로드캐스팅

v3의 제품설명서이지만… 일단 작동 방식은…
IP address는 192.168.0.1이고, 서브넷 마스크는 255.255.255.0 -> 변경 가능

Connect to the Internet

  1. 무선 네트워크 연결
  2. SSID 선택한 다음 연결 -> menu>Device Info에 SSID, PW, Login Name 담겨 있음

연결 화면

  1. 해당 화면 확인해서 기기에서 PW 입력하면 연결됨
    휴대폰이나 패드는 http://192.168.0.1 입력하고 아이디, 비밀번호 admin으로 로그인하면 좀 더 빠른 설정

웹 기반 관리 페이지에는 다음과 같은 기능 존재

관리 페이지
간단하게 살펴 봤는데

  1. Wizard
  2. Status: 읽기 전용이라 딱히..
  3. SMS: 받은, 보낼 편지 등 메세지 담겨 있음
  4. advanced

[5주차] 암호 문서화 과제

  1. AES
    공개키, 대칭 알고리즘
    128,192,256 비트 세 가지 종류가 AES 표준

알고리즘

  1. 평문을 128비트 또는 16바이트 블록화
  2. 열 우선 행렬 생성(state matrix)
  3. KeyExpansion: 암호화 키 가져와서 추가키 생성, 각 라운드마다 key 하나 생성 -> round key
  4. AddRoundKey: 라운트 키의 각 바이트 XOR 상태 행렬의 바이트,
    ( x_{i,j} = p_{i,j} \oplus k_{i,j} )
  5. 일련의 라운드 수행: SubBytes -> ShiftRows -> MixColumns(마지막 라운드 제외) -> AddRoundKey
#예제에서 쓰인 블록 암호 모드는 ECB, 해당 부분 설명으로 아래서... 
import string
import random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

#암호화 
def aes_encrypt(key, plaintext):
    cipher = AES.new(key, AES.MODE_ECB)
    ciphertext = cipher.encrypt(pad(plaintext.encode('utf-8'), AES.block_size)) #AES 사이즈 지정 
    return ciphertext

#복호화 
def aes_decrypt(key, ciphertext):
    cipher = AES.new(key, AES.MODE_ECB) #이때 key는 대칭키 
    decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size)
    return decrypted_data.decode('utf-8')
  1. DES
    비밀키, 대칭 암호 알고리즘

알고리즘

  1. Initial permutation: 각 비트 치환
  2. 라운드 함수로 서로 다른 서브키로 각 비트 치환
  3. 블록화
  4. Final permutation: 한 번 더 치환
from Crypto.Cipher import DES
from Crypto.Hash import SHA256 as SHA

class myDES():

    # DES 초기화
    def __init__(self, keytext, ivtext):
        hash = SHA.new()
        hash.update(keytext.encode('utf-8'))
        # keytext를 해시화했을 때 첫 8byte를 key로 함
        key = hash.digest()
        self.key = key[:8]

        hash.update(ivtext.encode('utf-8'))
        iv = hash.digest()
        # ivtext를 해시화했을때 첫 8byte를 iv로 함
        # iv는 CBC 모드 운영을 위한 초기화벡터를 말함 
        self.iv = iv[:8]

    # ECB 모드로 암호화
    def encrypt_ECB(self, plaintext):
        # 항상 8byte 단위로 끊어서 암호화하기 때문에 평문이 8byte로 끊기지 않는다면 
        # padding값을 추가해 8byte로 만들어줌
        while(len(plaintext) % 8 != 0):
            plaintext += ' '
        des = DES.new(self.key, DES.MODE_ECB)
        encryptMsg = des.encrypt(plaintext.encode())
        return encryptMsg

    # ECB 모드로 암호화된 암호문을 복호화
    def decrypt_ECB(self, ciphertext):
        des = DES.new(self.key, DES.MODE_ECB)
        descryptMsg = des.decrypt(ciphertext)
        return descryptMsg

    # CBC 모드로 암호화
    def encrypt_CBC(self, plaintext):
        while(len(plaintext) % 8 != 0):
            plaintext += ' '

        # CBC 모드에서는 iv(초기화 벡터) 값이 필요
        des = DES.new(self.key, DES.MODE_CBC, self.iv)
        encryptMsg = des.encrypt(plaintext.encode())
        return encryptMsg

    # CBC 모드로 암호화된 암호문을 복호화
    def decrypt_CBC(self, ciphertext):
        des = DES.new(self.key, DES.MODE_CBC, self.iv)
        descryptMsg = des.decrypt(ciphertext)
        return descryptMsg
  1. RSA
    비대칭 암호 알고리즘
    공개키, 비밀키 모두 사용하는데, 공개키는 암호화할 때, 비밀키는 복호화할 때 사용

알고리즘

  1. 두 큰 소수를 곱해서 공개키 생성
  2. 이때 개인키는 (공개키X개인키)mod 오일러(p*q)=1인 숫자 -> ( (e * d) \mod \Phi(n) = 1 )
    ( n = p * q ) (p, q는 소수)
    ( \Phi(n) = (p — 1) * (q — 1) )
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

def generate_keys():
    key = RSA.generate(2048)
    # 개인키 생성 
    private_key = key.export_key()
    # 공개키 생성, 이때 서로 소수 
    public_key = key.publickey().export_key()
    return private_key, public_key

# 암호화 
def encrypt_message(public_key, message):
    rsa_key = RSA.import_key(public_key)
    # OAEP는 RSA와 함께 사용되는 Padding 
    cipher = PKCS1_OAEP.new(rsa_key)
    encrypted_message = cipher.encrypt(message.encode())
    return encrypted_message

# 복호화 
def decrypt_message(private_key, encrypted_message):
    # key import(개인키) 
    rsa_key = RSA.import_key(private_key)
    cipher = PKCS1_OAEP.new(rsa_key)
    decrypted_message = cipher.decrypt(encrypted_message).decode()
    return decrypted_message
  1. 해시 함수
    해시 함수들은 복호화되지 않는다. 한 번 압축함수를 거치면 되돌릴 수 없다(예측 불가능성)
    해시 암/복호화 툴 사이트 (CTF에서 필요할 때 들어가기..)

4-1. SHA256
SHA 해시 함수는 1, 256, 384 등.. 다양함
어떤 입력값이든 고정된 길이 n(SHA256에서는 256)으로 변환 처리한다.

알고리즘
1) 패딩 -> 512bit의 배수가 되도록
2) 파싱 -> 32bit씩 나눈다.
3) 해싱

import hashlib #이전 암호 라이브러리와 다르다 

# SHA-256 해시 객체 생성
hash_object = hashlib.sha256()

# 데이터 업데이트
hash_object.update(data.encode())

# 해시 값 추출
hash_value = hash_object.hexdigest()

4-2. MD5
128비트 길이를 만들어주는 해시 함수
SHA1보다는 안전하나 지금은 원본을 찾을 수 있는 빠른 알고리즘이 나와 잘 사용하진 않는다.

알고리즘

  1. 데이터 비트 오른쪽에 1
  2. 비트 길이 448(mod 512)되도록 0 패딩(오른쪽에)
  3. 마지막 64비트에 padding되기 전 데이터 길이 저장(리틀 엔디언)
  4. 512비트 블록으로 쪼개기
  5. 각 블록에 대해 연산
import hashlib

result = hashlib.md5(data.encode()).hexdigest()
  1. 블록 암호
    평문을 분할하여 m길이*n개 블록을 만드는 알고리즘이다.
    운영모드는 5가지가 있는데 ECB, CBC, CFB, OFB, CTR
5-1. ECB
블록 단위로 나누고 각 블록마다 key로 암호화
반복 공격에 취약하다.

ECB

5-2. CBC
iv로 첫 블록을 암호화하고 다음 블록은 이전 암호와 평문을 xor 후 key로 암호화
초기 iv가 동일하며 출력 결과가 동일한 취약점이 있다.

CBC

5-3. CFB
iv로 첫 블록 암호화하고 이후 블록은 key와 암호화 후 평문과 xor
재전송 공격에 취약하다.

CFB

5-4. OFB
CFB와 이전 블록의 결과를 가져다 쓰는 위치가 다르다(그림으로 확인하기)
OFB는 xor 전 암호화 결과를 다음 블록에서 사용한다.

OFB

5-5. CTR
암호화 할 때마다 증가하는 Counter 변수를 두고, 이것을 key와 암호화

CTR

[5주차] block.py

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

KEY = ?
FLAG = ?

@chal.route('/ecb_oracle/encrypt/<plaintext>/')
def encrypt(plaintext):
    plaintext = bytes.fromhex(plaintext)

    padded = pad(plaintext + FLAG.encode(), 16)
    cipher = AES.new(KEY, AES.MODE_ECB)
    try:
        encrypted = cipher.encrypt(padded)
    except ValueError as e:
        return {"error": str(e)}

    return {"ciphertext": encrypted.hex()}

flag도 key도 모름… plain은 넣을 수 있음
16바이트로 padding
음… 문제 감을 못잡겠어서 라이트업 참고했다…

ECB Oracle

해당 사이트에서 푸는 거였다

패딩은 16 단위이다. 왼쪽은 \x61을 15번 채우면 flag에서 한 글자를 가져올 수 있다.
오른쪽에서 브루트포스처럼 a부터 넣어서 같은 output을 출력하는 한 글자를 찾으면 그 값이 flag의 첫글자이다.
이후에는 \x61 14번 + 찾은 글자 + 찾을 flag 한 글자로 반복한다.

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import time
import requests

#ciphertext를 받아온다. 
def get_cipher(plaintext):
    chellenge_site = 'https://aes.cryptohack.org/ecb_oracle/encrypt/' + plaintext.hex() + '/'
    r = requests.get(chellenge_site)
    ciphertext = r.json()
    return bytes.fromhex(ciphertext['ciphertext'])

flag=b''

#range 범위 설정에 대해서는 후술... 
for i in range(0, 32):
    plaintext = b'\x61' * (31 - i)  #\x61을 31-i만큼 채운다 
    ciphertext = get_cipher(plaintext)[:32]  #보내고 ascii 하나씩 대입해서 비교 
    plaintext += flag  #찾아둔 flag들 더해야 함 
    print("try %d" % (i+1))
    
    #ascii 브루트포스 for문 
    for j in range(33, 127):
        plaintext = plaintext[:31]
        plaintext += j.to_bytes(1, byteorder='big')

        wr_ci = get_cipher(plaintext)[:32]
        #같다면 
        if (wr_ci == ciphertext):
            flag += j.to_bytes(1, byteorder='big')
            print(flag)
            break
        time.sleep(1)

길이는 어떻게 알았냐 하면…
61을 1번 넣고, 2번 넣고… 하다보면 7번의 61을 넣었을 떄 갑자기 cipher가 길어진다. -> 블록이 추가됐다는 뜻
7+x=32
x=25이다.