0xL4ugh CTF의 Wanna Play a Game 문제에서 제공된 바이너리 내부의 각 함수의 동작 과정에 대해 설명하였다.
다행히 strip이 되어 있지 않아 분석에 용이했다.
main
함수int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
__int64 v3; // [rsp+0h] [rbp-10h]
__int64 v4; // [rsp+8h] [rbp-8h]
setup();
printf("[*] NickName> ");
if ( read(0, &username, 0x40uLL) == -1 )
{
perror("READ ERROR");
exit(-1);
}
while ( 1 )
{
menu();
func = read_int();
printf("[*] Guess>");
param = read_int();
((void (__fastcall *)(__int64))conv[func - 1])(param);
}
}
setup
함수로 기본적인 환경 설정을 한 후, 이름을 받아 username
에 저장한다. 이후 사용자로부터 입력을 받아 menu
에 있는 기능을 수행한다. 이때, menu
에 해당하는 기능은 conditional jump
로 구현하지 않고, index(func
)를 입력 받아 함수 배열(conv
)을 참조하여 함수를 호출하는 방식으로 구현되었다.
setup
함수void setup()
{
unsigned int v0; // eax
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
signal(14, handler);
alarm(0x3Cu);
change_passcode();
v0 = time(0LL);
srand(v0);
}
Buffered I/O
가 꽉 찬 이후에만 출력 되는 것을 방지하기 위해, buffer
를 초기화해준다. 이후 SIGALRM(14)
으로 0x3C초의 exploit 제한 시간을 설정한다. 이후, change_passcode
함수를 호출해 /dev/random
으로 passcode를 설정한다. 또한, srand(time(NULL))
로 현재 시간을 seed로 pseudo random도 설정한다.
change_passcode
함수int change_passcode()
{
int fd; // [rsp+Ch] [rbp-4h]
fd = open("/dev/random", 0);
if ( fd < 0 )
{
perror("OPEN ERROR");
exit(-1);
}
if ( read(fd, &passcode, 8uLL) == -1 )
{
perror("READ ERROR");
exit(-1);
}
puts("[*] PASSCODE CHANGED!");
return close(fd);
}
system 잡음 등으로 생성한 랜덤값이 저장되어 있는 /dev/random
파일을 열어 passcode
를 8byte의 랜덤값으로 설정한다.
menu
함수int menu()
{
puts("= = = = = CAN YOU BEAT ME! = = = = =");
puts("[1] Easy");
return puts("[2] Hard");
}
passcode leak?의 난이도 (Easy, Hard)에 대한 메뉴를 출력한다.
read_int
함수__int64 read_int()
{
__int64 buf[6]; // [rsp+0h] [rbp-30h] BYREF
buf[5] = __readfsqword(0x28u);
memset(buf, 0, 32);
printf("> ");
if ( read(0, buf, 0x20uLL) == -1 )
{
perror("READ ERROR");
exit(-1);
}
return atol((const char *)buf);
}
buf[5] = __readfsqword(0x28u);
는 Stack Canary 관련이기에 무시한다.
이 함수는 32byte 크기의 10진 정수를 입력 받아 integer로 변환해서 반환한다.
conv
배열에 있는 함수 - easy
int __fastcall easy(__int64 param)
{
if ( param == rand() )
return printf("[+] NICE GUESS!!\n[*] Current Score: %lu\n", score);
else
return puts("[-] WRONG GUESS :(");
}
main
함수에서 전달 받은 param
이 랜덤값과 동일한지 확인한다. 이때, 이후 출력될 수 있는 score
은 전역변수이다.
conv
배열에 있는 함수 - hard
unsigned __int64 __fastcall hard(__int64 param)
{
int i; // [rsp+14h] [rbp-2Ch]
char path[8]; // [rsp+2Fh] [rbp-11h] BYREF
char v4; // [rsp+37h] [rbp-9h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
strcpy(path, "<qz}<`{");
v4 = 0;
for ( i = 0; i <= 6; ++i )
path[i] ^= 0x13u;
if ( param == passcode )
{
puts("[+] WINNNN!");
execve(path, 0LL, 0LL);
}
else
{
puts("[-] YOU ARE NOT WORTHY FOR A SHELL!");
}
change_passcode();
return v5 - __readfsqword(0x28u);
}
main
함수에서 전달 받은 param
이 /dev/random
으로 가져온 랜덤값과 동일한지 확인한다. 동일하다면 shell을 실행하고, 아니라면 passcode
를 변경한다.
[그림 1] execve
함수의 인자로 사용되는 path
변수의 값 변화 확인