ghkdtlwns987
[pwnable.tw] silver_bullet 본문
시험 끝! 헤헹
FULL RELRO, NX enabled 되어 있다.
그리고 libc_32.so.6 파일을 준다.
-> 그렇다는 건 libc_base 를 구해야 할 것이다.
main 함수
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int v5; // [esp+0h] [ebp-3Ch]
const char *v6; // [esp+4h] [ebp-38h]
char s; // [esp+8h] [ebp-34h] ebp - 0x34
int v8; // [esp+38h] [ebp-4h]
init_proc();
v8 = 0;
memset(&s, 0, 48u); // ebp-0x34(52byte)
v5 = 0x7FFFFFFF;
v6 = "Gin";
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
v3 = read_int();
if ( v3 != 2 )
break;
power_up(&s); // power up(2)
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_16;
create_bullet(&s); // create(1)
}
if ( v3 == 3 )
break;
if ( v3 == 4 ) // exit(4)
{
puts("Don't give up !");
exit(0);
}
LABEL_16:
puts("Invalid choice");
}
if ( beat((int)&s, &v5) ) // beat(3)
return 0;
puts("Give me more power !!");
}
}
main 함수를 보면 s라는 배열의 48byte 를 모드 0으로 초기화 해준다.
그리고 v5 = 0x7FFFFFF , v6 = "Gin" 으로 초기화 되어 있다.
중간에 read_int() 함수로 숫자를 입력받는다.
이후에 차례대로 1 , 2 , 3 , 4 번을 입력하면 차례대로 함수가 호출되고, beat() 함수를 실행했을 시
HP 가 모두 떨어지면 프로그램이 종료되는데, 그렇지 않으면 함수가 계속 실행된다.
1. create_bullet()
int __cdecl create_bullet(char *s)
{
size_t v2; // ST08_4
if ( *s )
return puts("You have been created the Bullet !");
printf("Give me your description of bullet :");
read_input(s, 48u); // 48byte input
v2 = strlen(s); // 길이만큼 strlen
printf("Your power is : %u\n", v2);
*((_DWORD *)s + 12) = v2;
return puts("Good luck !!");
}
create_bullet() 은 read_input(s, 48) 으로 값을 입력받고, strlen(s) 로 길이를 v2에 저장, 후 printf() 함수에서 내가 입력했던 문자열의 길이가 출력된다.
create_bullet() 호출 시 인자로 받았던 *s(배열) 의 12*4 = 48 번째 인덱스에 저장된다.
=> 배열의 크기(48byte) 이후에 저장이 된다. 이는 직접 디버깅해서 보면 쉽게 파악 할 수 있다.
ssize_t __cdecl read_input(void *buf, size_t nbytes)
{
ssize_t v3; // [esp+0h] [ebp-4h]
v3 = read(0, buf, nbytes); // 48byte input
if ( v3 <= 0 )
{
puts("read error");
exit(1);
}
if ( *((_BYTE *)buf + v3 - 1) == 10 ) // 10(0xa) => '\n'
*((_BYTE *)buf + v3 - 1) = 0;
return v3;
}
create_bullet() 함수 내부에 존재하는 read_input() 함수는 read() 함수로 최대 48byte 를 입력받는다.
그런데 중간에 read() 함수에서 문자열을 읽은 길이의 -1 번째의 값이 '\n' 이 아니라면 0을 넣어
강제로 널 문자를 추가해 준다. (만약 canary가 있었다면 이 부분에서는 leak 이 안됬을 것이다.)
2. power_up()
int __cdecl power_up(char *dest)
{
char s; // [esp+0h] [ebp-34h]
size_t v3; // [esp+30h] [ebp-4h]
v3 = 0;
memset(&s, 0, 48u);
if ( !*dest )
return puts("You need create the bullet first !");
if ( *((_DWORD *)dest + 12) > 47u )
return puts("You can't power up any more !");
printf("Give me your another description of bullet :");
read_input(&s, 48 - *((_DWORD *)dest + 12));
strncat(dest, &s, 48 - *((_DWORD *)dest + 12));
v3 = strlen(&s) + *((_DWORD *)dest + 12);
printf("Your new power is : %u\n", v3);
*((_DWORD *)dest + 12) = v3;
return puts("Enjoy it !");
}
power_up() 함수는 bullet 이 하나 만들어져 있어야 한다. 그렇지 않으면 puts 를 return 해 주면서 프로그램이 종료된다.
또한, create_bullet() 에서 strlen()으로 buf+49 번째의 공간에 길이를 저장했는데, power_up() 함수에서 dest + 49가 넘어가게 되면 puts() 함수를 return 하면서 프로그램이 종료된다.
그렇지 않으면 값을 한번 더 입력하는데, 주의할 점이 입력할 수 있는 길이가 제한되어 있다.
(48byte - (s + 49))
이후에 strncat() 함수로 main() 함수에서 저장했던 dest 배열에 power_up함수 내에서 입력했던 값이 strncat 되어 값이 붙고, 새로 입력한 값의 길이가 이전에 입력했던 값의 문자열의 길이가 더해진다(buf+49)
그리고 printf() 함수에서 buf+49 에 저장되어 있는 문자열의 총길이를 출력하고 출력된 결과값이 저장된다.
3. beat()
signed int __cdecl beat(int a1, _DWORD *a2)
{
signed int result; // eax
if ( *(_BYTE *)a1 )
{
puts(">----------- Werewolf -----------<");
printf(" + NAME : %s\n", a2[1]);
printf(" + HP : %d\n", *a2); // leak address
puts(">--------------------------------<");
puts("Try to beat it .....");
usleep(1000000u);
*a2 -= *(_DWORD *)(a1 + 48);
if ( *a2 <= 0 )
{
puts("Oh ! You win !!");
result = 1;
}
else
{
puts("Sorry ... It still alive !!");
result = 0;
}
}
else
{
puts("You need create the bullet first !");
result = 0;
}
return result;
}
beat()의 첫번째 인자는 create_bullet, power_up 에서 쓰였던 buf가 들어가고, 두번째 인자는 main 함수에 선언되어 있는 a2가 들어갔다.
a2[1] , *a2 에는 각각 'Gin', 0x7FFFFFFF 가 저장되어 있다.
여기서 a2[1] 과 *a2를 출력해 주고, *a2(0x7FFFFFFF) 을 (a1+48)에 위치해 있는 값을 빼주고, *a2 가 <= 0 보다 작으면 프로그램이 종료되고, 그렇지 않으면 다시 main() 함수로 가서 프로그램을 실행시킨다.
5. read_int() 함수
int read_int()
{
char buf; // [esp+0h] [ebp-18h]
ssize_t v2; // [esp+14h] [ebp-4h]
v2 = read(0, &buf, 15u);
if ( v2 <= 0 )
{
puts("read error");
exit(1);
}
return atoi(&buf);
}
혹시나 하는 생각에 올렸다.
read() 함수로 최대 15자리수 까지 입력받고 atoi() 로 return 해주는 함수이다.
함수들을 분석해 보았으니 이걸로 어디에서 취약점이 생기고, 어떤 방식으로 exploit 해야할지 생각해보자.
이 프로그램에서 취약점은 power_up() 에 존재한다.
if 문 중에 *(DWORD *)dest + 12 > 47 이 있는데, 만약 48byte를 입력하면 더 이상 함수 호출이 불가능하지만,
47byte 를 입력하게 되면 read_input() 함수로 가고 strncat() 함수를 호출하게 된다.
여기서 off-by-one 취약점이 발생하는데, 47byte 를 create_bullet 함수에서 입력해 주고
1byte 를 power_up 으로 입력해 주면 dest + 12(길이가 저장되는 부분) 에 '\n'이 저장되므로
길이(len) 은 0으로 초기화 되었다가 1byte를 입력해 주었기 때문에, 1이 되고, 이를 이용해
RET 부분을 덮을 수 있게 될 뿐 아니라, beat() 로 HP 를 깎을 수 있으므로 exploit 이 가능해진다.
+exploit 코드를 공개하면 안된다는 말이 들려서 앞으로 pwnable.tw 문제는 exploit 코드를 공개하지 않겠다
'pwnable.tw' 카테고리의 다른 글
[pwnable.tw] seethefile (0) | 2021.01.16 |
---|---|
[pwnable.tw] applestore (0) | 2020.12.29 |
[pwnable.tw] hacknote (0) | 2020.12.07 |
[pwnable.tw] dubblesort (0) | 2020.11.22 |
[pwnable.tw] 3x17 (0) | 2020.11.20 |