Step 222. .NET과 파이썬 바이너리 리버싱 — 기계어가 아닌 바이너리

Step 222. .NET과 파이썬 바이너리 리버싱 — 기계어가 아닌 바이너리

Level 3 — Reversing 심화 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간

전제: Step 221(키젠), Step 178(Reversing 입문). 파이썬으로 간단한 스크립트를 읽고 쓸 수 있어야 합니다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 남의 프로그램을 디컴파일해 로직을 추출하는 행위는 라이선스 위반이 될 수 있습니다 — 오늘의 대상은 여러분이 직접 만든 스크립트입니다.

  • 준비물: Windows의 파이썬 3 (실측: 3.12.14) 또는 WSL의 python3 (실측: 3.12.3). dnSpy와 PyInstaller 계열 도구는 설치하지 않고 화면 예시로 안내합니다.
  • 주의: "기계어가 아닌 바이너리"는 오히려 너무 잘 읽혀서 위험합니다 — 남의 코드를 쉽게 훔쳐 볼 수 있다는 뜻이고, 그래서 법적 경계를 더 엄격히 지켜야 하는 영역입니다.

지금까지의 리버싱은 기계어(x86-64 어셈블리)와의 싸움이었습니다. 그런데 세상의 실행 파일이 전부 기계어는 아닙니다. .NET 프로그램은 IL 바이트코드, PyInstaller로 묶인 파이썬은 파이썬 바이트코드를 품고 있습니다 — 그리고 이들은 디컴파일하면 소스 코드에 가깝게 복원됩니다. 오늘의 첫 기술은 분석이 아니라 판별입니다: "이 바이너리, 어느 세계 것인가?"를 먼저 알아야 올바른 도구를 꺼낼 수 있습니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 네이티브 / .NET / 파이썬 바이너리의 차이를 설명하고 판별한다
  • 파이썬의 dis 모듈로 함수를 바이트코드로 분해해 상수를 읽는다
  • .pyc 파일에서 strings로 상수가 노출되는 것을 실측한다
  • PyInstaller exe의 내부 구조(부트로더 + 아카이브)를 설명한다
  • dnSpy가 .NET 바이너리에서 무엇을 보여 주는지 안다 (화면 예시)

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3.12 (실측: Windows 3.12.14) + WSL strings
오늘의 명령어 python -c "import dis; ...", python -m py_compile, strings
필요한 개념 바이트코드, IL(중간 언어), 디컴파일, .pyc 구조, PyInstaller 아카이브
오늘의 산출물 바이트코드 분해 결과 + "왜 잘 읽히는가" 정리 노트

2-1. 세 가지 세계 — 기계어, IL, 파이썬 바이트코드

