본문 바로가기

Writeups

[bi0sctf 2024] writeups

지금까지 CTF중 가장 양심이 없었다...!!! :blobangrypolice:

난이도 분포가 그나마 뉴비부터 어려운 문제까지 다양하다고 했는데, 정작 푼 걸 보면 쉬운거 두개만 딸깍 한 것 같다. 생소한 architecture여서라고 변명 아닌 변명을 해본다.

두 문제 다 BPF (Berkeley Packet Filter) 형태의 ELF를 분석하는 내용이었다. IDA로 못까는줄 알았더니 나중에 대회 discord를 다시 보니까 관련 플러그인이 있다는 사실을 알게 되었다.

BPF? (Berkeley Packet Filter)

BPF는 11개의 가상 레지스터를 사용하고, 문서에 따르면, x86-64 JIT 컴파일러에서 하드웨어 레지스터에 1:1로 매핑되어 사용함을 알 수 있었다.

%r0 <- $rax
%r1 <- $rdi
%r2 <- $rsi
%r3 <- $rdx
%r4 <- $rcx
%r5 <- $r8
%r6 <- $rbx
%r7 <- $r13
%r8 <- $r14
%r9 <- $r15
%r10 <- $rbp

Calling convention을 고려해 보았을 때 $r0에선 return value를 저장할 것이고, $r1 ~ $r5를 이용해 함수의 argument를 전달 (Caller-saved), $r6 ~ r9는 Callee-saved temporaries, $r10은 Stack base를 저장할 것이다.

`bpf-objdump`를 사용해 disassembly를 뜨고, 분석을 진행했다.

beehive

Disassembly

먼저 disassembly를 뜬 것을 보고 설명을 진행해보자.

beehive.o:    file format elf64-bpf

Disassembly of section raw_tracepoint/sys_enter:

0000000000000000 <weird_function>:
       0:    bf 13 00 00 00 00 00 00    r3 = r1
       1:    b7 01 00 00 00 00 00 00    r1 = 0
       2:    7b 1a f8 ff 00 00 00 00    *(u64 *)(r10 - 8) = r1
       3:    7b 1a f0 ff 00 00 00 00    *(u64 *)(r10 - 16) = r1
       4:    7b 1a e8 ff 00 00 00 00    *(u64 *)(r10 - 24) = r1
       5:    7b 1a e0 ff 00 00 00 00    *(u64 *)(r10 - 32) = r1
       6:    7b 1a d8 ff 00 00 00 00    *(u64 *)(r10 - 40) = r1
       7:    7b 1a d0 ff 00 00 00 00    *(u64 *)(r10 - 48) = r1
       8:    7b 1a c8 ff 00 00 00 00    *(u64 *)(r10 - 56) = r1
       9:    79 36 00 00 00 00 00 00    r6 = *(u64 *)(r3 + 0)
      10:    07 03 00 00 08 00 00 00    r3 += 8
      11:    bf a1 00 00 00 00 00 00    r1 = r10
      12:    07 01 00 00 c8 ff ff ff    r1 += -56
      13:    b7 02 00 00 08 00 00 00    r2 = 8
      14:    85 00 00 00 04 00 00 00    call <probe_read> (rbp - 56, 8, Called *r1 + 8)
      15:    79 a1 c8 ff 00 00 00 00    r1 = *(u64 *)(r10 - 56)
      16:    55 01 5f 00 37 13 03 00    if r1 != 201527 goto +95 <LBB0_18> 
      17:    bf 63 00 00 00 00 00 00    r3 = r6
      18:    07 03 00 00 70 00 00 00    r3 += 112
      19:    bf a1 00 00 00 00 00 00    r1 = r10
      20:    07 01 00 00 d0 ff ff ff    r1 += -48
      21:    b7 02 00 00 08 00 00 00    r2 = 8
      22:    85 00 00 00 04 00 00 00    call <probe_read> (r10 - 48, 8, Called *r1 + 112)
      23:    07 06 00 00 68 00 00 00    r6 += 104
      24:    bf a1 00 00 00 00 00 00    r1 = r10
      25:    07 01 00 00 d8 ff ff ff    r1 += -40
      26:    b7 02 00 00 08 00 00 00    r2 = 8
      27:    bf 63 00 00 00 00 00 00    r3 = r6
      28:    85 00 00 00 04 00 00 00    call <probe_read> (r10 - 40, 8, Called *r1 + 104)
      29:    79 a3 d0 ff 00 00 00 00    r3 = *(u64 *)(r10 - 48)
      30:    bf a6 00 00 00 00 00 00    r6 = r10
      31:    07 06 00 00 a8 ff ff ff    r6 += -88
      32:    bf 61 00 00 00 00 00 00    r1 = r6
      33:    b7 02 00 00 20 00 00 00    r2 = 32
      34:    85 00 00 00 72 00 00 00    call <probe_read_user_str> (rbp - 88, 32, *(rbp - 48))
      35:    18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00    r1 = 0 ll   // Offset of "Enter your key: %s"
      37:    b7 02 00 00 13 00 00 00    r2 = 19
      38:    bf 63 00 00 00 00 00 00    r3 = r6
      39:    85 00 00 00 06 00 00 00    call <trace_printk>
      40:    71 a5 a8 ff 00 00 00 00    r5 = *(u8 *)(r10 - 88)
      41:    15 05 13 00 00 00 00 00    if r5 == 0 goto +19 <LBB0_15>
      42:    b7 07 00 00 00 00 00 00    r7 = 0
      43:    b7 01 00 00 01 00 00 00    r1 = 1
      44:    18 02 00 00 14 00 00 00 00 00 00 00 00 00 00 00    r2 = 20 ll  // Offset of 0x00000056
      46:    b7 04 00 00 00 00 00 00    r4 = 0
      47:    b7 03 00 00 01 00 00 00    r3 = 1
      48:    b7 00 00 00 00 00 00 00    r0 = 0
      49:    05 00 0f 00 00 00 00 00    goto +15 <LBB0_3>

