[카테고리:] 기술

  • 컴파일러 Intrinsic이 뭘까?

    Disclaimer

    이 글을 읽으면서 한번에 이해가 쉽지 않아도 좋다. 웬만한 컴퓨터 관련 학과 3-4학년쯤에 배울 내용을 커버하고 있기 때문이다.

    프로그래밍 언어(프로그래밍 언어의 설계 등을 배우는 과목이다), 컴파일러, 컴퓨터 구조, 알고리즘 및 계산이론에 대한 지식들을 망라하고 있기 때문에 관련 과목의 사전 지식이 있으면 한결 이해하기 쉽다.

    한줄 요약

    intrinsic은 어떤 소스 없이도 컴파일러와 아키텍쳐가 자체적으로 정의하는 함수이다. 다른 모든 함수들은 이 위에서 구현된다.

    여러분에게 컴퓨터는 무엇인가?

    상당히 추상적인 질문이다. 누군가는 이 질문에 대한 답을 ‘하드웨어와 소프트웨어로 구성되어 일련의 목적을 달성하기 위해 사용하는 전자기기’ 라고 할 것이고, 누군가는 ‘스마트폰, 데스크톱 컴퓨터, 태블릿 컴퓨터, 전자 오락기기, 랩탑 등을 망라하는 일련의 기기 집합’ 이라고 정의할 것이다. 모두 훌륭한 정의지만, 수학적인 학문의 관점에서 컴퓨터를 정의하면 다음 한 단어로 정리된다.

    증명기

    진짜다. 언어-논리 학술적인 관점에서는 저 단어 하나로 끝이 난다. 쉽게 납득하기 힘들 수가 있는데, 이 채널 사람들이 늘 하고 있는 프로그래밍으로 예시를 들어 보자.

    대부분의 프로그래밍 언어에서 가산 연산은 다음과 같이 서술한다.

    a + b

    이는 우리가 쓰는 대부분의 프로그래밍 언어가 다음과 같이 정의되기 때문이다.

    좌변과 우변에 실수 타입의 변수 및 상수로 두는 operator로 표현되며, 해당 값을 실수 가산 연산하여 결과로 갖는다.

    컴파일러는 이를 다음과 같이 증명한다.

    1. + 연산자를 찾았다. 정의에 따르면 좌, 우변에는 변수나 상수가 와야 하고, 각 타입은 실수여야 한다.

    2. 좌변, 우변 모두 상수가 아니다. 따라서 이 둘은 변수로 해석해야 한다.

    3. 좌변과 우변의 변수 a, b를 스코프(이것도 나중에 프로그래밍 언어 관련 글에서 다루겠다)에서 가용한 변수로 찾는다. 찾을 수 없거나, 타입이 명시된 것과 다르다면 증명에 실패한다. 따라서 컴파일하지 않는다.

    실제 컴파일러가 정확히 이 논리로 동작한다. 자바스크립트에서는 어떻게 동작할까?

    C++과 같은 강타입/묵시적 형 변환 비 친화적 언어와 달리, 자바스크립트는 형 변환에 친화적인 언어이다. 자바스크립트는 덧셈에 다음과 같이 정의에 세부사항을 추가하여 변형한다.

    좌변과 우변에 실수, 문자열, 배열, 부울 타입의 변수 및 상수를 갖는 operator로 표현되며, 각각의 경우 다음과 같은 결과를 가진다.

    1. 양 변이 실수인 경우, 해당 값을 실수 가산 연산하여 결과로 갖는다.

    2. 한 변에 실수, 다른 변에 부울 형식이 올 경우, true를 1, false를 0 으로 취급한다.

    3. 양 변에 부울 형식이 올 경우, true를 1, false를 0으로 취급한다.

    4. 한 변에 문자열, 다른 변에 부울 및 실수 형이 올 경우, 문자열과 다른 변수를 concat 한다.

    여기서 벌써 문제가 발생한다. C++과 같은 강타입, 변수 형 변환 금지 언어의 경우에는 상대적으로 이 증명과정이 쉬웠다. 하지만 자바스크립트의 경우 스코프 내에서 변수의 타입이 얼마든지 변화할 수 있다. 그렇다면 이걸 어떻게 증명할까?

    자바스크립트는 인터프리터 언어기 때문에 이것이 문제가 되지 않는다. 즉, C++과 같은 정적 컴파일 언어의 경우에는 위 증명 과정이 실행 전에 끝나야 하기 때문에 JS와 같은 코드를 작성할 수 없다. 논리적으로는 문제가 없으나, C++의 사용 목적 중 하나가 ‘메모리를 최대한 예측 가능하게 활용하는 것’으로 사용 하는 것이기 때문에 메모리 사용 크기를 컴파일 시점에 모두 유추하여 실행 시점에 공간 낭비를 최대한 줄여 활용할 수 있어야 하므로, JS와 같이 변수의 공간을 비효율적으로 사용하는 방법으로 언어의 구성 요소를 정의할 수도 있지만 그렇게 하지 않은 것이다.

    자바스크립트는 이와 같은 증명을 컴파일 시간이 아니라 실행 시점에 증명을 시도하며, 위에서 정의한 더하기 연산의 규칙에 런타임에서 위배되지 않는다면 그대로 실행해 준다. 그렇다면 다음과 같은 상황은 어떨까?

    function concatTest (input) {
      var a = '';
      for (var I = 0; I < input.length; I++) {
        a = a + input[I];
      }
      return a;
    }

    input은 아마도 배열일 것이다. 그렇다면 위 코드에서 for 문 안을 도는 동안 a 에 input의 구성요소들을 매번 concat하여 저장하므로 a의 타입은 항상 문자열일 것이므로, 위에서 이미 정의한 더하기 연산의 정의에 의해 언제나 a는 문자열일 것이다. for문을 마치면 a의 타입은 문자열이므로, 최종적으로 이 함수는 문자열을 반환할 것이라 예측할 수 있다.

    그렇다면 여기서 문제: 만약 input의 형이 실행시점에서 배열이 아니었다면?

    이 경우 위의 코드는 실행 시점에서 증명이 불가하다. 위의 코드가 실행 가능하기 위해서는 input이 iterable(반복 가능. 일반적인 언어에서 Map(Dictionary), Tuple, Array(List), Queue, Stack 등을 의미한다) 해야 하기 때문이다(자바스크립트에서 length 프로퍼티는 대개 iterable 타입에서만 정의된다). 따라서 input가 length라는 프로퍼티를 가져야 한다고 전재한 3행에서 증명을 실패한다. 증명에 실패했으므로 이는 곧 예외로 이어진다.

    즉, 우리가 프로그래밍 과정에서 늘 만나는 예외는 컴퓨터 공학의 관점에서 다음과 같이 정의할 수 있다:

    증명 가능하다고 전제된 코드(== 작성된 코드)와, 실제 동작 시점에서의 전제 사이의 모순

    이 결론을 이해하기 힘들 수도 있다. 네트워크 실패는? 입출력 시간 초과는? Hardware Failure은? 논리에는 문제가 없다고 볼 수 있지 않나?

    위 3개의 예시를 다음과 같이 바꿔 적어 보겠다

    1. 네트워크 실패로 인한 예외: 네트워크를 통해 통신한 결과가 정상적일거라는 전제를 세웠으나 이것이 충족되지 않음. 즉 전제가 잘못되어 모순 발생

    2. 입출력 시간 초과: 제한 시간 내로 기대하는 Input이 올 거라는 전제가 충족되지 않음. 즉 전제가 잘못되어 모순 발생

    3. Hardware Failure: 하드웨어가 정상 동작할 거라는 전제가 충족되지 않음. 즉 전제가 잘못되어 모순 발생

    이와 같이 컴퓨터에서 존제하는 모든 예외상황(오류, 예외, Failure 등으로 불리는)은 ‘모순 발생’ 이라고 정리할 수 있다. 이는 컴퓨터가 ‘증명기’ 이기 때문이다.

    아직 납득하기 힘든 점이 있을 수 있다. 컴퓨터가 증명기라고 치자. 그렇다면 화면에 무언가 표시되는 과정은? 소리가 나는 것은? 현금 출납기의 프로세서가 연산을 마친 후 현급 투입사출구에서 현금이 나오는 것은? 로봇의 프로세서가 연산을 마친 후 모터를 움직이는 것은? 이 모든 과정은 논리와는 상관이 없지 않나? 컴퓨터가 증명을 위해 존재하는 것은 아니지 않나?

    이 또한 다음과 같이 정리하면 쉽게 이해가 가능하다: ‘증명 이외 모든 동작은 컴퓨터에게 있어 부작용이다

    즉, 현금 출납기의 현금 처리, 모니터를 통한 화면 출력, 스피커를 통한 음향 출력 등 논리 증명과 무관한 모든 동작은 학술적 관점에서의 컴퓨터에게 있어 단지 부작용일 뿐이다. 다만 그 부작용이 인간에게 굉장히 유용할 뿐이다.

    한술 더 떠, 컴퓨터가 켜지고 꺼질 때 까지의 모든 동작도 결국 증명 과정이라고 정리할 수 있다. 펌웨어, 부트로더, 운영체제, 응용 전 과정을 거쳐 사용자에게 유의미한 ‘부작용’ 을 제공하고, 최종적으로 펌웨어에게 정상 종료를 반환하고 증명을 종료하는 것이 학술적 관점에서의 컴퓨터 동작과정이다.

    주) 더 관심 있는 사람은 정지 문제를 찾아보길 바란다. 주어진 프로그램의 증명에 걸리는 시간을 일반화하여 예측할 수 있는 방법이 존재하지 않는다는 것을 설명하는 문제이다. 가장 유명한 증명은 앨런 튜링의 증명, 최초의 증명은 앨런조 처치의 증명이다.

    증명은 하드웨어까지 이어져야 한다.

    소프트웨어 관점에서 지금까지 컴퓨터가 증명기인 이유를 보였다. 하지만 컴퓨터는 소프트웨어와 하드웨어로 구성된다. 소프트웨어에서의 논리 증명이 완벽하다고 한들, 이 증명이 하드웨어에서 실제로 구동되지 않으면 의미가 없다. 즉, 소프트웨어에서의 결론과 하드웨어 사이의 Missing link를 채워주는 것이 필요하다. 이 논리적 공백을 어떻게 채워야 할까?

    이 논리적 공백은 바로 컴파일러가 채워 준다. 하드웨어에서의 논리 증명을 맡는 것이 바로 프로세서, 컨트롤러 등의 연산장치이다. 이들은 기억장치 및 연결 신호에서 실행 가능한 바이너라와, operand 들을 입력으로 받아 결과를 산출한다. 이 때 이들이 해독 가능하며 실행 가능한 바이너리로의 전환, 즉 소프트웨어와 하드웨어에서의 실행 사이의 논리 공백을 해소하는 것이 바로 컴파일러인 것이다(인터프리터는 실행 시점의 컴파일러로 이해하면 편하다).

    컴파일러는 궁극적으로 다음과 같이 구성된다: 어셈블리로 직접 작성된 코드와 아키텍쳐별로 컴파일러 내에 구현되어 있는 함수

    어셈블리로 작성된 코드는 이해하기가 쉽지만 컴파일러 내에 직접 구현된 함수라는 말은 이해하기가 어렵다. 모든 언어별 표준 함수가 컴파일러 내에 구현된 것이 아닌가?

    놀랍게도 이 질문에 대한 답은, 아니오이다.

    C/C++의 stdio.h 헤더 파일에 정의된 함수의 실제 구현체를 보면, __로 시작하는 타입과 함수들, __asm__asm__ 등으로 가득 차 있는 코드를 볼 수 있다.  이 중 __asm__(GCC 기준, MSVC와 Clang에서는 __asm 확장으로 정의)은 아키텍쳐별 기계어로 직접 작성된 코드로, 컴파일러에 의해 0과 1의 연속으로 1:1 변환된다. 그럼 나머지 함수는 뭘까?

    __로 시작하는 함수들이 대개, 컴파일러가 어떤 헤더와 소스 파일 없이도 자체적으로 정의하고 있는 함수, 즉 intrinsic이다. 실제로 이 함수들도 거의 코드와 기계어 간 1:1 변환을 통해 0과 1의 연속으로 변환되는데, 모든 함수를 inline 어셈블리로 정의하는 것은 지나치게 가독성이 떨어지니 1:1 변환 과정을 쉽게 하기 위해서 내부적으로 함수라고 정의를 해 두고, 컴파일러가 나중에 기계어로 1:1 번역을 하는 것이 바로 intrinsic이다. 해당 함수들은 각 아키텍쳐별로 정의되며(x86-IA32및 x64-amd64는 인텔과 AMD가, ARMv7 및 ARMv8은 arm이 정의한다), 이들을 컴파일러들이 표준삼아 구현한다. 물론 컴파일러가 자체적으로 정의하기도 한다.

    때문에 같은 컴파일러라 하더라도, 아키텍쳐별로 intrinsic들은 다르게 정의되며, C/C++의 stdio.h도 컴파일러가 지원하는 하드웨어 벤더별로 각각 다른 인라인 어셈블리와 intrinsic들을 사용해 작성한다. 이와 같은 과정을 통해서 비로소 논리적으로 증명 가능한 코드가 하드웨어에서까지 증명 가능해지는 것이다.

    그런데 우리가 이들 코드를 쓸 필요는 없지 않나? 어차피 컴파일러가 다 알아서 해 주지 않나?

    물론 대개의 경우는 그렇다. 그런데, 게임 개발이나 운영체제, 커널 개발 등 하드웨어와 매우 밀접한 프로그래밍을 해야 할 때, 성능을 위해서 각 프로세서의 기능들을 언어의 추상화를 거치지 않고 직접 가져다 써야 할 때가 있다. 가령 벡터합 등 여러 oeprand의 연산을 한 개 명령어로 처리하는 경우가 대표적이다(인텔의 AVX 명령어셋, ARM의 NEON 명령어셋 등의 SIMD 관련 명령어셋이 대표적이다). 이와 같은 기능을 개발할 경우에는 실제로 intrinsic을 여러분이 직접 쓰게 된다.

    주) 벡터 명령어는 병렬 처리의 대표 사례 중 하나지만, 멀티코어 프로그래밍과는 개념이 좀 다르다. 멀티코어 프로그래밍은 프로그램을 여러 코어에 나눠서 처리하는 것이고, 벡터 명령어는 여러 개의 operand들을 한 개 코어가 동시에 처리하는 것이다.

    그렇다면 각 아키텍쳐별로 정의된 intrinsic들은 어디서 찾아볼 수 있을까?

    가장 유명한 문서 중 하나가 MSDN의 Compiler Intrinsic이다(관련 문서 중 제일 깔끔하다). 각 하드웨어 벤더가 정의한 intrinsic들의 MSVC 집합이 서술되어 있다. 이외에도 GCC(워낙에 문서 정리를 개판으로 해서 굳이 링크를 걸진 않겠다. 구글링하면 어차피 다 나온다), LLVM-Clang (다만 Clang은 모든 언어를 하드웨어 어셈블리하는 로직을 추상화하고 있는 LLVM 백엔드를 쓰고 있어서, LLVM, Clang 등의 문서를 모두 따로 보아야 한다), 인텔(x86 전체를 인텔이 관리하는 만큼 AMD가 자체 명령어를 제외하면 별도로 정리하고 있진 않다), ARM 등이 정리한 문서를 참조하면 필요 정보를 모두 찾을 수 있다.

  • 네이티브 응용 개발 환경별로 안전한 저장소를 알아보자.

    요약: 각 운영체제가 정의하는 Secure Storage API를 적극적으로 활용하자. iOS, macOS등 애플의 환경이면 Keychain, Android의 경우 KeyStore, Windows의 경우 CNG(Cryptography: Next Generation)가 있으나 구체적으로 파고들면 고민이 필요하다.

    어떻게 정보를 보호할까

    요약: 사용자 정보가 유출되지 않으려면 파일에 키를 저장하는 수준으로는 충분하지 않다

    개발을 하다 보면 한번쯤 이런 생각을 해본 사람들이 있을 것이다.

    “인증 토큰을 어떻게 저장해야 하지?” 라든가 “노출되면 안되는 사용자의 정보를 어디에다가 저장해야 하지?” 와 같은.

    이런 개발 단 요구사항들의 공통점은 다음과 같이 요약할 수 있다:

    1. 인가되지 않은 읽기(혹은 더 광범위하게 수정을 포함해서 접근 자체)를 거부
    2. 인가되지 않은 수정이 발생했을 경우 이를 탐지

    가장 쉽게 생각할 수 있는 것은 암호화이다. 실제로 암호화가 바로 이와 같은 목적을 달성하기 위해서 생긴 것이니. 키를 알 수 없는 외부 공격자는 정보를 읽어도 유의미한 정보를 읽어낼 수 없으며, 키를 모르는 상태에서 임의의 방법으로 정보를 수정했을 때는 복호화가 진행되지 않는다. 따라서 키를 안전하게 보관하는 경우 공격자로부터 중요한 정보를 안전하게 보호할 수 있다.

    문제는 여기서 발생한다:

    과연 어떻게 키를 안전하게 보관할 것인가?

    메모리 단에 저장하는 경우에는 문제가 간단하다. 대부분의 운영체제는 메모리 Scope를 하드웨어의 도움을 받아 안전하게 보호한다. 물론 메모리 보호를 응용 단에서 추가로 해야 하는 많은 경우가 있지만, 이와 같은 개발을 직접 하는 것은 배보다 배꼽이 더 커지게 되므로(또 이를 보다 완전한 통제 하에 두기 위해서는 사실상 운영체제의 응용 동작 하위 레벨에서 작동하는 코드를 작성해야 하므로) 일반적인 경우에는 운영체제의 메모리 보호 자체를 신뢰하는 것 만으로 충분하다.

    그런데 위에서 말한 인증 토큰과 같은 경우를 생각해 보자. 만약 매번 응용을 실행할 떄마다 사용자의 로그인 작업을 반복해야 한다면? 엔지니어의 고민은 손쉽게 해결되겠지만 사용자들은 불만을 터뜨릴 것이다. 이는 비즈니스를 운영하는 데 있어서는 치명적인 문제점이다.

    그렇다면 메모리의 내용물이 유실되지 않도록 응용을 계속해서 켜 두는 것은 어떨까? 데스크톱 컴퓨터를 유저들이 지속적으로 켜 두도록 강제할 수도 없고, 더욱이 모바일 환경 운영체제들은 대개 이와 같은 작업을 허용하지 않는다. 운영체제 입장에서는 메모리를 좀먹는 암덩어리 같은 존재거니와, 메모리 내용물을 유지하기 위해서는 대개 전력 사용이 필수적이라는 것을 잊지 말자. 따라서 이는 현실성이 없는 안이다.

    그렇다면 보다 현실적인 방법은, 이와 같은 정보들을 함부로 가져올 수 없는 장기 저장공간에 저장하는 것이다. 하지만 여기서 문제가 생긴다. 그런게 어디 있단 말이지?

    Seruce Storage 만세

    요약: 하드웨어의 기능을 사용해 돌아가는 보안 표준들은 충분히 안전하다. 쉬운 이해를 위해 애플의 구현을 가져왔다.

    다행스럽게도, 구글의 안드로이드와 애플의 운영체제들은 이를 위한 표준들을 제공한다. 안드로이드의 경우 이를 Keystore라고 부르며, 애플의 경우 Keychain이라고 부른다.

    안드로이드의 경우 운영체제는 표준을 제공할 뿐이지 실제 해당 표준을 하드웨어 벤더별로 구현하는 방법은 상이하므로, 하드웨어와 소프트웨어를 동시에 통제하는 애플의 경우를 먼저 보도록 하자.

    우선 애플의 키 보안 모델은 다음과 같이 구현된다:

    1. 하드웨어 보안 무결성 검증: 프로세서의 실행 코드 검증 및 Secure Enclave로 대표
    2. 운영체제 자체의 메모리 보안
    3. 파일 권한 모델
    4. 어플리케이션 서명
    5. 암호화 가속

    이 아래로는 귀찮으니 위에서 영어로 표기한 것들을 그냥 한글로 적도록 하겠다.

    애플이 제공하는 키체인 API는 오로지 키를 저장하기 위해서 사용하는 데이터베이스를 운영한다. 해당 정보가 어디 저장되는지 궁금할 텐데, 애플도 세부적으로 공개하진 않았지만 기본적으로는 시큐어 인클레이브 안에도 비휘발성 저장소가 존재하며, 이 안에 모든 민감 정보를 저장할 수는 없기에 컴퓨터의 저장 공간도 활용한다. 하지만 해당 공간들에는 시큐어 인클레이브를 통하지 않고는 접근할 수가 없다. 또한 해당 데이터베이스는 응용과 사용자 단위로 접근할 수 있는 범위가 제한되어 있다. 따라서 기본적으로 정해진 앱과 사용자가 아니면 해당 정보들을 읽는 것도, 수정하는 것도 모두 불가능하다. 이 챈 사용자들이 자주 다루는 파일과는 아주 다른 계층에 저장되는 정보라는 뜻이다.

    그렇다면 해당 하드웨어에 비인가 상태로 접근하려고 하면 어떤 일이 벌어질까? 당연하게도, 정해진 수를 넘어서 접근하면 해당 저장소가 모두 잠긴다. 시큐어 인클레이브에 브루트포싱을 시도하더라도 정해진 횟수 이상으로 키를 틀리면 킬스위치가 하드웨어 단에서 올라가니, 나노미터 단위의 정확도의 손을 가진게 아니라면 꿈꺠는 것이 좋다. 이는 삼성 Knox에서도 구현된 사항이다. 이를 해제하기 위해서는 시큐어 인클레이브를 초기화 해야 하는데, 그 과정에서 저장된 모든 정보는 증발한다. 정보를 털릴 바에야 모두가 함께 자살하겠다는 데이터 논개와 같은 태도다.

    혹시라도 애플 디바이스의 다른 부품들로부터 분해하고 재조립해 다양한 공격 시도를 하는 것이 가능하지 않은가 싶은가? 이것도 꿈 꺠는 편이 좋다. 이럴 걸 예상하고 시큐어 인클레이브에는 하드웨어 무결성 검사가 있는데, 어려우니 간단히 말하면 애플이 인가한 하드웨어 식별자 조합이 아니면 동작을 거부한다는 소리다. 아이폰 사설 수리점에서 부품을 교체할 경우 경고가 뜨는 걸 본 적이 있을 텐데, 이 중에서도 보안 시스템에 핵심적인 부품들이 교체되면 디바이스 전체가 동작을 중단한다. 결국 시큐어 인클레이브가 잠겼다면 남은 방법은 오로지 전체 저장소를 초기화하는 것 뿐이다. 이때도 아이클라우드 분실 모드 정보 등은 증발하지 않기 때문에 디바이스가 탈취 되었을때 이를 재활성화 하는 것은 오로지 디바이스 단에서 분실 키를 입력해 시큐어 인클레이브 잠금모드를 해제하는 것 뿐이다.

    운영체제의 추가타

    요약: 운영체제와, 운영체제가 요구되는 하드웨어단 보안 피쳐는 충분히 안전하다. 잘 활용하자.

    아예 상식을 넘어선 방법으로 해당 공간에 접근하는 것은 불가능하다는 것을 알았다. 그렇다면 공격자의 남은 시나리오에는 어떤 것이 있을까? 그것은 바로 운영체제 자체의 동작에 개입하거나, 원하는 키에 접근할 수 있는 앱의 로직을 수정하거나, 앱이 구동중인 경우 해당 앱의 메모리 공간에 침범하는 방법이 있겠다.

    우선 메모리 공간 침범 문제는 간단히 해결된다. 프로세서와 운영체제가 기본적으로 인가되지 않은 메모리 수정을 방어해준다. 인가되지 않은 수정이 발생했을 때 앱은 꺼지거나 커널 패닉이 일어난다. 심각할 경우에는 마찬가지로 시큐어 인클레이브 락이 걸린다. 운영체제의 동작에 개입하는 것은 앱의 메모리 보호에 적용되는 소프트웨어-하드웨어 조치에 좀 더 많은 장치가 걸리게 되므로 물론 더 어렵다.

    그렇다면 앱을 수정하는 것은 어떨까? 키를 키체인 API에서 가져온 다음 메모리 어딘가에 저장해서 사용할테니 말이다. 거기에 원격으로 키를 전송하거나 디바이스 내 어딘가에 저장하는 코드를 심을 수 있지 않을까? 안타깝게도 이는 앱 사이닝에 의해 막힌다. 인가되지 않은 앱 수정이 일어난다 해도, 앱스토어를 통해 출시된 모든 앱에는 해당 앱이 개발자에 의해 작성된 이후에 수정된 것이 아닌지 검증하는 값들이 달리는데, 이 키는 개발자와 애플이 소유하고 있기 때문에 해커가 해당 키를 알아내지 않는 한 마음대로 앱 로직을 수정한다면 운영체제가 해당 앱 전체의 실행 자체를 거부한다. 앱 스토어가 아닌 곳에서 앱을 다운받아 실행하기 위해서는 탈옥이 필요했던 대표적인 이유다. 탈옥한 iOS는 바로 이 부분이 비활성화되거나 완화되기 떄문이다.

    다른 회사들의 구현

    요약: 윈도우도, 안드로이드도 애플의 정보 보호에 대한 충분히 안전한 대체재들이 존재한다. 다만 윈도우의 경우 앱 변조에 유의하자.

    안드로이드에서 구현된 키스토어도 기본적으로 위와 비슷한 동작들을 하는데, 단지 제조사별로 구체적인 펌웨어에서의 소프트웨어적인 구현, 해당 사항들을 구현하기 위한 하드웨어 구성요소들이 상이할 뿐이다. 대표적인 게 삼성의 녹스인데, Secure Enclave와 유사하게 하드웨어 무결성 검사, 킬스위치, 암복호화 가속 등이 달려 있다.

    윈도우의 경우에는 조금 어렵다. 레거시를 위해서 기본적으로 이전부터 쓰던 방법에는 Protected storage(서버 2003, XP 이후) 나 DPAPI(Data Protection API, 윈도우 2000 이후) 등이 있는데 위 운영체제들과는 달리 정보들이 단순한 파일로 저장되고, 비밀번호로 유래되는 마스터 키 하나로 전체 정보를 해독할 수 있으며, 앱별로 접근을 제한하는 방법이 없기 때문에 위에 비해서는 안정성이 퇴색되는 감이 있다. 이건 마소가 멍청하거나 보안에 무신경해서 그런 게 아니라 윈도우의 레거시 역사가 너무 길기 때문에 현대화된 보안 정책이 수립되기 전에 만들어진 앱이나 하드웨어 환경들을 버릴 수가 없기 떄문이다.

    일단 API 자체는 윈도우의 Cryptography Next Generation API를 사용할 수 있으며, 저장소로 다행히도 TPM이 설치된 경우에는 이를 해당 API 구동에 사용한다. 또한 앱 로직과 에셋을 보호하기 위해서는 윈도우 8 이후로 도입된 패키징을 사용해 배포하거나, 기존 win32 환경에서 개발된 로직에 Windows appcontainer를 사용하면 비슷한 수준의 보안 이점을 누릴 수 있다. 윈도우 스토어를 통해 설치된 앱들은 외부에서 접근이 차단된 C:\Program Files\WindowsApps 디렉터리 안에 저장되는데, 기본적으로 외부에서 접근할 수 없다. 또한 앱 간에도 상호간의 파일에 마음대로 접근할 수 없으며, 가상화된 샌드박스 안에서 동작한다(VM 수준은 아니지만 보안 관점에서는 충분한 환경 격리를 제공한다). 이것이 불가한 경우에는 차선책으로, 애플의 앱 환경과 마찬가지로 로직 변조를 막기 위해서 인증서로 앱을 사이닝하면 되는데… 이게 비용이 좀 들어갈 거다(운영체제별 앱 스토어는 이 과정을 개발자 등록을 하면 자체 인증서로 싸게 대신해준다). 아무튼 앱 변조를 방어해낸 환경에서 CNG나 이에 상응하는 다른 기능들을 사용하면 충분히 안전하게 정보를 저장하고 읽을 수 있다. 핵심은 가능한 한 보안 하드웨어 표준을 사용하는 방향(대표적으로 TPM과 연관된)의 기능들을 활용하는 것이다. 윈도우는 개발 환경별로 이런 게 너무 많으니 알아서 찾아 쓰면 될 것 같다.

    그럼에도 불구하고 다른 운영체제들에 비해 윈도우의 보안 정책이 미흡한 것은 사실이라 마소가 보안 쪽에서 무능한 회사인 것처럼 느껴질 수 있는데, 그렇지 않다. TPM은 중요한 키들을 저장하고, 암복호화를 가속하기 위해서 사용되며 SecureBoot는 하드웨어 무결성을 검증하는 역할을 하는데 모두 윈도우 8부터 지원한다. 게다가 마이크로소프트는 애플의 시큐어 인클레이브나 삼성의 녹스에 대응되는 Pluton이라고 하는 하드웨어를 자체적으로 설계했는데, 대표적으로 이게 탑재된 하드웨어가 바로 엑스박스 원 이후의 엑스박스이다. 그리고 이건 어느정도 우회법이 찾아진 시큐어 인클레이브나 삼성 녹스와도 차원이 다른 물건이다. 그걸 어떻게 아냐고? 엑스박스 360이야 파훼되었지만(하드웨어 보안도 없는 상태에서 이 파훼조차도 부분적이다. 마소의 소프트웨어 보안 짬밥이 나오는 부분이다), 플루톤이 탑재된 원 이후로는 해적판 앱들을 깔아서 실행하거나 간단한 정보라도 탈취한 애들이 없기 때문이다. 쉽게 말해서, 위에 적은 놈들보다 더 지독한 녀석이다. 마소가 바보가 아니라 맘만 먹으면 레거시 좀 희생하고 본인들이 통제하는 현대화된 보안 모델로 갈아버리는 건 전혀 불가능한 게 아니란 이야기이다. 단지 아직까지는 레거시 지원떄문에 참고 있을 뿐이다.

    관련 문서

    네이티브

    크로스플랫폼 네이티브 앱용 패키지

    안타깝게도 Avalonia나 Tauri를 위한 패키지는 찾지 못했다. 찾거나 알려주면 글에 추가해 두겠다.

    마무리

    쓰다 보니 글이 꽤나 어렵게 써졌다. 모두들 사용자 민감정보 보안에 신경쓰면서 개발하자.