컴파일된 프로그램은 세 갈래로 나뉩니다.

  1. 네이티브(C/C++): CPU가 직접 실행하는 기계어. 디컴파일하면 "의사 코드" — 복원율이 낮습니다. Step 178~221의 세계입니다.
  2. .NET(C# 등): IL(Intermediate Language, 중간 언어) 바이트코드로 저장되고, 실행 시 CLR이 기계어로 번역합니다. 클래스·메서드 이름 같은 메타데이터가 통째로 남아 있어 dnSpy가 거의 완벽한 C#으로 복원합니다.
  3. 파이썬(PyInstaller 등): 파이썬 바이트코드(.pyc)를 exe 안에 포장합니다. 추출하면 디스어셈블(dis)이나 디컴파일(pycdc 계열)이 가능합니다.

실전의 첫 질문은 "어떻게 읽지"가 아니라 "어느 세계 것이지" 입니다. 판별이 틀리면 도구가 전부 틀어집니다 — 기계어 도구(objdump)로 .NET을 읽으려 하면 IL 덩어리가 나올 뿐입니다.

2-2. 파이썬 바이트코드와 dis

파이썬은 소스를 바로 실행하는 게 아니라, 먼저 바이트코드(bytecode)로 컴파일한 뒤 가상 머신이 해석합니다. 표준 라이브러리의 dis 모듈은 이 바이트코드를 사람이 읽을 수 있는 형태로 분해해 줍니다 — 별도 설치 없이 파이썬만 있으면 되는, 가장 가벼운 리버싱 도구입니다.

바이트코드 목록에서 리버싱의 단서가 되는 것은 LOAD_CONST — 함수가 참조하는 상수(문자열, 숫자)가 그대로 실려 있습니다. 비밀번호 해시, API 키, URL이 상수라면 여기에 보입니다.

2-3. .pyc와 PyInstaller의 구조

파이썬은 import 시 바이트코드를 .pyc 파일로 캐시합니다 (__pycache__ 폴더). 헤더 16바이트에는 매직 넘버(파이썬 버전 식별자)가 있고, 그 뒤에 바이트코드가 옵니다.

PyInstaller는 파이썬 프로그램을 단일 exe로 묶는 도구입니다. 구조는 이렇습니다:

PyInstaller exe
├─ 부트로더 (실제로 실행되는 C 프로그램)
└─ 아카이브 (압축된 .pyc 모음 + 파이썬 인터프리터 DLL)
     └─ 실행되면 부트로더가 아카이브를 풀어 바이트코드를 실행

익숙하지 않나요? Step 220의 패커 구조(스텁 + 압축 페이로드)와 같은 그림입니다. 그래서 분석 절차도 닮았습니다 — pyinstxtractor로 아카이브에서 .pyc를 추출하고(언패킹), pycdc 같은 디컴파일러로 소스를 복원합니다.

2-4. 왜 잘 읽히는가 — 메타데이터와 심볼

기계어 바이너리는 변수명·함수명이 사라진 채 번역됩니다. 반면 IL과 파이썬 바이트코드는 실행 환경(CLR, 파이썬 인터프리터)이 이름을 필요로 하므로 이름과 구조가 파일에 남습니다. 디컴파일이 잘 되는 이유는 도구가 똑똑해서가 아니라, 지워지지 않은 정보가 많기 때문입니다.

방어하는 쪽도 이걸 압니다. 그래서 .NET에는 ConfuserEx 계열의 난독화(obfuscation) 도구 — 이름을 무의미하게 바꾸고 제어 흐름을 꼬는 — 이 존재합니다. 난독화 대응은 Step 224의 주제이고, 오늘은 "난독화되지 않은 관리 코드는 소스급으로 읽힌다"는 사실 자체를 체감하는 것이 목표입니다.


3. 따라 하기

3-1. 실험실 — 분석 대상 스크립트 만들기

출제자가 되어 "비밀 문고" 스크립트를 만듭니다. Windows든 WSL이든 파이썬 3이 있으면 됩니다.

입력 (vault222.py)

import hashlib

def check(pw: str) -> bool:
    digest = hashlib.sha256(pw.encode()).hexdigest()
    return digest == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

def main():
    pw = input("password: ")
    if check(pw):
        print("correct! 문이 열립니다")
    else:
        print("wrong.")

if __name__ == "__main__":
    main()

먼저 정상 동작을 확인합니다:

echo hello | python vault222.py
password: correct! 문이 열립니다

(2026-09-09 실측, Windows 파이썬 3.12.14. 참고로 저 해시는 sha256("hello")입니다 — 소스를 본 우리만 아는 비밀입니다.)

3-2. dis로 해부 — 상수가 그대로 보인다

이제 소스를 잊고, "이 .py가 뭘 하는지"를 바이트코드로만 읽어 봅시다. 실전에서는 .pyc만 있는 상황을 상정합니다:

python -c "import dis, vault222; dis.dis(vault222.check)"
  3           0 RESUME                   0

  4           2 LOAD_GLOBAL              1 (NULL + hashlib)
             12 LOAD_ATTR                2 (sha256)
             32 LOAD_FAST                0 (pw)
             34 LOAD_ATTR                5 (NULL|self + encode)
             54 CALL                     0
             62 CALL                     1
             70 LOAD_ATTR                7 (NULL|self + hexdigest)
             90 CALL                     0
             98 STORE_FAST               1 (digest)

  5         100 LOAD_FAST                1 (digest)
            102 LOAD_CONST               1 ('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824')
            104 COMPARE_OP              40 (==)
            108 RETURN_VALUE

(2026-09-09 실측, 파이썬 3.12.)

출력 읽는 법: 왼쪽 숫자(3, 4, 5)는 원본 소스의 줄 번호 — 바이트코드에도 소스 위치 정보가 남습니다. 명령 흐름을 읽으면: hashlib를 불러 sha256 속성을 꺼내고(4번 줄), pw.encode()를 호출하고, hexdigest()까지 호출해 digest에 저장. 그리고 5번 줄 — LOAD_CONST에 64자 16진수 문자열이 평문으로 실려 있고, digest== 비교합니다.

분석가의 결론: "sha256 해시와 비교하는 검사 함수다. 비교 대상 해시는 상수로 박혀 있다." 소스 없이 바이트코드만으로 알고리즘 전체를 읽었습니다. 해시의 원문이 "hello"임은 사전 공격(흔한 단어의 해시 목록 대조) 한 번이면 나옵니다 — sha256은 빠른 해시라 비밀번호 보관에 취약합니다 (Step 105의 해시 강의 기억).

3-3. .pyc 실측 — 캐시 파일도 다 보여 준다

import 과정에서 만들어진 캐시 파일을 직접 확인합니다:

python -m py_compile vault222.py
ls __pycache__
vault222.cpython-312.pyc

(2026-09-09 실측. 파일명의 cpython-312는 "CPython 3.12로 컴파일됨"이라는 뜻입니다.)

이 파일에 strings를 들이대 봅니다 (WSL):

strings vault222.cpython-312.pyc | grep -iE "2cf24|password|correct|sha256"
@2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824)
sha256
password: u
correct! 

