OverTheWire_Wargame Leviathan

[ level7 ]




OverTheWire Leviathan 끝

OverTheWire_Wargame Leviathan

[ level6 -> level7 ]



이번 레벨에도 어김없이 setuid bit가 걸려 있는 바이너리가 버티고 있습니다. 실행을 해 보니 사용 방법을 친절하게 알려줍니다. 사용법대로 입력 해 보니, 값이 틀렸다고 오류 메시지를 출력해주고 프로그램을 종료시킵니다. 아마 지금까지의 leviathan 문제를 풀어본 결과 gdb로 까보면 바로 답이 있을 것 같습니다.



자, 일단 main 함수를 disassemble 한 상태입니다. 맨 처음 esp+0x1c에 0x1bd3을 집어 넣어주네요. 이게 답인듯 합니다. 0x1bd3 = 7123

그리고 바로 아래에서 argc가 2가 아닐 경우 오류 메시지(사용법)를 출력하고 프로그램을 종료시키네요.

그 다음 입력받은 값을 int형으로 바꿔주는 atoi 함수를 호출하고, 그 값과 [esp+0x1c](0x1bd3)을 비교하고, 값이 다를 경우 오류 메시지(Wrong)를 출력하고 프로그램을 종료시키네요.

만약 옳은 패스워드를 입력 했을 경우에는 system 함수를 호출하여 쉘을 띄워주는 것 같습니다.



상수 값을 확인해보니 /bin/sh으로, system 함수는 쉘을 떨어뜨려주는 것으로 확인이 되었습니다.

그럼 우리가 확인한 0x1bd3(7123)을 프로그램으로 집어 넣어줍시다.



깔끔하게 쉘이 떨어졌고, 패스워드를 출력 해줍니다.

OverTheWire_Wargame Leviathan

[ level5 -> level6 ]




자, 벌써 level5입니다. 홈 디렉토리를 확인하니 leviathan5라는 바이너리가 딱 버티고 있습니다.

권한을 확인 해 보니 이번에도 setuid bit가 걸려있네요. leviathan6 권한으로 실행이 되나봅니다. 파일을 실행 해 보니 "Cannot find /tmp/file.log"라는 오류 메시지를 내뱉고 프로그램을 종료시킵니다. 어떤 이유인지 확인하기 위해 gdb로 까보도록 하죠.



main 함수를 disassemble 한 상태입니다. 맨 처음에 fopen으로 파일 하나를 오픈 한 후, 해당 파일이 없으면 오류메시지를 내뱉고 프로그램을 종료시킵니다.

만약 파일이 있을 경우에는 fgetc와 feof 함수를 이용해 파일의 끝까지 이동하면서 한 바이트씩 읽어가며, 도중 putchar을 이용해 읽었던 값을 출력해줍니다.

그리고 eof에 도달하면 반복을 종료하고, getuid, setuid 함수를 이용해 현재 함수의 uid를 얻어와 setuid시키며 0x8048722에 들어있는 문자열을 인자로 unlink합니다.

fopen 할 때 인자로 전달하는 값 중 첫 번째 인자와 unlink 할 때 전달하는 인자가 동일합니다. 즉, fopen 하는 파일은 이 프로그램을 실행시키고 끝까지 실행됨과 동시에 삭제가 된다는 소리가 되겠네요.

상수 값을 분석해 본 결과, fopen의 두 번째 인자는 "r", 첫 번째 인자는 "/tmp/file.log"였습니다. 따라서 /tmp/file.log 파일을 읽어서 fgetc로 한 글자씩 저장하고, 출력하는 행위를 eof를 만날 때 까지 한다는 소리가 되겠네요.

그렇다면 /etc/leviathan_pass/leviathan6를 /tmp/file.log에 심볼릭 링크하여 leviathan5 프로그램을 실행시키면 해결 될 것 같습니다.



leviathan6로 통하는 패스워드를 아름답게 내뱉어주네요.

OverTheWire_Wargame Leviathan

[ level3 -> level4 ]




이번 문제는 trash 폴더가 숨김 속성으로 꽁꽁 숨어있었습니다. 해당 폴더로 들어가서 ls를 때려주면 bin이라는 바이너리가 존재하는데, 실행시켜보니 다수의 2진코드들이 나타났습니다. 일단 gdb로 분석 해 보도록 하죠.



일단 시작하자마자 fopen으로 파일 하나를 열어둡니다. 그리고 fgets 함수로 어떤 값을 받네요. 그리고 putchar 함수가 있는 것을 보니, putchar 함수를 통해 한 바이트씩 출력을 해 주는 것 같습니다.

