Login as : level1
Password : level1
유닉스 계열의 운영체제는 계정에 읽기(R), 쓰기(W), 실행(X) 권한을 줄 수 있는데, 내가 만든 파일을 소유자, 그룹 사용자, 다른 계정 이렇게 3가지 경우로 나누어 각각에 읽고, 쓰고, 실행할 수 있는 권한을 부여하는 것이다.
여기에 추가로 특수 권한인 SetUID와 SetGID를 부여할 수 있는데, 이를 이용하면 다른 계정에서 만든 파일을 내 계정에서 읽거나 쓰거나 실행할 때 파일을 실행하는 동안에만 다른 계정의 권한을 잠시 얻을 수 있다.
sticky bit : 파일 혹은 디렉토리의 소유자가 아니면 삭제 혹은 수정이 불가능하도록 하는 것이다.
sticky bit가 설정된 파일 혹은 디렉토리는 다른 사용자가 자신의 디렉토리처럼 사용할 수 있는 공용이 되지만, 소유자가 아닌 사용자는 해당파일 혹은 디렉토리를 삭제나 수정이 불가능하다.
ls
hint 파일 : 각 레벨마다 존재하며, 각 레벨을 어떻게 풀어야 하는지에 대한 힌트가 적혀있다.
public_html : 각 사용자들의 홈페이지 파일이 들어가 있고, 해킹에 성공했을 때 이 디렉토리 안에 있는 파일을 수정한다고 한다.
tmp : 권한에 상관없이 누구나 이 디렉토리에 임시로 파일을 저장할 수 있다고 한다.
cat hint
level1의 힌트는 level2 권한에 setuid가 걸린 파일을 찾아야 한다고 한다.
즉, level2 권한에 setuid가 걸린 파일을 이용해 level2의 권한을 얻으면 된다.
find / -user level2 -perm +6000 -exec ls -l {} \; 2>/dev/null
find 명령어를 이용해
"루트 디렉토리(시스템 내 전체)에서 소유자가 level2이고, setuid(4000) 권한 또는 setgid(2000) 권한 둘 중 하나라도 걸린 파일 혹은 디렉토리를 찾고, 해당 결과를 ls -l 명령으로 나열하고, find 명령어를 진행하는 중에 에러가 난 내용들은 휴지통으로 버린다."
위의 명령어를 실행하면 /bin 디렉토리 하위에 있는 ExecuteMe 파일이 나온다.
리눅스 읽고, 쓰고, 실행 권한
Read : 4
Write : 2
Execute : 1
리눅스 특수 권한
Setuid : 4000
Setgid : 2000
Sticky bit : 1000
755 == 0755 == 소유자 : 읽고, 쓰고, 실행 / 그룹 소유자 : 읽고, 실행 / 기타 사용자 : 읽고, 실행 권한만 있다.
위의 +6000은 FTZ 에서는 사용 가능하지만 최근의 리눅스에서 사용하려면 ' + ' 대신 ' / ' 기호를 사용하면 된다.
즉, ' - '(마이너스) 기호와 ' / ' 기호 사용하고 ' / ' 기호가 과거의 ' + ' 기호인 것이다.
' - ' 기호는 AND 연산으로 생각하고, ' / ' 기호는 OR 연산으로 생각하면 된다.
위의 명령어를 예로 들면 -6000은 setuid와 setgid 모두 걸린 파일이고, +6000은 setuid 또는 setgid 둘 중 하나라도 걸린 파일이다.
분석
gdb -q /bin/ExecuteMe
disas main
GDB를 이용해 /bin/ExecuteMe 파일을 분석해본다.
0x08048488 <main+0>: push %ebp
0x08048489 <main+1>: mov %esp,%ebp
0x0804848b <main+3>: sub $0x28,%esp
0x0804848e <main+6>: and $0xfffffff0,%esp
0x08048491 <main+9>: mov $0x0,%eax
0x08048496 <main+14>: sub %eax,%esp
0x08048498 <main+16>: sub $0xc,%esp
0x0804849b <main+19>: push $0x8048680
0x080484a0 <main+24>: call 0x8048358 <system>
0x080484a5 <main+29>: add $0x10,%esp
0x080484a8 <main+32>: sub $0xc,%esp
0x080484ab <main+35>: push $0x804868f
0x080484b0 <main+40>: call 0x8048378 <chdir>
0x080484b5 <main+45>: add $0x10,%esp
0x080484b8 <main+48>: sub $0xc,%esp
0x080484bb <main+51>: push $0x80486a0
0x080484c0 <main+56>: call 0x80483a8 <printf>
0x080484c5 <main+61>: add $0x10,%esp
0x080484c8 <main+64>: sub $0xc,%esp
0x080484cb <main+67>: push $0x80486e0
0x080484d0 <main+72>: call 0x80483a8 <printf>
0x080484d5 <main+77>: add $0x10,%esp
0x080484d8 <main+80>: sub $0xc,%esp
0x080484db <main+83>: push $0x8048720
0x080484e0 <main+88>: call 0x80483a8 <printf>
0x080484e5 <main+93>: add $0x10,%esp
0x080484e8 <main+96>: sub $0xc,%esp
0x080484eb <main+99>: push $0x8048760
0x080484f0 <main+104>: call 0x80483a8 <printf>
0x080484f5 <main+109>: add $0x10,%esp
0x080484f8 <main+112>: sub $0xc,%esp
0x080484fb <main+115>: push $0x8048782
0x08048500 <main+120>: call 0x80483a8 <printf>
0x08048505 <main+125>: add $0x10,%esp
0x08048508 <main+128>: sub $0x4,%esp
0x0804850b <main+131>: pushl 0x8049948
0x08048511 <main+137>: push $0x1e
0x08048513 <main+139>: lea 0xffffffd8(%ebp),%eax
0x08048516 <main+142>: push %eax
0x08048517 <main+143>: call 0x8048368 <fgets>
0x0804851c <main+148>: add $0x10,%esp
0x0804851f <main+151>: lea 0xffffffd8(%ebp),%eax
0x08048522 <main+154>: sub $0x8,%esp
0x08048525 <main+157>: push $0x804879c
0x0804852a <main+162>: push %eax
0x0804852b <main+163>: call 0x8048388 <strstr>
0x08048530 <main+168>: add $0x10,%esp
0x08048533 <main+171>: test %eax,%eax
0x08048535 <main+173>: je 0x8048551 <main+201>
0x08048537 <main+175>: sub $0xc,%esp
0x0804853a <main+178>: push $0x80487c0
0x0804853f <main+183>: call 0x80483a8 <printf>
0x08048544 <main+188>: add $0x10,%esp
0x08048547 <main+191>: sub $0xc,%esp
0x0804854a <main+194>: push $0x0
0x0804854c <main+196>: call 0x80483c8 <exit>
0x08048551 <main+201>: lea 0xffffffd8(%ebp),%eax
0x08048554 <main+204>: sub $0x8,%esp
0x08048557 <main+207>: push $0x80487e8
0x0804855c <main+212>: push %eax
0x0804855d <main+213>: call 0x8048388 <strstr>
0x08048562 <main+218>: add $0x10,%esp
0x08048565 <main+221>: test %eax,%eax
0x08048567 <main+223>: je 0x8048583 <main+251>
0x08048569 <main+225>: sub $0xc,%esp
0x0804856c <main+228>: push $0x8048800
0x08048571 <main+233>: call 0x80483a8 <printf>
0x08048576 <main+238>: add $0x10,%esp
0x08048579 <main+241>: sub $0xc,%esp
0x0804857c <main+244>: push $0x0
0x0804857e <main+246>: call 0x80483c8 <exit>
0x08048583 <main+251>: sub $0xc,%esp
0x08048586 <main+254>: push $0x8048826
0x0804858b <main+259>: call 0x80483a8 <printf>
0x08048590 <main+264>: add $0x10,%esp
0x08048593 <main+267>: sub $0x8,%esp
0x08048596 <main+270>: push $0xbba
0x0804859b <main+275>: push $0xbba
0x080485a0 <main+280>: call 0x80483b8 <setreuid>
0x080485a5 <main+285>: add $0x10,%esp
0x080485a8 <main+288>: sub $0xc,%esp
0x080485ab <main+291>: lea 0xffffffd8(%ebp),%eax
0x080485ae <main+294>: push %eax
0x080485af <main+295>: call 0x8048358 <system>
0x080485b4 <main+300>: add $0x10,%esp
0x080485b7 <main+303>: leave
0x080485b8 <main+304>: ret
0x080485b9 <main+305>: nop
0x080485ba <main+306>: nop
0x080485bb <main+307>: nop
End of assembler dump.
/bin/ExecuteMe 파일의 main 함수의 대략적인 동작 원리는 아래와 같다.
1. 스택을 구성하고, 늘린 뒤 값을 스택에 넣고 system() 함수로 명령어를 실행한다.
2. chdir() 함수로 디렉토리 위치를 이동시킨다.
3. 5번의 printf() 함수 호출로 문자열을 출력한다.
4. fgets() 함수로 사용자에게 입력을 받는다.
5. 두 번의 strstr 함수 호출이 있는데, 이 함수를 이용해 4번 과정에서 받은 입력값에 "my-pass", "chmod" 문자열이 있는지 검사한다.
6. strstr() 함수로 비교 한 결과 "my-pass", "chmod" 문자열과 같지 않은 경우 아래의 작업을 수행한다.
7. setreuid(3002, 3002) 함수를 이용해 실행되는 파일의 User ID 권한을 level2 계정으로 설정한다.
8. system() 함수를 이용해 입력받은 문자열을 리눅스의 명령어로 실행한다.
"my-pass"와 "chmod" 문자열과 사용자가 입력한 문자열을 비교하여 같으면 exit() 함수를 호출한다.
즉, "my-pass"와 "chmod" 문자열을 필터링하는 것이다.
"my-pass", "chmod" 이외의 문자열이면 system() 함수의 인자로 넘겨져 실행되는데, 리눅스에서 사용되는 명령어라면 정상적으로 실행이 되고, 사용되지 않는 명령어라면 에러가 뜰 것이다.
이번에는 main() 함수의 동작 원리를 자세히 봐본다.
0x08048488 <main+0>: push %ebp
0x08048489 <main+1>: mov %esp,%ebp
0x0804848b <main+3>: sub $0x28,%esp
0x0804848e <main+6>: and $0xfffffff0,%esp
0x08048491 <main+9>: mov $0x0,%eax
0x08048496 <main+14>: sub %eax,%esp
0x08048498 <main+16>: sub $0xc,%esp
1. main() 함수로 진입하면서 EBP 레지스터에 저장되어 있는 main() 함수 이전 함수의 EBP 값을 스택에 저장한다.
(스택에서 이전 EBP 값이 저장된 공간을 SFP(Saved Frame Pointer)라고 한다.)
2. 현재 ESP(스택 포인터)에 든 값을 EBP(베이스 포인터) 레지스터에 저장함으로써 ESP와 EBP를 같은 위치로 만든다.
(ESP는 수시로 변하기 때문에 변경되기 전에 EBP에 저장하는 것이다.)
3. 현재 ESP 위치에서 52(0x34)byte 만큼 뺀다.
(스택은 높은 주소(0xFFFFFFFF에 가까운 쪽)에서 낮은 주소(0x00000000에 가까운 쪽)로 거꾸로 자라기 때문에 스택을 n 만큼 뺀다는 것은 스택의 공간을 늘린다는 것이고, n 만큼 더한다는 것은 스택의 공간을 줄인다는 것이다.)
0x0804849b <main+19>: push $0x8048680
0x080484a0 <main+24>: call 0x8048358 <system>
0x080484a5 <main+29>: add $0x10,%esp
0x080484a8 <main+32>: sub $0xc,%esp
x/s 0x8048680
main 주소로부터 19만큼 떨어져있는 주소에서 0x8048680 주소를 스택에 넣는다.
x 명령어를 이용해 메모리 조사를 해보면 0x8048680 주소에는 "/usr/bin/clear" 문자열이 있다.
즉, "/usr/bin/clear" 문자열을 스택에 넣고, system() 함수를 호출한다.
그러면 이는 system("/usr/bin/clear") 라는 코드를 실행해서 리눅스 터미널 화면을 지우는 것이다.
0x080484ab <main+35>: push $0x804868f
0x080484b0 <main+40>: call 0x8048378 <chdir>
0x080484b5 <main+45>: add $0x10,%esp
0x080484b8 <main+48>: sub $0xc,%esp
x/s0x804868f
이어서 "/home/level2" 라는 문자열을 스택에 넣고, chdir() 함수를 호출한다.
즉, chdir("/home/level2") 코드가 실행되고, 리눅스에서는 /home/level2 디렉토리로 이동한다.
0x080484bb <main+51>: push $0x80486a0
0x080484c0 <main+56>: call 0x80483a8 <printf>
0x080484c5 <main+61>: add $0x10,%esp
0x080484c8 <main+64>: sub $0xc,%esp
0x080484cb <main+67>: push $0x80486e0
0x080484d0 <main+72>: call 0x80483a8 <printf>
0x080484d5 <main+77>: add $0x10,%esp
0x080484d8 <main+80>: sub $0xc,%esp
0x080484db <main+83>: push $0x8048720
0x080484e0 <main+88>: call 0x80483a8 <printf>
0x080484e5 <main+93>: add $0x10,%esp
0x080484e8 <main+96>: sub $0xc,%esp
0x080484eb <main+99>: push $0x8048760
0x080484f0 <main+104>: call 0x80483a8 <printf>
0x080484f5 <main+109>: add $0x10,%esp
0x080484f8 <main+112>: sub $0xc,%esp
0x080484fb <main+115>: push $0x8048782
0x08048500 <main+120>: call 0x80483a8 <printf>
x/s 0x80486a0
x/s 0x80486e0
x/s 0x8048720
x/s 0x8048760
x/s 0x8048782
그리고 위의 각 문자열들을 스택에 넣고 printf() 함수를 호출한다.
즉, 아래의 코드를 실행하는 것이다.
printf("\n\n\n\t\t레벨 2의 권한으로 당신이 원하는 명령어를\n");
printf("\t\t한가지 실행시켜 드리겠습니다.\n");
printf("(단, my-pass와 chmod는 제외)\n");
printf("\n\t\t어떤 명령을 실행시키겠습니까?\n");
printf("\n\n\t\t[level2@ftz level2]$ ");
0x08048505 <main+125>: add $0x10,%esp
0x08048508 <main+128>: sub $0x4,%esp
0x0804850b <main+131>: pushl 0x8049948
0x08048511 <main+137>: push $0x1e
0x08048513 <main+139>: lea 0xffffffd8(%ebp),%eax
0x08048516 <main+142>: push %eax
0x08048517 <main+143>: call 0x8048368 <fgets>
0x0804851c <main+148>: add $0x10,%esp
0x0804851f <main+151>: lea 0xffffffd8(%ebp),%eax
0x08048522 <main+154>: sub $0x8,%esp
fgets() 함수를 호출하는데 fgets() 함수는 총 3개의 인자가 필요한데 첫 번째 인자부터 "입력받은 값을 저장할 주소", "크기", "형태" 이다.
즉, 위의 코드는 아래의 함수를 호출하는 것이다.
fget(ebp-0xFFFFFFd8, 0x1e(30) byte, STDIN);
0x08048525 <main+157>: push $0x804879c
0x0804852a <main+162>: push %eax
0x0804852b <main+163>: call 0x8048388 <strstr>
0x08048530 <main+168>: add $0x10,%esp
0x08048533 <main+171>: test %eax,%eax
0x08048535 <main+173>: je 0x8048551 <main+201>
0x08048537 <main+175>: sub $0xc,%esp
0x0804853a <main+178>: push $0x80487c0
0x0804853f <main+183>: call 0x80483a8 <printf>
0x08048544 <main+188>: add $0x10,%esp
0x08048547 <main+191>: sub $0xc,%esp
0x0804854a <main+194>: push $0x0
0x0804854c <main+196>: call 0x80483c8 <exit>
0x08048551 <main+201>: lea 0xffffffd8(%ebp),%eax
0x08048554 <main+204>: sub $0x8,%esp
0x08048557 <main+207>: push $0x80487e8
0x0804855c <main+212>: push %eax
0x0804855d <main+213>: call 0x8048388 <strstr>
0x08048562 <main+218>: add $0x10,%esp
0x08048565 <main+221>: test %eax,%eax
0x08048567 <main+223>: je 0x8048583 <main+251>
0x08048569 <main+225>: sub $0xc,%esp
0x0804856c <main+228>: push $0x8048800
0x08048571 <main+233>: call 0x80483a8 <printf>
0x08048576 <main+238>: add $0x10,%esp
0x08048579 <main+241>: sub $0xc,%esp
0x0804857c <main+244>: push $0x0
0x0804857e <main+246>: call 0x80483c8 <exit>
0x08048583 <main+251>: sub $0xc,%esp
0x08048586 <main+254>: push $0x8048826
0x0804858b <main+259>: call 0x80483a8 <printf>
0x08048590 <main+264>: add $0x10,%esp
0x08048593 <main+267>: sub $0x8,%esp
0x08048596 <main+270>: push $0xbba
0x0804859b <main+275>: push $0xbba
0x080485a0 <main+280>: call 0x80483b8 <setreuid>
0x080485a5 <main+285>: add $0x10,%esp
0x080485a8 <main+288>: sub $0xc,%esp
x/s 0x804879c : "my-pass"
x/s 0x80487c0 : "\n\t\tmy-pass 명령은 사용할 수 없습니다.\n\n"
x/s 0x80487e8 : "chmod"
x/s 0x8048800 : "\n\t\tchmod명령은 사용할 수 없습니다.\n\n"
x/s 0x8048826 : "\n\n"
사용자 입력 값에 "my-pass" 문자열이 있는지 strstr() 함수로 검색하고
있다면 "my-pass 명령은 사용할 수 없습니다." 문자열을 출력한 뒤 exit() 함수를 호출한다.
없다면 0x8048551 주소로 점프한다.
이어서 사용자 입력 값에 "chmod" 문자열이 있는지 strstr() 함수로 검색하고
있다면 "chmod명령은 사용할 수 없습니다." 문자열을 출력한 뒤 exit() 함수를 호출한다.
없다면 0x8048583 주소로 점프하여 "\n\n"으로 두 줄 개행한 후 0xbba(3002)를 스택에 두 번 넣고, setreuid() 함수를 호출한다.
setreuid() 함수는 Real UID와 Effective UID를 설정하는 함수로 아래의 형태로 호출하고, 3002는 level2의 uid이다.
즉, 현재 level1 사용자이므로 root 권한은 없지만, 현재 UID가 첫 번째 인자로 들어온 실제 UID나 saved set uid와 같다면, 두 번째 인자로 들어온 유효한 UID 값으로 일시적으로 설정한다.
setreuid(real uid, effective uid);
// real UID : 실제 사용자(ex : 로그인 할 때 사용자)
// effective UID : 유효한 사용자(ex : 현재 실제 유효한 사용자, id 명령어 결과)
0x080485ab <main+291>: lea 0xffffffd8(%ebp),%eax
0x080485ae <main+294>: push %eax
0x080485af <main+295>: call 0x8048358 <system>
0x080485b4 <main+300>: add $0x10,%esp
0x080485b7 <main+303>: leave
0x080485b8 <main+304>: ret
effective UID로 설정된 이후 즉, level2 권한을 얻은 상태에서 ebp - 0xFFFFFFd8 주소를 스택에 넣고 system() 함수를 호출한다.
즉, 사용자 입력값이 my-pass도 chmode도 아니면 level2 권한으로 system("사용자 입력값") 함수를 호출한다.
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
char input[30];
char *mypass = "my-pass";
char *chmod = "chmod";
system("/usr/bin/clear");
chdir("/home/level2");
printf("\n\n\n\t\t레벨 2의 권한으로 당신이 원하는 명령어를\n");
printf("\t\t한가지 실행시켜 드리겠습니다.\n");
printf("(단, my-pass와 chmod는 제외)\n");
printf("\n\t\t어떤 명령을 실행시키겠습니까?\n");
printf("\n\n\t\t[level2@ftz level2]$ ");
fgets(input, sizeof(input), stdin);
if(strstr(input, mypass) != NULL)
{
printf("\n\t\tmy-pass 명령은 사용할 수 없습니다.\n\n");
exit(0);
}
if(strstr(input, chmod) != NULL)
{
printf("\n\t\tchmod명령은 사용할 수 없습니다.\n\n");
exit(0);
}
printf("\n\n");
setreuid(3002, 3002);
system(input);
}
위의 분석 내용을 통합하여 의사 코드로 표현하면 위와 같다.
풀이 1
위의 분석 내용을 바탕으로 봤을 때 /bin/ExecuteMe 파일을 실행한 후 값을 입력하고 해당 값이 필터링 되지 않으면 level2 권한을 잠시 얻는다.
level2의 비밀번호를 보기 위해서는 my-pass를 입력하는데, my-pass를 입력하면 필터링에서 걸리므로 level2 권한을 얻은 상태에서 이 권한을 유지해야 하는데, 권한을 유지하는 방법에는 여러가지가 있지만 bash를 실행하면 level2 권한이 유지가 된다.
쉽게 말하자면, setreuid() 함수로 인해 level2 권한을 얻은 채로 사용자가 입력한 값이 system()의 인자로 들어가므로 입력값이 유효한 리눅스 명령어라면 level2 권한으로 해당 명령어를 실행하는 것이 된다.
level2 권한으로 bash 명령어를 실행하면 level2 권한이 유지된 채로 셸이 실행되고, 이 상태에서 비밀번호를 보기 위해 my-pass 명령을 입력하면 된다.
hacker or cracker
풀이 2
vi 편집기에서는 셸을 실행하는 부가 기능이 있다.
이 기능을 이용하면 위의 풀이 1과는 다른 방법으로 풀 수도 있다.
위와 같이 /bin/ExecuteMe 파일을 실행 후 vi [임의로 만들 파일 이름] 을 입력하면
level2 권한으로 vi 명령을 실행하고
위와 같이 새로운 파일을 편집할 수 있는 창이 뜬다.
이 상태에서 위와 같이 :!my-pass 를 입력하면, 현재 level2 권한을 얻었으므로 level2의 패스워드가 보이게 된다.
(my-pass 명령어 대신 bash를 줘서 bash를 실행하도록 할 수도 있다.)
'전쟁 > hackerschool ftz' 카테고리의 다른 글
[hackerschool FTZ] level6(시스템 인터럽트) (0) | 2022.12.15 |
---|---|
[hackerschool FTZ] level5(레이스 컨디션) (0) | 2022.12.15 |
[hackerschool FTZ] level4(xinetd) (0) | 2022.12.14 |
[hackerschool FTZ] level3 (system 함수의 위험성) (0) | 2022.12.13 |
[hackerschool FTZ] level2(부가 기능을 잘 알아두면 좋다. VI의 기능) (0) | 2022.12.12 |