(2026-09-09 실측.)

읽는 법: .pyc는 바이너리지만, 상수 문자열은 압축도 암호화도 없이 실려 있습니다. 해시, 함수가 쓰는 이름(sha256), 프롬프트까지 그대로입니다. 헤더의 매직 넘버도 확인해 둡시다:

python -c "data = open('__pycache__/vault222.cpython-312.pyc','rb').read(16); print(' '.join(f'{b:02x}' for b in data))"
cb 0d 0d 0a 00 00 00 00 ae 29 a1 6a 6c 01 00 00

(2026-09-09 실측. 앞 4바이트 cb 0d 0d 0a가 파이썬 3.12의 매직 넘버입니다. 버전마다 달라서, 낯선 .pyc를 받으면 매직으로 어느 버전의 산물인지 먼저 판별합니다.)

3-4. PyInstaller exe를 만났다면 — 절차 예시

이 환경에는 PyInstaller와 추출 도구가 없으므로 절차는 예시로 안내합니다. 실전에서 "파이썬으로 의심되는 exe"를 만나면:

# 출력 예시 — PyInstaller 분석 절차
1. 판별: strings에 "PYINSTALLER", "_MEI..." 같은 흔적 검색
2. 추출: python pyinstxtractor.py sample.exe
   → sample.exe_extracted/ 폴더에 .pyc들이 풀림
   → 메인 스크립트는 보통 파일명 = 원래 프로그램 이름
3. 버전 확인: .pyc 헤더의 매직 넘버로 파이썬 버전 판별 (3-3의 방법)
4. 디컴파일: pycdc main.pyc  (또는 버전이 맞으면 decompyle3/uncompyle6)
5. 디컴파일이 깨지면 최후 수단: dis로 바이트코드 읽기 (3-2의 방법)

읽는 법: 2-3에서 본 대로 절차는 언패킹과 닮았습니다 — 포장을 풀고(추출), 안의 바이트코드를 읽습니다(디컴파일). 함정은 5번 — pycdc 계열은 최신 파이썬 바이트코드에서 완벽하지 않아, 디컴파일이 깨질 때는 3-2처럼 dis가 최후의 보루가 됩니다. 바이트코드는 느리지만 반드시 읽힙니다.

3-5. .NET 바이너리와 dnSpy — 화면 예시

.NET 세계의 표준 도구는 dnSpy입니다 (Windows 도구라 이 환경에는 없습니다). 절차는 이렇습니다:

# 화면 예시 — dnSpy 사용 흐름
1. dnSpy에 대상 exe를 드래그
2. 왼쪽 트리에 어셈블리 > 네임스페이스 > 클래스가 "이름 그대로" 펼쳐짐
3. Main 메서드를 클릭하면 거의 원본 수준의 C#이 표시됨:

private static void Main(string[] args)
{
    Console.Write("serial: ");
    string s = Console.ReadLine();
    if (License.Check(s)) { Console.WriteLine("activated!"); }
    ...
}

4. License.Check을 클릭하면 그 정의로 바로 이동 — 검증 로직이 읽힘

읽는 법: 2-4의 원리가 그대로입니다 — 클래스·메서드 이름이 파일에 살아 있어서, 디컴파일 결과가 "의사 코드"가 아니라 사실상 원본입니다. Step 221에서 우리가 디스어셈블리를 뜯어 역산했던 일이, 난독화되지 않은 .NET에서는 클릭 몇 번으로 끝납니다. 심지어 dnSpy는 편집 후 다시 저장(Edit Method)도 됩니다.

3-6. 판별 훈련 — 도구 선택표

오늘의 진짜 산출물은 이 표를 머리에 넣는 것입니다:

바이너리의 정체 첫 단서 도구 복원 수준
네이티브 (C/C++) file이 ELF/PE, strings에 시스템 호출 objdump, gdb, Ghidra 의사 코드 (낮음)
.NET strings에 mscorlib, PE 안에 IL dnSpy / ILSpy 거의 원본 (높음)
파이썬 (PyInstaller) strings에 PYINSTALLER 흔적 pyinstxtractor + pycdc/dis 소스에 가까움 (높음)
자바 .jar = zip, .class 파일 JD-GUI / jadx 거의 원본 (높음)

4. 미션과 연습문제

미션 — "바이트코드로만 풀기"

  1. vault222.py의 해시를 다른 단어의 sha256으로 바꾼 vault222b.py를 만듭니다 (단어는 스스로 고르고, 해시는 python -c "import hashlib; print(hashlib.sha256(b'단어').hexdigest())"로 계산)
  2. 이제 소스를 잊습니다 — vault222b의 check 함수를 dis로 분해하고, LOAD_CONST에서 해시를 읽어 냅니다
  3. 그 해시의 원문 단어를 찾아 프로그램을 통과시킵니다 (방법: 후보 단어들의 sha256을 계산해 대조 — 미니 사전 공격)
  4. 기록: "어느 LOAD_CONST에서 해시를 읽었고, 어떻게 원문을 찾았는가"

연습문제

문제 1. .NET과 파이썬 바이너리가 네이티브보다 "잘 읽히는" 이유를, 파일에 남아 있는 정보 관점에서 설명하세요.

문제 2. PyInstaller exe의 구조가 Step 220의 패커 구조와 닮은 점과 다른 점을 각각 한 가지씩 드세요.

문제 3. 3-2의 바이트코드에서 왼쪽의 숫자(3, 4, 5)는 무엇이며, 왜 분석에 도움이 되나요?

문제 4. pycdc가 어떤 .pyc를 디컴파일하지 못했을 때 시도할 수 있는 대안을 두 가지 말하세요.


5. 모범 답안과 완료 기준

미션 모범 답안

[제작] 단어 "orange"로 해시 계산:
       python -c "import hashlib; print(hashlib.sha256(b'orange').hexdigest())"
       → 9b1d9f4e9f9c3d68cf3c70b06e5a2b3b2fbe8fd6d95b4b8f2d95b0b90e04a1c9 (예시 — 실제 계산값 사용)
[분해] python -c "import dis, vault222b; dis.dis(vault222b.check)"
       → LOAD_CONST 줄에서 새 해시 발견 (행 번호 5의 COMPARE_OP 직전)
[원문 찾기] 후보 목록 돌리기:
       for w in ["apple","banana","orange","grape"]:
           if hashlib.sha256(w.encode()).hexdigest() == target: print(w)
       → orange 발견 → echo orange | python vault222b.py → correct!

검증하는 법: ① dis 출력의 LOAD_CONST 값과 미니 사전 공격의 target이 일치했는가, ② 찾은 단어로 실제 correct!가 출력됐는가. 소스를 다시 열어 확인하는 것은 마지막 채점에만 쓰세요 — 풀이 과정에서는 잊는 것이 규칙입니다.

연습문제 해답