fopen을 호출 할 때 상수값을 이용하여 인자를 입력받기 때문에 해당 상수에 어떤 값이 들어있는 지 확인해보도록 합시다.




fopen 함수는 f = fopen("/etc/leviathan_pass/leviathan5", "r"); 와 같이 실행이 된 모양입니다.

그리고 곧바로 if함수로 해당 FILE 포인터(ex. f)가 존재하지 않을 경우(파일이 존재하지 않을 경우) 프로그램을 즉시 종료시킵니다.

정상적으로 파일이 열렸을 경우에는 fgets로 값을 입력받는데, if문에서 FILE 포인터를 검사 할 때 사용했던 [esp+0x1c]를 다시 사용하는 것을 보니, leviathan5로 통하는 패스워드가 들어있는 파일을 읽어내는 듯 합니다.

분석해보면 fgets(buffer, 256, f); 와 같이 사용을 한 것 같습니다.

그리고 아래쪽으로 jmp 등으로 분기하는 부분이 여럿 있는데, 디버깅 능력이 부족하다 보니 이것까지 분석을 하지는 못했습니다.

하지만 putchar 함수를 이용하여 1바이트씩 찍어내는 부분이 있으며, 따로 암호화 하는 부분은 보이지 않기 때문에 해당 파일의 내용을 1바이트씩 출력한 것으로 예상하고 값을 찾아냈습니다.


01010100 : 0x54 : T

01101001 : 0x69 : i

01110100 : 0x74 : t

01101000 : 0x68 : h

00110100 : 0x34 : 4

01100011 : 0x63 : c

01101111 : 0x6f : o

01101011 : 0x6b : k

01100101 : 0x65 : e

01101001 : 0x69 : i

00001010 : 0x0a : 개행


Tith4cokei


Leviathan5 계정의 패스워드로 위 문자열을 집어 넣었더니 정상적으로 접속이 되었습니다.





OverTheWire_Wargame Leviathan

[ level3 -> level4 ]


Leviathan3로 접속을 하면 홈 디렉토리에 level3 바이너리가 우릴 기다리고 있습니다. 해당 바이너리는 leviathan4 권한으로 setuid bit가 걸려있기 때문에 이번 레벨은 level3 바이너리로 풀어내면 될 것 같습니다. 한 번 실행 시켜 보도록 하죠.



바이너리를 실행시키면 패스워드를 요구하는데, 패스워드가 틀렸을 때 오류 메시지를 내뱉고 프로그램을 즉시 종료시킵니다.

긴 문자열을 보내도 segmentation fault같은 메시지를 안 내뱉는 것을 보니 BOF 유형은 아닌 것 같아 보입니다. gdb로 분석해보도록 합시다.



main 함수를 disassemble 한 상태입니다. 중간에 보면 printf로 0x804878f에 들어 있는 정보를 출력합니다. 확인 해 보니 해당 문자열은 "Enter the password> " 였습니다. 이 문자열은 우리가 바이너리를 실행시켰을 때 패스워드를 입력받기 직전에 내뱉는 문자열이었습니다. 따라서 그 이후에 진입하년 do_stuff 함수에 패스워드를 입력 및 비교하는 루틴이 들어 있을 것이라고 예상 할 수 있겠습니다.

그렇다면 do_stuff 함수도 분석 해 보도록 합시다.



중간에 보면 strcmp 함수를 호출하는 것을 볼 수 있습니다. 또한, strcmp 이후 if문의 조건을 통해 루틴을 분기시키는 부분이 있네요. strcmp를 통해 패스워드 일치/불일치 여부를 확인 한 후 이 if문에서 프로그램을 종료시킬 지, 다음 루틴으로 넘어갈 지 판별하는 듯 합니다.

또한, 조금 더 아래쪽을 살펴보면 system 함수가 호출되는 것을 확인 할 수 있는데, 위의 mov 명령을 통해 esp에 들어있는 주소의 문자열을 인자로 함수를 실행시킵니다.

strcmp 함수와 system 함수의 인자에는 어떤 값들이 전달되는 지 확인해보도록 합시다.



strcmp 함수의 1, 2번 인자는 우리가 입력한 문자열과 "snlprintf"입니다. 즉, 우리가 입력한 문자열이 snlprintf일 경우 프로그램을 종료시키지 않고 다음 루틴을 실행한다는 뜻이 되겠죠. 그리고 값이 동일할 경우 0x8048760에 들어있는 문자열을 출력 한 후 0x8048774에 들어있는 문자열을 인자로 system 함수를 실행시킵니다. 0x8048760은 "[You've got shell]!", 0x8048774는 "/bin/sh"인 것을 보니, snlprintf를 입력받았을 경우 전자의 문자열을 출력하고 쉘을 띄워주는 프로그램인 것 같습니다. gdb를 종료하고 프로그램을 실행시켜봅시다.



