ECW 2022 - UEFI

UEFI is a reverse-engineering challenge I designed for the 2022 edition of the European Cyber Week. Here is the source code for this challenge.

You’re given a disk.img file and the following qemu command line:

1qemu-system-x86_64 -cpu qemu64 \
2  -drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \
3  -drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
4  -drive format=raw,file=disk.img,if=virtio \
5  -net none \
6  -nographic \
7  -serial mon:stdio \
8  -monitor telnet::45454,server,nowait \

This launches an EFI app asking for a password:

First stage

Now let’s find out what’s inside the disk.img file:

1$ md5sum disk.img 
21b7217d4df99afc1c19535c188af9e64  disk.img
3$ binwalk --dd=".*" disk.img
4
5DECIMAL       HEXADECIMAL     DESCRIPTION
6--------------------------------------------------------------------------------
71788416       0x1B4A00        Microsoft executable, portable (PE)
81792965       0x1B5BC5        Unix path: /home/valkheim/workspace/ecw_uefi/edk2/MdePkg/Library/BasePrintLib/PrintLibInternal.c
91793612       0x1B5E4C        Unix path: /home/valkheim/workspace/ecw_uefi/edk2/MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.c

This output should give you somes hints about what’s inside the disk.img file. When reverse engineering the PE at 0x1B4A00, we find that it’s a dropper for another efi app. A first stage efi app is extracting and loading a second stage app.

To retrieve the second stage, you have two options:

  • dynamically by breaking after the decompression process. From there dump the memory region associated with the second EFI application
  • statically by extracting the compressed/encoded buffer from the first app and decompress manually

Here, I only show the decompression / decoding process for the static way:

First, locate the second stage (help yourself from xrefs):

Second stage

Then, extract compressed/encoded second stage app:

1$ dd if=_disk.img.extracted/1B4A00 of=compressed_efi_app.bin bs=1 count=936 skip=6336 
2$ md5sum compressed_efi_app.bin 
3287161b4938cff80961af8ab97b26831  compressed_efi_app.bin

Finally, decompress:

 1$ python3.8
 2Python 3.8.10 (default, Jun 22 2022, 20:18:18) 
 3[GCC 9.4.0] on linux
 4Type "help", "copyright", "credits" or "license" for more information.
 5>>> import EfiCompressor
 6>>> with open("compressed_efi_app.bin", "rb") as fh:
 7...     data = bytearray(fh.read())
 8...     data_len = len(data)
 9... 
10>>> EfiCompressor.UefiDecompress.__doc__
11'Decompress(): Decompress data using UEFI standard algorithm\n'
12>>> decompressed = EfiCompressor.UefiDecompress(data, data_len)
13>>> with open("decompressed.bin", "wb") as fh:
14...     fh.write(decompressed)
15... 
162048
17>>> quit()

I used the python-eficompressor package which is a python wrapper for the standard decompression algorithm.

The standard UEFI decompression is the lzma one, not the tiano custom one. There exists a reference implementation shipped with the UEFI specification.

Decode:

 1$ python3.8
 2Python 3.8.10 (default, Jun 22 2022, 20:18:18)
 3[GCC 9.4.0] on linux
 4Type "help", "copyright", "credits" or "license" for more information.
 5>>> with open("decompressed.bin", "rb") as fh:
 6...     data = bytearray(fh.read())
 7...
 8>>> for i in range(len(data)):
 9...     data[i] = data[i] ^ 0x71
10...
11>>> with open("stage2.efi", "wb") as fh:
12...     fh.write(data)
13...
142048
15>>> quit()

When decompressed, we should have another EFI application:

1$ file stage2.efi
2stage2.efi: PE32+ executable (DLL) (EFI application) EFI byte code, for MS Windows

The second stage is an EFI byte code stage, yay! EFI byte code virtual machine is well defined in the specification. Hence, you should be able to decode the risc and understand the password-asking procedures.