문제 1 해답. 실행 환경(CLR, 파이썬 인터프리터)이 실행 중에 이름으로 클래스·메서드·속성을 찾아야 하므로, 이 이름들(메타데이터)이 파일에 지워지지 않고 남아 있기 때문입니다. 기계어로의 컴파일은 이름을 주소로 대체해 버리지만, 바이트코드/IL로의 컴파일은 이름을 보존합니다. 디컴파일러는 이 보존된 이름과 구조를 읽어 원본에 가까운 코드를 복원합니다.

문제 2 해답. 닮은 점: 둘 다 "작은 실행 코드 + 묶여 있는 페이로드" 구조입니다 — 패커의 스텁처럼 PyInstaller의 부트로더가 먼저 실행되고, 안의 내용을 풀어 실행합니다. 다른 점: 목적입니다. 패커는 숨기거나 줄이려고 싸고(그래서 strings에 원본이 안 보임), PyInstaller는 배포를 위해 묶는 것이라 추출하면 원본 바이트코드가 그대로 나옵니다. 은폐 의도가 없어서 방어 장치가 없습니다.

문제 3 해답. 원본 소스의 줄 번호입니다. 바이트코드에는 디버깅용 위치 정보가 함께 저장되기 때문입니다. 덕분에 분석가는 "이 바이트코드 뭉치가 소스의 몇 번 줄에 해당하는지" 알 수 있고, 함수의 구조(어느 줄이 한 문장이었는지)를 복원하는 실마리로 씁니다.

문제 4 해답. 첫째, 다른 디컴파일러를 시도합니다 — decompyle3, uncompyle6 등 디컴파일러마다 지원하는 파이썬 버전이 달라, .pyc 매직 넘버로 버전을 확인하고 맞는 도구를 고릅니다. 둘째, 디컴파일을 포기하고 dis로 바이트코드를 직접 읽습니다 (3-2). 디컴파일은 실패해도 디스어셈블은 항상 가능합니다 — 느릴 뿐이지 막히지는 않습니다.

완료 기준 체크리스트

  • [ ] 네이티브/.NET/파이썬 바이너리의 차이를 한 문장씩 말할 수 있다
  • [ ] dis로 함수를 분해해 LOAD_CONST의 상수를 읽었다
  • [ ] .pyc에서 strings로 상수가 노출되는 것을 실측했다
  • [ ] .pyc 헤더에서 매직 넘버의 위치와 의미(버전 식별)를 안다
  • [ ] PyInstaller exe의 구조(부트로더 + 아카이브)를 설명할 수 있다
  • [ ] dnSpy의 화면 구성(트리, 디컴파일 뷰)과 Edit Method의 존재를 안다
  • [ ] 미션: 바이트코드만으로 해시를 읽고 원문을 찾아 통과했다

6. 흔한 실수와 해결

벽 1. dis 출력이 책과 다르다

증상: 명령 이름이 다릅니다 — 예: 책엔 CALL인데 예전 자료에는 CALL_FUNCTION이 보입니다.
원인: 파이썬 바이트코드는 버전마다 바뀝니다. 3.11에서 큰 개편(적응형 인터프리터)이 있었고, 3.12의 명령 체계가 다릅니다 (오늘 실측은 3.12 기준).
해결: 명령 이름을 외우지 말고 역할을 읽으세요 — LOAD_…(값 적재), CALL(호출), COMPARE_OP(비교), STORE_…(저장). 역할은 버전이 바뀌어도 같습니다.

벽 2. pycdc로 디컴파일했더니 깨진 코드가 나온다

증상: 중간에 끊기거나 이상한 의사 코드가 섞입니다.
원인: pycdc 계열은 최신 파이썬(3.11+)의 새 바이트코드를 완전히 지원하지 못합니다. 도구의 한계이지 여러분의 실수가 아닙니다.
해결: .pyc 매직으로 버전을 확인하고(3-3), 그 버전을 지원하는 디컴파일러를 고르세요. 그래도 안 되면 3-2의 dis 직독이 최후의 보루입니다.

벽 3. exe를 objdump로 뜯었는데 알 수 없는 코드만 나온다