leviathan4로 통하는 패스워드를 깔끔하게 뱉어줍니다.



OverTheWire_Wargame Leviathan

[ level2 -> level3 ]




레벨2에는 printfile이라는 바이너리가 자리 잡고 있습니다. 레벨1과 마찬가지로 setuid bit가 걸려있기 때문에 이 파일을 이용해서 레벨3로의 패스워드를 알아 내야 할 것입니다.

gdb로 한번 까 보도록 하겠습니다.



먼저 main을 disassemble 한 상태입니다.

위쪽은 파라미터 갯수 검증하는 부분이고, 빨간 체크박스부터 살펴보면 access 함수를 호출하는 것을 알 수 있습니다.

access 함수의 원형을 살펴보죠.

int access(const char *pathname, int mode);

2가지 인자를 받는데, 경로를 포인터로 받고 mode 또한 숫자 또는 상수로 받습니다.


R_OK(0x01)

 읽기 권한 여부

 W_OK(0x02)

 쓰기 권한 여부

 X_OK(0x03)

 실행 권한 여부

 F_OK(0x04)

 파일 존재 여부


함수 호출 규약에 따라 먼저 들어간 0x04가 mode, eax가 *pathname이 되겠군요.

스택에서 한번 어떤 값이 들어가는 지 살펴보도록 하겠습니다.



eax에 들어가는 값을 따라가 보니 argv[1]에 우리가 넣어줬던 파일 경로의 값이 들어갑니다.

그렇다면 여기서 호출하는 access 함수의 목적은 파일이 존재하는지 존재하지 않는지의 여부를 파악하고, 만약 존재하지 않을 경우 프로그램을 종료시키는 것이 될 것입니다.

사실 여기서 바로 /etc/leviathan_pass/leviathan3 로 넣어주면 깔끔하지 않느냐?

라는 질문이 있을 수도 있는데, 이유는 모르겠지만 그렇게 넣어주면 파일 미존재로 인해 프로그램이 종료되었습니다. 뭔가 다른 장치를 해 놓은 듯 합니다.

혹시 아시는 분이 계시면 댓글 부탁드립니다.


자, 그럼 이제 access 함수 이후에 어떤 행동을 하는지 더 살펴보도록 하죠.



access 함수 이후에 snprintf 함수를 호출하는데, sprintf와 동일하지만 버퍼의 길이를 제한하여 넣어준다는 차이가 있습니다.

이 함수는 대충 이런 모양이 될 것 같습니다.


snprintf(buffer, 0x1ff, "/bin/cat %s", argv[1]);


우리가 넣어 준 파일 경로 앞에 /bin/cat을 넣어주고 buffer 변수에 넣는거죠.

그리고 마지막엔 system함수의 인자로 buffer을 넣어줘서 결국 "/bin/cat 파일"을 실행하는 것입니다.

자, 여기까지 이해를 했다면 어떤식으로 문제를 풀어야 할 지 감이 오지 않나요?



/tmp/ 경로에 fstr3am_2라는 디렉토리를 만들어주고, 그 곳에 ".bashrc;sh"라는 이름의 파일을 하나 던져줍니다.

세미콜론 앞에 오는 문자열은 어떤 문자가 오던 간에 상관은 없습니다.

이렇게 한 이유는 access 함수를 우회하기 위해 실제로 존재하는 파일을 생성해주는것이고, 파일명을 /bin/cat에 붙여주기 때문에 결과적으로 버퍼에 들어가는 문자열은 아래와 같이 될 것입니다.


"/bin/cat .bashrc;sh"


이 프로그램에서는 실행을 위해 system 함수를 이용하기 때문에 system("/bin/cat .bashrc;sh") 라는 명령어가 완성이 되는군요.

결과적으로 /bin/cat .bashrc, sh 두 가지의 명령어가 따로 실행이 되는것입니다.



깔끔하게 쉘을 떨어뜨렸습니다. 권한이 leviathan3로 정상적으로 상승된 것을 확인하고, leviathan3의 패스워드를 뱉어내도록 합시다.

OverTheWire_Wargame Leviathan

[ level1 -> level2 ]




Level1입니다. ls를 때려보니 check라는 바이너리가 떡 하고 자리 잡고 있네요.

게다가 leviathan2의 id로 setuid가 걸려있습니다. 이 check 바이너리를 가지고 요리 하면 level2의 패스워드를 획득 할 수 있을것만 같습니다.

gdb로 까보도록 하죠.



