Reverse Engineering of a Chat-Server
December 5, 2018
This is a writeup for a CTF challenge hosted by the computer science chair III of the University of Würzburg. Originally intended to be an exercise completed over the course of a semester, I finished my writeup three days after the challenge was announced and won the challenge.
The challenge consists of four tasks that revolve around two binaries, server and client. When executed, the server allows multiple client connections. Users running client are assigned IDs and can communicate via text with other connected users.
Race Condition
The first task of this challenge is to create two clients with the same ID. The assigned ID is incremented with each new user and starts at 1. To understand, how two users may get assigned to the same ID, the server binary needs to be reverse engineered. As depicted in the disassembled functions below, the architecture is fairly simple. The function main is started at first, calling accept_loop. This function represents a loop that does a full iteration each time a new user joins. In each iteration, a separate thread is started, who runs the new_connection function.
main
method:
.text:00000000004032D8
.text:00000000004032D8
.text:00000000004032D8 ; Attributes: noreturn bp-based frame
.text:00000000004032D8
.text:00000000004032D8 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000000004032D8 public main
.text:00000000004032D8 main proc near
.text:00000000004032D8 push rbp
.text:00000000004032D9 mov rbp, rsp
.text:00000000004032DC mov esi, offset aStartingServer ; "===Starting Server==="
.text:00000000004032E1 mov edi, offset _ZSt4cout@@GLIBCXX_3_4
.text:00000000004032E6 call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<>(std::basic_ostream> &,char const*)
.text:00000000004032EB mov esi, offset __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ ; std::endl>(std::basic_ostream> &)
.text:00000000004032F0 mov rdi, rax
.text:00000000004032F3 call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:00000000004032F8 call accept_loop
.text:00000000004032F8 main endp
.text:00000000004032F8
accept_loop
method:
.text:000000000040325C
.text:000000000040325C loc_40325C:
.text:000000000040325C mov esi, offset aWaitingForConn ; "===Waiting for Connection==="
.text:0000000000403261 mov edi, offset _ZSt4cout@@GLIBCXX_3_4
.text:0000000000403266 call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<>(std::basic_ostream> &,char const*)
.text:000000000040326B mov esi, offset __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ ; std::endl>(std::basic_ostream> &)
.text:0000000000403270 mov rdi, rax
.text:0000000000403273 call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:0000000000403278 mov eax, [rbp+sock]
.text:000000000040327B mov edx, 0 ; addr_len
.text:0000000000403280 mov esi, 0 ; addr
.text:0000000000403285 mov edi, eax ; fd
.text:0000000000403287 call _accept
.text:000000000040328C mov [rbp+new_sock], eax
.text:000000000040328F lea rdx, [rbp+new_sock] ; int *
.text:0000000000403293 lea rax, [rbp+t]
.text:0000000000403297 mov esi, offset new_connection ; __f
.text:000000000040329C mov rdi, rax ; this
.text:000000000040329F call _ZNSt6threadC2IRFviEJRiEEEOT_DpOT0_ ; std::thread::thread(void (&)(int) &&,int &)
.text:00000000004032A4 lea rax, [rbp+t]
.text:00000000004032A8 mov rdi, rax ; this
.text:00000000004032AB call __ZNSt6thread6detachEv ; std::thread::detach(void)
.text:00000000004032B0 lea rax, [rbp+t]
.text:00000000004032B4 mov rdi, rax ; this
.text:00000000004032B7 call _ZNSt6threadD2Ev ; std::thread::~thread()
.text:00000000004032BC jmp short loc_40325C
.text:00000000004032BC accept_loop endp
.text:00000000004032BC
new_connection
method:
.text:0000000000402E62
.text:0000000000402E62
.text:0000000000402E62 ; Attributes: bp-based frame
.text:0000000000402E62
.text:0000000000402E62 ; void __cdecl new_connection(int sock)
.text:0000000000402E62 public new_connection
.text:0000000000402E62 new_connection proc near
.text:0000000000402E62
.text:0000000000402E62 sock= dword ptr -104h
.text:0000000000402E62 it= std::_Rb_tree_iterator > ptr -100h
.text:0000000000402E62 str= std::__cxx11::string ptr -0F0h
.text:0000000000402E62 msg= message_header ptr -0D0h
.text:0000000000402E62 entry= map_entry ptr -0B0h
.text:0000000000402E62 buf= std::__cxx11::string ptr -90h
.text:0000000000402E62 var_61= byte ptr -61h
.text:0000000000402E62 var_60= std::__cxx11::string ptr -60h
.text:0000000000402E62 __rhs= std::__cxx11::string ptr -40h
.text:0000000000402E62 __x= std::_Rb_tree_iterator >::_Self ptr -20h
.text:0000000000402E62
.text:0000000000402E62 push rbp
.text:0000000000402E63 mov rbp, rsp
.text:0000000000402E66 push r12
.text:0000000000402E68 push rbx
.text:0000000000402E69 sub rsp, 100h
.text:0000000000402E70 mov [rbp+sock], edi
.text:0000000000402E76 mov eax, cs:numUsers
.text:0000000000402E7C add eax, 1
.text:0000000000402E7F mov cs:numUsers,
... ...
For the task of assigning the same ID twice, only the first
section of the
new_connection function needs to be investigated. In that
part, the numUsers
variable
sticks out.
.text:0000000000402E76 mov eax, cs:numUsers
.text:0000000000402E7C add eax, 1
.text:0000000000402E7F mov cs:numUsers,
As we remember, the new_connection is run in separate
threads for every user. Threads share their memory and hence,
operations that affect shared variables should be atomic. This is
not the case for numUsers
. The variable
incrementation takes three operations, creating a race condition:
If one thread reads from numUsers
(0x402E76) before another thread has written their changes to the
variable (0x402E7F), there is no way that the first thread is able
to notice the change.
The race condition may occur rarely, but it can be enforced with gdb, as shown in the video below.
gdb server
(gdb) break new_connection
Breakpoint 1 at 0x402e76: file main.cpp, line 203.
(gdb) run
Starting program: server
===Starting Server===
===Waiting for Connection===
[New Thread 0x7ffff77ca700 (LWP 160471)]
===Waiting for Connection===
[Switching to Thread 0x7ffff77ca700 (LWP 160471)]
Thread 2 "server" hit Breakpoint 1, new_connection (sock=4)
(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff77cb740 (LWP 160466))]
(gdb) set scheduler-locking on
(gdb) continue
Continuing.
[New Thread 0x7ffff6fc9700 (LWP 160494)]
===Waiting for Connection===
[Switching to Thread 0x7ffff6fc9700 (LWP 160494)]
Thread 3 "server" hit Breakpoint 1, new_connection (sock=5)
(gdb) next
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff77ca700 (LWP 160471))]
(gdb) next
(gdb) set scheduler-locking off
(gdb) continue
Continuing.
client output:
./client
===Starting Client===
Username: 2
Verification: 12481149142591352243821512244822389222192
===Chat Connection Established===
===Send a Message===
Enter 1 to send a Direct Message or 2 to send a Broadcast Message or 3 to exit
---Processing Broadcast Message---
Sender:0 To:0 Message:
As can be observed in the video above, both clients are assigned the same username (2). This indicates that the race condition was successfully exploited.
Coming soon...
I solved all CTF tasks. Writeups for tasks 2 to 4 are coming soon...