0000000000000190 <LBB0_13>:
      50:    07 02 00 00 04 00 00 00    r2 += 4
      51:    bf a5 00 00 00 00 00 00    r5 = r10
      52:    07 05 00 00 a8 ff ff ff    r5 += -88
      53:    0f 15 00 00 00 00 00 00    r5 += r1
      54:    07 01 00 00 01 00 00 00    r1 += 1
      55:    71 55 00 00 00 00 00 00    r5 = *(u8 *)(r5 + 0)
      56:    bf 67 00 00 00 00 00 00    r7 = r6
      57:    55 05 07 00 00 00 00 00    if r5 != 0 goto +7 <LBB0_3>

00000000000001d0 <LBB0_14>:
      58:    67 03 00 00 20 00 00 00    r3 <<= 32
      59:    77 03 00 00 20 00 00 00    r3 >>= 32
      60:    15 03 2f 00 00 00 00 00    if r3 == 0 goto +47 <LBB0_16>

00000000000001e8 <LBB0_15>:
      61:    18 01 00 00 88 00 00 00 00 00 00 00 00 00 00 00    r1 = 136 ll // Offset of "Key is correct"
      63:    b7 02 00 00 10 00 00 00    r2 = 16
      64:    05 00 2e 00 00 00 00 00    goto +46 <LBB0_17>

0000000000000208 <LBB0_3>:
      65:    bf 58 00 00 00 00 00 00    r8 = r5
      66:    b7 06 00 00 01 00 00 00    r6 = 1
      67:    15 08 01 00 40 00 00 00    if r8 == 64 goto +1 <LBB0_5>
      68:    bf 76 00 00 00 00 00 00    r6 = r7

0000000000000228 <LBB0_5>:
      69:    bf 68 00 00 00 00 00 00    r8 = r6
      70:    67 08 00 00 20 00 00 00    r8 <<= 32
      71:    77 08 00 00 20 00 00 00    r8 >>= 32
      72:    b7 07 00 00 01 00 00 00    r7 = 1
      73:    15 08 01 00 00 00 00 00    if r8 == 0 goto +1 <LBB0_7>
      74:    b7 07 00 00 00 00 00 00    r7 = 0

0000000000000258 <LBB0_7>:
      75:    b7 09 00 00 01 00 00 00    r9 = 1
      76:    55 08 01 00 00 00 00 00    if r8 != 0 goto +1 <LBB0_9>
      77:    b7 09 00 00 00 00 00 00    r9 = 0

0000000000000270 <LBB0_9>:
      78:    0f 94 00 00 00 00 00 00    r4 += r9
      79:    0f 70 00 00 00 00 00 00    r0 += r7
      80:    bf 47 00 00 00 00 00 00    r7 = r4
      81:    0f 07 00 00 00 00 00 00    r7 += r0
      82:    67 07 00 00 20 00 00 00    r7 <<= 32
      83:    77 07 00 00 20 00 00 00    r7 >>= 32
      84:    25 07 1b 00 20 00 00 00    if r7 > 32 goto +27 <LBB0_18>
      85:    15 01 e4 ff 1e 00 00 00    if r1 == 30 goto -28 <LBB0_14>

      86:    bf 57 00 00 00 00 00 00    r7 = r5
      87:    57 07 00 00 0f 00 00 00    r7 &= 15
      88:    67 07 00 00 04 00 00 00    r7 <<= 4
      89:    57 05 00 00 f0 00 00 00    r5 &= 240
      90:    77 05 00 00 04 00 00 00    r5 >>= 4
      91:    4f 75 00 00 00 00 00 00    r5 |= r7                            r5 = ((r5 & 0b11110000) >> 4) | ((r5 & 00001111) << 4)

      92:    bf 57 00 00 00 00 00 00    r7 = r5
      93:    57 07 00 00 33 00 00 00    r7 &= 51                            
      94:    67 07 00 00 02 00 00 00    r7 <<= 2
      95:    77 05 00 00 02 00 00 00    r5 >>= 2
      96:    57 05 00 00 33 00 00 00    r5 &= 51
      97:    4f 75 00 00 00 00 00 00    r5 |= r7                            r5 = ((r5 & 00110011) << 2) | ((r5 >> 00110011) & 51)

      98:    bf 57 00 00 00 00 00 00    r7 = r5
      99:    57 07 00 00 55 00 00 00    r7 &= 85
     100:    67 07 00 00 01 00 00 00    r7 <<= 1
     101:    77 05 00 00 01 00 00 00    r5 >>= 1
     102:    57 05 00 00 55 00 00 00    r5 &= 85
     103:    4f 75 00 00 00 00 00 00    r5 |= r7                            r5 = ((r5 & 01010101) << 1) | ((r5 >> 1) & 01010101)

     104:    61 27 00 00 00 00 00 00    r7 = *(u32 *)(r2 + 0)    // Offset of (&(0x00000056) + r2)
     105:    1d 57 c8 ff 00 00 00 00    if r7 == r5 goto -56 <LBB0_13>
     106:    b7 03 00 00 00 00 00 00    r3 = 0
     107:    05 00 c6 ff 00 00 00 00    goto -58 <LBB0_13>

