지금까지 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
형태의 데이터를 비트 단위로 뒤집고 있는 모습을 확인할 수 있다.
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 |