You can also pick a tool to do this for you (e.g. https://github.com/Pebaz/spore). Here is a spore dump I annotated for convenience:

  1$ spore stage2.efi
  2Print:
  3        79 01 C0 12  MOVRELw R1, 4800
  4              20 91  MOVq R1, @R1
  5        72 91 85 21  MOVnw R1, @R1(+5, +24)
  6        B5 08 10 00  PUSHn @R0(+0, +16)
  7              35 01  PUSHn R1
  8  83 29 01 00 00 10  CALL32EXa @R1(+1, +0)
  9        60 00 02 10  MOVqw R0, R0(+2, +0)
 10                 04  RET
 11
 12 Gtfo:
 13        79 01 A4 12  MOVRELw R1, 4772
 14              32 96  MOVn R6, @R1
 15        72 E6 88 21  MOVnw R6, @R6(+8, +24)
 16        77 31 02 00  MOVIqw R1, 2
 17        77 32 00 00  MOVIqw R2, 0
 18        77 33 00 00  MOVIqw R3, 0
 19              35 03  PUSHn R3
 20              35 03  PUSHn R3
 21              35 02  PUSHn R2
 22              35 01  PUSHn R1
 23  83 2E 8A 01 00 10  CALL32EXa @R6(+10, +24)
 24        60 00 04 20  MOVqw R0, R0(+4, +0)
 25                 04  RET
 26
 27EfiMain:
 28        79 01 7A 12  MOVRELw R1, 4730
 29        72 89 41 10  MOVnw @R1, @R0(+1, +16)
 30
 31              20 91  MOVq R1, @R1
 32        72 91 85 21  MOVnw R1, @R1(+5, +24)
 33        79 02 A6 12  MOVRELw R2, 4774
 34              6B 02  PUSH64 R2
 35              35 01  PUSHn R1
 36  83 29 01 00 00 10  CALL32EXa @R1(+1, +0)
 37        60 00 02 10  MOVqw R0, R0(+2, +0)
 38
 39              56 22  XOR64 R2, R2               ; init password counter
 40              6B 02  PUSH64 R2                  ; save counter
 41
 42ST->ConIn->Reset()
 43        79 01 56 12  MOVRELw R1, 4694
 44              20 91  MOVq R1, @R1
 45        72 91 63 10  MOVnw R1, @R1(+3, +24)
 46        77 32 00 00  MOVIqw R2, 0
 47              35 02  PUSHn R2
 48              35 01  PUSHn R1
 49              03 29  CALL32EXa @R1
 50              36 01  POPn R1
 51              36 02  POPn R2
 52
 53BS->WaitForEvent()
 54        79 03 3E 12  MOVRELw R3, 4670
 55              20 B3  MOVq R3, @R3
 56        72 B3 89 21  MOVnw R3, @R3(+9, +24)
 57        79 02 3C 12  MOVRELw R2, 4668
 58              35 02  PUSHn R2
 59        60 11 02 10  MOVqw R1, R1(+2, +0)
 60              6B 01  PUSH64 R1
 61        77 31 01 00  MOVIqw R1, 1
 62              35 01  PUSHn R1
 63  83 2B 89 01 00 10  CALL32EXa @R3(+9, +24)
 64        60 00 03 10  MOVqw R0, R0(+3, +0)
 65
 66ST->ConIn->ReadKeyStroke()
 67        79 01 18 12  MOVRELw R1, 4632
 68              20 91  MOVq R1, @R1
 69        72 91 63 10  MOVnw R1, @R1(+3, +24)
 70        79 02 5E 12  MOVRELw R2, 4702
 71              35 02  PUSHn R2
 72              35 01  PUSHn R1
 73  83 29 01 00 00 10  CALL32EXa @R1(+1, +0)
 74        60 00 02 10  MOVqw R0, R0(+2, +0)
 75
 76ST->ConIn->Reset()
 77        79 01 FC 11  MOVRELw R1, 4604
 78              20 91  MOVq R1, @R1
 79        72 91 63 10  MOVnw R1, @R1(+3, +24)
 80        77 32 00 00  MOVIqw R2, 0
 81              35 02  PUSHn R2
 82              35 01  PUSHn R1
 83              03 29  CALL32EXa @R1
 84        60 00 02 10  MOVqw R0, R0(+2, +0)
 85
 86ST->ConOut->OutputString(conout, L"*")
 87        77 11 00 00  MOVIww R1, 0
 88              6B 01  PUSH64 R1
 89        77 11 2A 00  MOVIww R1, 42              ; print a '*' for a key read
 90              6B 01  PUSH64 R1
 91        77 11 00 00  MOVIww R1, 0
 92              6B 01  PUSH64 R1
 93        60 01 01 10  MOVqw R1, R0(+1, +0)
 94              6B 01  PUSH64 R1
 95        79 01 CC 11  MOVRELw R1, 4556
 96              20 91  MOVq R1, @R1
 97        72 91 85 21  MOVnw R1, @R1(+5, +24)
 98              35 01  PUSHn R1
 99  83 29 01 00 00 10  CALL32EXa @R1(+1, +0)
100        60 00 05 20  MOVqw R0, R0(+5, +0)
101
102              36 02  POPn R2                    ; get counter
103
104Get unicode char of key
105        79 01 04 12  MOVRELw R1, 4612
106        72 11 02 00  MOVnw R1, R1 2
107              1E 94  MOVw R4, @R1
108
109Get current flag char
110        79 05 E6 0E  MOVRELw R5, 3814           ; data/preflag base addr
111        77 36 80 00  MOVIqw R6, 128
112        77 33 02 00  MOVIqw R3, 2
113              4E 36  MUL64 R6, R3
114              4E 36  MUL64 R6, R3               ; offset by 2*2*128
115              4E 23  MUL64 R3, R2
116              4C 35  ADD64 R5, R3
117              4C 65  ADD64 R5, R6
118              1E D5  MOVw R5, @R5               ; flag[2*i]
119
120Junk (1+3+3=7)
121        77 31 01 00  MOVIqw R1, 1
122              4C 13  ADD64 R3, R1
123              4C 31  ADD64 R1, R3
124              4C 31  ADD64 R1, R3
125        6D 01 07 00  CMPI64weq R1, 7
126              82 00  JMP8cc 0
127
128Cmp flag char value
129        77 33 04 00  MOVIqw R3, 4               ; Flag rot
130              4C 35  ADD64 R5, R3
131              45 54  CMPeq64 R4, R5
132              82 8F  JMP8cc -113
133
134Test length
135        77 33 01 00  MOVIqw R3, 1
136              4C 32  ADD64 R2, R3
137        6F 02 18 00  CMPI64wgte R2, 24
138              82 8A  JMP8cc -118
139
140Goodboy
141        79 01 7C 11  MOVRELw R1, 4476
142              6B 01  PUSH64 R1
143  83 10 A0 FE FF FF  CALL32 R0 -352
144              6C 01  POP64 R1
145
146  83 10 B4 FE FF FF  CALL32 R0 -332             ; goto gtfo
147
148                 04  RET
149
150Then some data (3814)
Note
The UEFI specification will help you identify the system table as well as the boot and runtime services tables used here.

At this point we identified and isolated th ebasic blocks responsible for the flag decoding (paying attention to utf-16). What we can do is to rot/grep the efi app. I’ll do it on the data section specifically:

 1$ dd if=stage2.efi of=stage2_data.bin bs=1 count=702 skip=1024
 2702+0 records in
 3702+0 records out
 4702 bytes copied, 0,00406523 s, 173 kB/s
 5$ xxd stage2_data.bin
 600000000: 7800 3000 6300 5600 2400 3200 6500 6b00  x.0.c.V.$.2.e.k.
 700000010: 4600 3200 5100 6900 7a00 7600 3600 5e00  F.2.Q.i.z.v.6.^.
 800000020: 6f00 7900 7100 5e00 7000 5500 4800 4b00  o.y.q.^.p.U.H.K.
 900000030: 5500 6700 4600 6a00 3100 4a00 6400 5f00  U.g.F.j.1.J.d._. ;
1000000040: 5f00 5600 3400 4c00 4b00 5700 3400 3500  _.V.4.L.K.W.4.5. ; :eyes:
1100000050: 4800 3300 5200 3300 5f00 5f00 5100 7600  H.3.R.3._._.Q.v. ;
1200000060: 4e00 3300 4000 7300 4d00 7700 4700 6500  N.3.@.s.M.w.G.e.
1300000070: 5700 7700 3000 5600 4b00 4200 5900 4600  W.w.0.V.K.B.Y.F.
1400000080: 7a00 5200 6200 7600 6900 7100 3600 7500  z.R.b.v.i.q.6.u.
1500000090: 2300 3700 5200 4100 3900 4100 7200 6e00  #.7.R.A.9.A.r.n.
16000000a0: 4d00 3800 5800 4400 4900 4500 4500 7600  M.8.X.D.I.E.E.v.
17000000b0: 4800 5100 2600 4800 4700 5400 4000 5300  H.Q.&.H.G.T.@.S.
18000000c0: 7600 2600 4c00 5500 5a00 6400 6200 3400  v.&.L.U.Z.d.b.4.
19000000d0: 4200 4600 3600 2500 3200 5f00 3400 6400  B.F.6.%.2._.4.d.
20000000e0: 6300 6900 3300 3300 3500 3900 3500 5e00  c.i.3.3.5.9.5.^.
21000000f0: 5600 5a00 5100 6500 6f00 6a00 6900 5e00  V.Z.Q.e.o.j.i.^.
2200000100: 7a00 5e00 7500 6300 5000 5600 6800 6300  z.^.u.c.P.V.h.c.
2300000110: 2300 2600 6300 5400 3600 2300 4e00 4800  #.&.c.T.6.#.N.H.
2400000120: 3000 5e00 3900 3700 4f00 2400 3700 5700  0.^.9.7.O.$.7.W.
2500000130: 7100 6f00 6600 4d00 3300 7000 4800 7000  q.o.f.M.3.p.H.p.
2600000140: 7900 4d00 7300 5900 3400 5700 6500 5400  y.M.s.Y.4.W.e.T.
2700000150: 7400 5300 2600 6500 6500 4e00 7700 7100  t.S.&.e.e.N.w.q.
2800000160: 3400 3600 3600 6b00 5600 3600 5f00 5f00  4.6.6.k.V.6._._.
2900000170: 4700 4800 4700 3700 6500 2600 5300 2600  G.H.G.7.e.&.S.&.
3000000180: 5200 6500 7500 4f00 3300 3500 3300 7000  R.e.u.O.3.5.3.p.
3100000190: 7600 5e00 5500 7000 7000 4c00 6400 3500  v.^.U.p.p.L.d.5.
32000001a0: 2a00 2400 3500 2100 5400 4400 5f00 5f00  *.$.5.!.T.D._._.
33000001b0: 6e00 6900 7000 6700 6400 7500 5a00 6400  n.i.p.g.d.u.Z.d.
34000001c0: 7800 7a00 7600 2300 6f00 4400 5700 6400  x.z.v.#.o.D.W.d.
35000001d0: 2600 4400 4600 4e00 7a00 5600 5700 4100  &.D.F.N.z.V.W.A.
36000001e0: 6d00 4f00 5f00 3700 6a00 4500 4800 3300  m.O._.7.j.E.H.3.
37000001f0: 3800 4400 4700 6200 2500 6400 6b00 4100  8.D.G.b.%.d.k.A.
3800000200: 4100 3f00 5300 7700 4100 4200 4500 5b00  A.?.S.w.A.B.E.[.
3900000210: 3e00 7500 7000 2f00 5b00 5f00 2c00 6000  >.u.p./.[._.,.`.
4000000220: 2f00 5b00 6e00 7100 6800 2f00 7600 7900  /.[.n.q.h./.v.y.
4100000230: 3400 5000 7200 6800 7300 7500 6c00 5000  4.P.r.h.s.u.l.P.
4200000240: 2500 7700 4d00 4e00 7000 6700 2600 3400  %.w.M.N.p.g.&.4.
4300000250: 6300 5200 5900 3700 5300 3800 7800 5e00  c.R.Y.7.S.8.x.^.
4400000260: 2100 5600 6500 7000 7400 6e00 3900 6b00  !.V.e.p.t.n.9.k.
4500000270: 4b00 5f00 5f00 5000 3800 4400 3300 6a00  K._._.P.8.D.3.j.
4600000280: 3400 3100 5600 2500 7100 6b00 7400 4200  4.1.V.%.q.k.t.B.
4700000290: 3700 6900 5f00 4c00 2600 5600 6900 4a00  7.i._.L.&.V.i.J.
48000002a0: 6400 7200 3100 2500 2300 5000 2600 4400  d.r.1.%.#.P.&.D.
49000002b0: 6800 7900 3400 4300 3300 4800 0000       h.y.4.C.3.H...

Apply the rot:

 1$ python
 2Python 3.10.7 (main, Sep  7 2022, 15:22:19) [GCC 9.4.0] on linux
 3Type "help", "copyright", "credits" or "license" for more information.
 4>>> with open("stage2_data.bin", "rb") as fh:
 5...     data = bytearray(fh.read())
 6...
 7>>> for i in range(len(data)):
 8...     if data[i] == 0x00:
 9...             continue
10...     data[i] = data[i] + 4
11...
12>>> with open("stage2_data_rot.bin", "wb") as fh:
13...     fh.write(data)
14...
15702
1$ strings -el stage2_data_rot.bin | grep ECW
2|4gZ(6ioJ6Um~z:bs}ubtYLOYkJn5NhccZ8PO[89L7V7ccUzR7DwQ{Ki[{4ZOF]J~Vfzmu:y';VE=EvrQ<\HMIIzLU*LKXDWz*PY^hf8FJ:)6c8hgm779=9bZ^Uisnmb~bygTZlg'*gX:'RL4b=;S(;[usjQ7tLt}Qw]8[iXxW*iiR{u8::oZ:ccKLK;i*W*ViyS797tzbYttPh9.(9%XHccrmtkhy^h|~z'sH[h*HJR~Z[EqSc;nIL7<HKf)hoEECW{EFI_Byt3_c0d3_rul3z}8TvlwypT){QRtk*8gV];W<|b%Zitxr=oOccT<H7n85Z)uoxF;mcP*ZmNhv5)'T*Hl}8G7L

That last command gave us the flag: ECW{EFI_Byt3_c0d3_rul3z}.

I hope you enjoyed it and learned some stuff! Go check the full source code of this challenge.