0000000000000360 <LBB0_16>:
     108:    18 01 00 00 98 00 00 00 00 00 00 00 00 00 00 00    r1 = 152 ll // Offset of "Key is incorrect"
     110:    b7 02 00 00 12 00 00 00    r2 = 18

0000000000000378 <LBB0_17>:
     111:    85 00 00 00 06 00 00 00    call <trace_printk>

0000000000000380 <LBB0_18>:
     112:    b7 00 00 00 00 00 00 00    r0 = 0
     113:    95 00 00 00 00 00 00 00    exit

Analysis

가장 큰 블럭인 LBB0_9에서 수행하는 작업을 살펴보면 uint8 형태의 데이터를 비트 단위로 뒤집고 있는 모습을 확인할 수 있다.

`readonly` section 같은 영역이다.

Program tracing

Condition to die

프로그램의 흐름을 확인해 보면 LBB0_16에서 죽는 메시지를 띄우고 있다. 해당 레이블까지 도달하기 위한 각 레지스터들의 상태를 생각해 보면 다음과 같다.

1. LBB0_9의 105th opcode에서 r7 == r5를 만족시키지 못함
2. LBB0_9의 106th opcode에서 r3 = 0 수행 후 LBB0_13으로 점프
3. LBB0_13에서 LBB0_14로 점프 없이 흘러감
4. LBB0_14의 60th opcode에서 r3 == 0으로 점프
5. LBB0_16 도달, 이후 점프 없이 수행되는 과정은 trace_printk("Key is incorrect", 0x12);

Register setting

결국 처음에 죽지 않기 위해 r7 == r5를 만족시켜야 하며, 각 값들은 다음과 같다.

  • r7: LBB0_9의 104th opcode에서 r7 = *(u32 *)(r2 + 0) 으로 설정한다.
    • r2의 초기화는 weird_function의 37th opcode에서 r2 = 19로 설정한다. 앞의 이미지의 Enter your key로 시작하는 read-only section의 19th index를 가리킨다.
    • 각 loop마다 방문하는 LBB0_13의 50th opcode에서 r2 += 4를 수행해 4byte size의 array를 순회함을 확인할 수 있다.
  • r5: 입력된 각 uint8 크기를 LBB0_9에서 bit 순서를 뒤집어 나온 결과이다.

Solution

Read-only에 꽂혀있는 메모리를 uint32씩 읽어와서 uint8로 LSB만 살려서 Casting, 이후 bit pattern을 뒤집으면 된다.

def revbits(x: int) -> int:
    return int(f"{x:08b}"[::-1], 2)

mat = [      0x56, 0xae, 0xce,
       0xec, 0xfa, 0x2c, 0x76,
       0xf6, 0x2e, 0x16, 0xcc,
       0x4e, 0xfa, 0xae, 0xce,
       0xcc, 0x4e, 0x76, 0x2c,
       0xb6, 0xa6, 0x02, 0x46,
       0x96, 0x0c, 0xce, 0x74,
       0x96, 0x76]

print("".join([chr(revbits(x)) for x in mat]))

 

FLAG: bi0sctf{jus7_4noth3r_us3rn4me@bi0s.in}

baebpf

진짜 양심이 없던 문제.

  • BPF로 하나만 나오는 것 같지만 ->
  • 짜잔! 스테이지가 두개입니다. (여기서 1차로 혈압이 오름) ->
  • Stage 2에서 암호화된거 이악물고 복호화하면 결과가 시간복잡도 말아먹은 Python code임 (...) ->
  • 기초적 PS적 지식으로 시간복잡도 낮춰서 실행하면 나오는게 Flag.

작성중.

'Writeups' 카테고리의 다른 글

[dicectf 2024] writeups  (0) 2024.02.06
[insomnihack 2024] writeups  (0) 2024.01.26
[securinetsCTF 2023] tomojiksi  (0) 2024.01.16
[dreamhack.io] multipoint2  (0) 2024.01.11