포스트

[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 함수 호출에서 버퍼 오프셋으로 사용된다.
  • v3v1 - 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 라이센스를 따릅니다.

"2025 Hacksium Busan" 카테고리의 게시물