Introduction
The challenge files can be downloaded here.
Challenge contains three files: partyplanning.strip, partyplanning.dump, and relocator.py
partyplanning.strip is the main binary, with less protection:
Features
Binary patching
When executing the binary it will ask you give two addresses, the binary will "patch" itself according to the address you input:
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 "insert" sleep() to the specific address, no registers will be affected.
The two files partyplanning.dump and relocator.py 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.
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); dlink.next = 0LL; dlink.prev = head->prev; if ( dlink.prev ) head->prev->next = &dlink; else head->next = &dlink; head->prev = &dlink; if ( global_lock ) unlock((int *)global_lock); unlock(&head->futex); 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 lock(&head->futex); dlink = head->next; if ( dlink ) { if ( dlink->prev ) dlink->prev->next = dlink->next; else head->next = dlink->next; if ( dlink->next ) dlink->next->prev = dlink->prev; else head->prev = dlink->prev; dlink->next = 0LL; dlink->prev = 0LL; } unlock(&head->futex); 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
- Alice: enqueue(Bob->head) // Alice starts to wait Bob
- 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, 0) because 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; sleep(2); dlink->hashval = hashval; sys_futex(&dlink->futex, 1, 1u, 0LL, 0LL, 0);
Then the execution flow would become:
Exploit
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)
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:
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}
- Alice: enqueue(Bob->head)
- sleep(1) before sys_futex
- Bob: dequeue(Bob->head)
- dlink->futex = 1;
- sleep(2)
- Alice: Wake up from sleep(1) and sys_futex has no effect
- continue to execute ANOTHER function
- Bob: Wake up from sleep(2)
- dlink->hashval = hashval; change a value on Alice's stack!
Exploit
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, ...) { /* <deleted> */ fgets(s, 48, stdin); // <----- Alice is waiting here! lb = strchr(s, 10); if ( lb && lb != s ) { *lb = 0; dest = strdup(s); } else { dest = (char *)malloc(0x30uLL); do { if ( lb ) *lb = 0; strcpy(dest, s); if ( lb && lb != s ) break; if ( !fgets(s, 48, stdin) ) break; 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:
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}
沒有留言:
張貼留言