check 바이너리를 실행했을 때 password가 맞지 않으면 에러를 내뱉는 구조이기 때문에 strcmp는 당연히 들어있을 것입니다.

main 함수를 disassemble 했을 때 strcmp@plt의 위치를 잡고 breakpoint를 걸어주도록 합시다.

그리고 strcmp의 구조 상 스택에 들어가는 순서는 source, destination, function call 입니다.

그러므로 자연스럽게 $esp+0x18(=$esp+4)는 src, $esp+0x14(=$esp)는 dst가 될 것입니다.

스택을 확인하면서 보도록 합시다.



일단 string값을 확인 해 보았는데, sex, god, love, secret 등 우리가 알아 볼 수 있는 단어들이 여럿 나열 되어 있습니다.

이 중 하나가 우리가 찾는 이 바이너리의 password 일 것입니다.



hex값으로 보면 esp+0과 esp+4만 확인하면 됩니다.

esp+4의 위치에 있는 'sex'가 바로 우리가 찾는 password였습니다.



'sex'를 password로 입력하자 bash shell이 떨어졌고, euid와 groups 권한을 보니 leviathan2로 상승 되어 있었습니다. 이제 leviathan2의 패스워드만 알아 내면 될 것입니다.

find로 leviathan2의 유저 권한을 가지고 있는 파일을 찾아보았더니 bandit과 유사한 경로에 leviathan2의 패스워드가 자리 잡고 있는 것을 확인 할 수 있었습니다.

cat으로 깔끔하게 패스워드를 뱉어줍시다.

OverTheWire_Wargame Leviathan

[ level0 -> level1 ]


Leviathan 시작합니다.



홈 디렉토리를 확인하니 backup 폴더가 숨김 속성으로 고스란히 자리잡고 있습니다.

들어 가 보니, bookmarks.html 파일이 하나 있는데, 용량이 큰 것으로 봐서 cat으로 출력하면 있던 답도 노답이 될것만 같습니다.

grep으로 왠지 있을것만 같은 leviathan을 필터링 해 봅시다.



아니나 다를까, leviathan1의 패스워드를 뱉어줍니다.

OverTheWire_Wargame Bandit

[ level25 -> level26 ]


와.. 드디어 마지막 문제입니다. bandit는 난이도가 어려운 편은 아니었지만 단계가 너무 많아서 시간이 오래 걸렸던 것 같습니다.

바로 시작합니다!


이번 문제는 bandit26은 /bin/bash로 로그인이 되지 않는다고 합니다. 어떤 식의 문제일 지 한번 살펴보죠.



아.. RSA key를 던져줬습니다. 이건 이전에 풀었던 문제와 동일한 문제인 것 같습니다. 오히려 더 쉽네요. 방법은 아래 글에 정리 되어 있습니다.


OverTheWire Bandit [Level13 -> Level14]




이렇게 bandit26이 나타나면서 끝이 납니다!


이렇게 putty로 keygen을 해서 접속하는 방법 외에 그냥 바로 ssh 명령어로 붙을 수도 있습니다.



해당 RSA key를 ssh -i 속성을 줘서 import 시키고 bandit26으로 접속하면 동일하게 클리어 가능합니다.





OverTheWire Bandit 끝!

OverTheWire_Wargame Bandit

[ level24 -> level25 ]




자, 이번 문제는 드디어 cron에서 벗어났네요.

한 데몬이 30002포트로 listening 상태에 있다고 합니다. 만약 bandit24의 패스워드와 함께 4자리의 숫자 핀코드를 제대로 때려박아준다면 bandit25로 향하는 패스워드를 알려주겠다네요.

전형적인 브루트포싱 문제입니다. 파이썬으로 코드를 짜서 돌리도록 합시다.



먼저, nc로 반응이 어떠한지 확인을 해 보았습니다.

한 번 연결하면 계속 입력을 받는 구조군요. 실패 시 출력 값은 Fail! You did not supply enough data. Try again. 입니다.

그런줄 알았는데....

Fail 부분은 제대로 된 길이의 인자를 전송하지 않았을 때 발생하는 에러 출력값이었습니다.

제대로 된 문자열을 전송했을 경우의 실패 메시지는 Wrong! Please enter the correct pincode. 입니다.

이걸 기반으로 코드를 짜보도록 하겠습니다.



4중 for문으로 브루트포싱하였습니다. 맨 처음 기본 출력값이 있기 때문에 recv를 한번 해주었고, 그 이후에는 send와 recv를 번갈아가며 실행, 실패 메시지가 출력되지 않았을 경우 그 문자열을 뱉어내는 코드입니다.



bandit25로 향하는 문을 열어줄 패스워드를 뱉어냈습니다!

+ Recent posts