[SQLite fts3] fts3_tokenizer 취약점

두비니

·

2021. 7. 27. 01:03

 


fts3_tokenizer


 

0. fts3_tokenizer에 대해서

 

일단 이 친구가 뭐하는 친구인지 봅시다. FTS란 Full Text Search의 줄임말로, 효율적으로 빠르게 문서(특히 대용량)을 검색하기 위해 구상된 Table입니다. 무식하게 찾으면 시간이 무진장 오래 걸리니깐 최대한 최적화시켜놓은 시스템이라고 보는게 가장 편할 것 같네요. 

그래서 그 검색을 편하게 해주는 것 중 하나가 Tokenizers입니다. Tokenizer는 문장에서 단어를 어떤 방식으로 잘라낼지에 대한 방법을 지정합니다. 보통은 문장에서 공백(' ')을 기준으로 token을 만들겠지만 특정 언어에 맞추어진 custom tokenizer라고 생각하시면 좋을 것 같습니다.

 

기본적으로 제공하는 tokenizer는 다음과 같습니다.

  • unicode61 (기본값)
  • ascii
  • porter

인자는 순서대로 이름, 두 번째부터는 tokenizer implementation에 이용되는 값들입니다. 사용 방법은 다음과 같습니다.

 

-- The following are all equivalent
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = 'porter ascii');
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = "porter ascii");
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = "'porter' 'ascii'");
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = '''porter'' ''ascii''');

-- But this will fail:
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = '"porter" "ascii"');

-- This will fail too:
CREATE VIRTUAL TABLE t1 USING fts5(x, tokenize = 'porter' 'ascii');

출처 : https://www2.sqlite.org/fts3.html

 

그래서 이런식으로 전체 텍스트 인덱스를 사용하면 테이블에 여러 개의 큰 문서가 포함되어 있어도 하나 이상의 단어(토큰)가 포함 된 모든 행에 대해 쉽게 끌어올 수 있다보니깐 효율적으로 쿼리를 할 수 있습니다.

 

 

1. sqlite3_tokenizer_module 구조체 보기

struct sqlite3_tokenizer_module {
  int iVersion;
  
  int (*xCreate)(
    int argc,                           /* Size of argv array */
    const char *const*argv,             /* Tokenizer argument strings */
    sqlite3_tokenizer **ppTokenizer     /* OUT: Created tokenizer */
  );
  
  int (*xDestroy)(sqlite3_tokenizer *pTokenizer);

  int (*xOpen)(
    sqlite3_tokenizer *pTokenizer,       /* Tokenizer object */
    const char *pInput, int nBytes,      /* Input buffer */
    sqlite3_tokenizer_cursor **ppCursor  /* OUT: Created tokenizer cursor */
  );

  int (*xClose)(sqlite3_tokenizer_cursor *pCursor);

  int (*xNext)(
    sqlite3_tokenizer_cursor *pCursor,   /* Tokenizer cursor */
    const char **ppToken, int *pnBytes,  /* OUT: Normalized text for token */
    int *piStartOffset,  /* OUT: Byte offset of token in input buffer */
    int *piEndOffset,    /* OUT: Byte offset of end of token in input buffer */
    int *piPosition      /* OUT: Number of tokens returned before this one */
  );

전체적으로 이렇습니다. 그냥 전체적인 구조 자체만 보면 될 것 같습니다.

그래도 나중에 취약점 발현 과정에 있어서 이해하기 좋게 xCreate 콜백에 대해서는 파악을 하고 가겠습니다.

  /*
  ** Create a new tokenizer. The values in the argv[] array are the
  ** arguments passed to the "tokenizer" clause of the CREATE VIRTUAL
  ** TABLE statement that created the fts3 table. For example, if
  ** the following SQL is executed:
  **
  **   CREATE .. USING fts3( ... , tokenizer <tokenizer-name> arg1 arg2)
  **
  ** then argc is set to 2, and the argv[] array contains pointers
  ** to the strings "arg1" and "arg2".
  **
  ** This method should return either SQLITE_OK (0), or an SQLite error 
  ** code. If SQLITE_OK is returned, then *ppTokenizer should be set
  ** to point at the newly created tokenizer structure. The generic
  ** sqlite3_tokenizer.pModule variable should not be initialized by
  ** this callback. The caller will do so.
  */

이건 소스코드에 있는 설명을 가지고 온 것입니다. xCreate()는 기본적으로 tokenizer를 새로 생성하는 과정이고, 이 함수가 실행이 되면, tokenizer에 접근하는 과정에서 이 포인터 값이 실행되는 것입니다. 그렇게 설정을 하고, 성공/실패 여부를 리턴합니다.

 

2. fts3_tokenizer 취약점

이 fts3_tokenizer는 custom으로 지정을 할 수 있다는 점에서 취약점이 발생합니다. 

기본적인 로직은 다음과 같습니다.

 

1. 인자를 하나만 전달할 경우

SELECT fts3_tokenizer(<tokenizer-name>);

현재 등록되어있는 tokenizer가 구현된 pointer가 return

 

2. 인자를 두 개 이상 전달할 경우

SELECT fts3_tokenizer(<tokenizer-name>, <sqlite3_tokenizer_module ptr>);

새로 입력한 값이 tokenizer로 등록되고, 그 사본이 return

 

 

 

자, 그럼 이렇게 값을 집어넣으면 어떻게 될까요?

SELECT hex(fts3_tokenizer('simple'));

이러면 밑의 실습에서도 확인하겠지만, 해당 포인터의 값이 빅엔디안 방식으로 출력이 됩니다. 이게 첫 번째 취약점이에요.

뭐 실제로 주소를 쓰려면 리틀엔디안으로 바꿔야 한다는 점이 있지만, 그게 대수가 아니쥬

 

한 발 더 나아가 이렇게 하면 어떻게 될까요?

SELECT fts3_tokenizer('simple', x'4141414141414141');
CREATE VIRTUAL TABLE vt USING fts3 (content TEXT);

지금 두 번째 인자값을 그냥 주소로 넣어버린 모습입니다. 이렇게 할 경우, tokenizer의 주소가 아예 '0x4141414141414141'로 바뀌어버립니다. fts3_tokenizer는 콜백 함수 포인터의 유효성을 검증하지 않기 때문에, 두 번째 취약점인 Untrusted pointer dereference가 발생합니다.

아무튼 그런 상태에서 두 번째 명령어를 실행시킨다면? 0x4141414141414141에 접근하려고 노력하면서 프로그램이 segfault를 띄우게 됩니다.

 

이제 이게 저런 더미값이 아니라 함수의 주소라면?🤔🤔 a bit interesting...

이걸 이용한 CVE가 CVE-2015-7036입니다. 벌써 햇수로 6년이 지난 취약점인데, 으렵네여,,

 

3. 실습

[REDACTED]

 

 

 

참고

 

https://www2.sqlite.org/fts3.html

https://tourspace.tistory.com/395 << 참고로 이글 너무 잘 정리되어있습니당 시간나면 꼭 보시길.. (fts5기준인거 참고)

https://www.youtube.com/watch?v=N6MAhXfuv74&t=8s 21분즈음

https://www.blackhat.com/docs/us-17/wednesday/us-17-Feng-Many-Birds-One-Stone-Exploiting-A-Single-SQLite-Vulnerability-Across-Multiple-Software.pdf