prettify code

2017年4月25日 星期二

[Write-up] PlaidCTF 2017 - pwn400 Plaid Party Planning


The challenge files can be downloaded here.

Challenge contains three files: partyplanning.strippartyplanning.dump, and

partyplanning.strip is the main binary, with less protection:


Binary patching

When executing the binary it will ask you give two addresses, the binary will "patch" itself according to the address you input:

$ ./partyplanning.strip
Before we start, where would you like to help out the most? 401457<enter>
And second most? 401335<enter>
All right. We can help out 0x401457
All right. We can help out 0x401335

The patching for both addresses, in brief is just jump to sleep() and jump back. And the two addresses you specified will become sleep(2) and sleep(1), respectively. Patching can be thought as "insertsleep() to the specific address, no registers will be affected.

The two files partyplanning.dump and are helpers for patching, no need to care them.

Main feature

After patching, it will ask you input five names. Then it will start five threads to "prepare and hold a party".

The five characters will do their jobs like prepare food, music, or decorations. And there're approximate 20 futex locks for threads to work.

It's a bad idea to list what these five characters actually do. Let's move to describe the vulnerability.


Obviously, the patching feature clues us to find race-condition vulnerabilities.

We found some useless vulnerabilities that will lead to deadlock, but since they're not pwnable so we'll not discuss them here.

Let's introduce two functions before show where the vulnerability is.

Function at 0x401277:
// dlink on stack
__int64 __fastcall enqueue(LinkList *head, volatile signed __int32 *global_lock)
  __int64 result; // rax@11
  __int64 v3; // rcx@11
  Dlink dlink; // [sp+20h] [bp-30h]@1
  __int64 v5; // [sp+48h] [bp-8h]@1

  v5 = *MK_FP(__FS__, 40LL);
  dlink.futex = 0;
  dlink.prev = 0LL;
  lock(&head->futex); = 0LL;
  dlink.prev = head->prev;
  if ( dlink.prev )
    head->prev->next = &dlink;
    head->next = &dlink;
  head->prev = &dlink;
  if ( global_lock )
    unlock((int *)global_lock);
  while ( !dlink.futex && !(unsigned int)sys_futex(&dlink.futex, 0, 0, 0LL, 0LL, 0) )
  if ( global_lock )
    lock((int *)global_lock);
  result = dlink.hashval;
  v3 = *MK_FP(__FS__, 40LL) ^ v5;
  return result;

Function at 0x401392:
void __fastcall dequeue(LinkList *head, __int64 hashval)
  Dlink *dlink; // [sp+18h] [bp-8h]@1

  dlink = head->next;
  if ( dlink )
    if ( dlink->prev )
      dlink->prev->next = dlink->next;
      head->next = dlink->next;
    if ( dlink->next )
      dlink->next->prev = dlink->prev;
      head->prev = dlink->prev;
    dlink->next = 0LL;
    dlink->prev = 0LL;
  if ( dlink )
    dlink->futex = 1;
    dlink->hashval = hashval;
    sys_futex(&dlink->futex, 1, 1u, 0LL, 0LL, 0);

An example usage of these two functions is shown as follows:
Two threads, named them Alice and Bob

  1. Alice: enqueue(Bob->head) // Alice starts to wait Bob
  2. Bob: dequeue(Bob->head) // Alice's lock released by Bob
When Alice called enqueue, she should wait at sys_futex(&dlink.futex, 0, 0, 0LL, 0LL, 0because it's a lock syscall.
And when Bob called dequeue, a hash value would be set on Alice's stack (dlink->hashval = hashval) and Alice would back from the syscall.

So, what if we make(patch) Alice sleep BEFORE she calls sys_futex?
Bob will call dequeue when Alice is sleeping, dlink->futex = 1 would be executed. So Alice will not be blocked by sys_futex!

And one more thing, we also patch the dequeue function to be:
dlink->futex = 1;
dlink->hashval = hashval;
sys_futex(&dlink->futex, 1, 1u, 0LL, 0LL, 0);

Then the execution flow would become:

  1. Alice: enqueue(Bob->head)
    • sleep(1) before sys_futex
  2. Bob: dequeue(Bob->head)
    • dlink->futex = 1;
    • sleep(2)
  3. Alice: Wake up from sleep(1) and sys_futex has no effect
    • continue to execute ANOTHER function
  4. Bob: Wake up from sleep(2)
    • dlink->hashval = hashval; change a value on Alice's stack!


So we patch two addresses at 0x401457 and 0x401335, while this wouldn't cause any segmentation fault. After tracing, Alice is waiting input in function 0x401771 when Bob changes Alice's stack value:
(part of function 0x401771)
char *printf_and_read(Person *a1, char *a2, ...)
  fgets(s, 48, stdin);  // <----- Alice is waiting here!
  lb = strchr(s, 10);
  if ( lb && lb != s )
    *lb = 0;
    dest = strdup(s);
    dest = (char *)malloc(0x30uLL);
      if ( lb )
        *lb = 0;
      strcpy(dest, s);
      if ( lb && lb != s )
      if ( !fgets(s, 48, stdin) )
      lb = strchr(s, 10);
    while ( s[0] );
  /* <deleted> */

The variable changed by Bob is dest. Since dest will be set by either strdup or malloc, no segmentation fault would be raised.

But the solution is easy, make Alice wait at another fgets  - after malloc , then segfault will be raised inside strcpy(dest, s) !

Proof of Concept:
$ cat in
                                <---- must have one blank line

$ (cat in;cat) | ./partyplanning.strip
Before we start, where would you like to help out the most? And second most? All right. We can help out 0x401457
All right. We can help out 0x401335
We bucket people into at most 31 bins
Who is helping out today?
Person 1: What a bucket e64f9343
Person 2: What a bucket d960287d
Person 3: What a bucket cd86f7a0
Person 4: What a bucket 6df654c8
Person 5: What a bucket 1e98dff6
Alice: I really hate planning parties
david942j: Can't wait to get going
WWWW: Can't wait to get going
Bob: Can't wait to get going
QQpie: Can't wait to get going
david942j: This party should really happen in Pittsburgh
Bob: We really should have food be easy to pick up
QQpie: What kind of music is your favorite?
Bob: What food from around Pittsburgh do you want?
Alice: Is FOOD really what you want to eat? [y/N]y<enter>

28557 segmentation fault (core dumped)  ./partyplanning.strip

dest will be changed to a hash value of name (Bob), so we have an arbitrary 4-byte address write. We change the value of free_got to system("/bin/sh") (already present in binary) and get shell ;)

Last thing is find a name that has hash value equals to 0x604218 (free_got), all we need is brute force.

exploit script: github

flag: PCTF{4nd_th4ts_why_w3_d0nt_p14n_p4rt13s_1n_p4r4113l}