증상: main도 없고, 이상한 데이터 덩어리와 런타임 코드뿐입니다.
원인: 대상이 네이티브가 아니라 PyInstaller exe — 보고 있는 것은 부트로더뿐이고, 진짜 로직은 안쪽 아카이브의 .pyc에 있습니다. 판별을 건너뛰고 도구를 잘못 골랐습니다.
해결: 3-6의 표로 돌아가세요. strings ./sample.exe | grep -i pyinstaller부터. 판별 30초가 삽질 몇 시간을 줄입니다.

벽 4. .pyc의 매직이 안 맞는다는 오류 (디컴파일러)

증상: Unsupported magic 류의 오류.
원인: .pyc는 만든 파이썬 버전에 묶여 있습니다. 다른 버전의 도구/인터프리터는 거부합니다.
해결: 헤더 앞 4바이트(3-3 실측: 3.12는 cb 0d 0d 0a)를 읽어 버전을 판별하고, 같은 버전의 파이썬이나 그 버전 지원 디컴파일러를 준비하세요.

벽 5. "잘 읽힌다"는 사실에 방심해 법적 경계를 넘을 뻔한다

증상: 회사 소프트웨어, 구매한 프로그램의 로직이 궁금해집니다 — dnSpy로 열면 다 보이니까요.
원인: 기술적 용이성과 법적 허용성은 별개입니다. 리버싱을 금지하는 라이선스 조항이 있는 소프트웨어를 디컴파일하는 것은 위법이 될 수 있습니다.
해결: 대상이 "내가 만든 것" 또는 "분석을 허용한 것(연습용 crackme, CTF 문제, 악성코드 분석 등 직무상 정당한 경우)"인지 먼저 확인하는 습관을 들이세요. 쉽게 열린다고 열어도 되는 것은 아닙니다.


7. 정리

오늘의 개념

개념 한 줄 설명
바이트코드 파이썬 소스가 컴파일된 중간 형태 — 가상 머신이 해석한다
dis 파이썬 표준 모듈 — 바이트코드를 사람이 읽을 수 있게 분해
IL(중간 언어) .NET의 바이트코드 — 메타데이터가 남아 거의 완벽히 디컴파일된다
dnSpy .NET 디컴파일러 — 이름 그대로의 C#을 보여 주고 편집도 된다
.pyc 파이썬 바이트코드 캐시 — 헤더에 버전 매직, 상수는 평문
PyInstaller 파이썬을 exe로 묶는 도구 — 부트로더 + .pyc 아카이브 구조
난독화 잘 읽히는 것을 막는 기술 — 이름 파괴, 제어 흐름 꼬기

오늘의 명령어

명령 하는 일
python -c "import dis, 모듈; dis.dis(모듈.함수)" 함수를 바이트코드로 분해 — 상수(LOAD_CONST) 읽기
python -m py_compile 파일.py .pyc 생성 (분석 대상 만들기)
strings 파일.pyc | grep 패턴 바이트코드 캐시에서 평문 상수 건지기
.pyc 헤더 16바이트 읽기 앞 4바이트 = 매직 넘버 → 파이썬 버전 판별
strings sample.exe | grep -i pyinstaller PyInstaller exe 판별
pyinstxtractor → pycdc exe에서 .pyc 추출 → 디컴파일 (절차 예시)

명령어보다 중요한 감각

오늘의 요점은 도구 목록이 아니라 순서입니다. 판별이 먼저, 분석은 그다음. 바이너리를 받으면 filestrings로 어느 세계 것인지부터 확인하고, 그 세계의 도구를 꺼냅니다. 그리고 세계가 다르면 난도가 극적으로 다르다는 것 — 기계어에서는 며칠 걸릴 분석이 관리 코드에서는 몇 분이면 끝납니다.

마지막으로, 이 "잘 읽힘"은 양날의 검이라는 것도 기억하세요. 여러분이 만든 프로그램도 누군가에게는 똑같이 잘 읽힙니다. 비밀번호 해시를 상수로 박는 일, API 키를 소스에 쓰는 일 — 오늘 여러분이 5분 만에 읽어 낸 것들입니다. 리버싱을 배운 눈으로 코드를 다시 보면, 지키는 코드도 달라집니다.


전부 체크되면 Step 222 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.