Moritz Finke · Blog

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.

Description of the Chat-Server challenge

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 debugging:
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...