[Codegate 2014] membership (800pt pwnable) write-up

This is a write-up for 800 point pwnable challenge called ‘membership’ from Codegate CTF 2014 Pre-qual round. PPP was the only solver for this challenge during the competition, so I have decided to do a write-up for the challenge. Enjoy. (awesie and ricky solved it during the competition.)

1. Challenge overview

You can download the copy of the binary here.
During the competition, we could ssh into one of their machines to exploit and read the flag.

As you can see, it’s a 32-bit ELF binary. So, let’s open it up in IDA and start reversing.

The program looks really simple. It just installs a couple signal handlers (specifically for SIGSEGV and SIGFPE) and calls a main (interesting) function, where it prompts us for the userid and password. Then, the program calculates SHA256 hash of the given password and compares with the stored hash. If the password hashes do not match, a runtime error exception is thrown and the program is aborted. If we pass in the correct password, we get a shell :)

Since guessing the correct password or cracking the hash is not viable option for us, we try to locate some other bugs that can be useful.

As highlighted above, the program dereferences a null pointer (*0 = 0) if the length of the password is greater than or equal to 16 bytes. Obviously it is going to trigger SIGSEGV, but do you remember what we said earlier about installing the signal handlers? And yes, one of them was SIGSEGV handler.

So, instead of crashing it miserably, the handler will be called.
Let’s examine what this handler does.

SIGSEGV handler installer

Now, if we look at the SIGSEGV_handler, we may think it doesn’t really do anything useful.
Note that it just fills up exception information and calls __cxa_throw to throw exception.

At this point, we could go on and explain what SIGFPE_handler does as well, but we’ll skip it since it’s not that interesting and is not needed for a successful exploitation.
You may ask… so, what’s left?

2. Vulnerability

Notice that this is a C++ program with exception throwing. We should check how C++ exception handling works.

It uses a thing called, DWARF, which is a standardized debugging data format for unwinding the stack and handling exceptions.

There was a CTF problem in the past that involved DWARF (called Khazad from Ghost in the Shellcode 2012): Check out these write-ups if you are interested!

Take a close look at the entry with “pc=08048fa7..0804904d”.
This entry basically describes what should happen when the exception is thrown between that PC range. Note that the SIGSEGV_handler throws an exception at
0x0804901A , which is in that range (that range is precisely SIGSEGV_handler function).

Ok. Now, we have to make sense of what all those operations mean :)
DW_CFA_val_expression contains CFA expressions that are defined here.

Luckily, it’s not that hard to understand the expressions. We can simply think of it as a stack machine:

So, in short, it checks if the username is “stdchpie” and the password[2:5] is equal to “\xb1\x2e\x40”.
If any of the condition fails, it transfers execution to
0x8048f18 , which does
exit(0) .

What happens if we satisfy the conditions? Good question.
It basically dumps us to the following code:

1

2

3

4

5

6

7

8

9

10

11

.text:08048CE8mov[esp],eax

.text:08048CEBcall___cxa_begin_catch

.text:08048CF0movdwordptr[esp],offsetaNested; "nested"

.text:08048CF7call_puts

.text:08048CFCmoveax,(offsetpassword_buf+1)

.text:08048D01moveax,[eax]

.text:08048D03movedx,(offsetpassword_buf+1)

.text:08048D08movedx,[edx+4]

.text:08048D0Bmov[eax],edx

.text:08048D0Dcall___cxa_end_catch

.text:08048D12jmpshortloc_8048CC0

This code prints out “nested” string and writes password[5:9] to *password[1:5]. Meaning, we get to write anything in
0x402eb1?? address space with any 4 byte value we choose. 4-byte write is pretty strong tool in exploitation, but when we are limited to 256 byte range, it’s difficult to make it useful. Also, it immediately jumps to
0x8048cc0 , where it does another null pointer dereference causing SIGSEGV to happen — thus, we get infinite ‘nested’ string printed out.

During the competition, we chose each data structure of interest and traced backwards to find out whether by controlling said structure we can influence anything (e.g. function pointer) on callers while handling exceptions to hijack the control flow.

Since we now know which one can be used to control EIP, we will start from there: frame_hdr_cache_head is our target. [It is very well be possible to solve the challenge with different method/structure, but this is the one that we ended up using during the CTF.]

If we locate the place that frame_hdr_cache_head is referenced, we land in the middle of _Unwind_IteratePhdrCallback function in libgcc/unwind-dw2-fde.dip.c.

unwind-dw2-fde-dip.c

C

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

...(omitted)...

/* Find data->pc in shared library cache.

Set load_base, p_eh_frame_hdr and p_dynamic

plus match from the cache and goto

"Read .eh_frame_hdr header." below. */

structframe_hdr_cache_element*cache_entry;

for(cache_entry=frame_hdr_cache_head;

cache_entry;

cache_entry=cache_entry->link)

{

if(data->pc>=cache_entry->pc_low

&&data->pc<cache_entry->pc_high)

{

load_base=cache_entry->load_base;

p_eh_frame_hdr=cache_entry->p_eh_frame_hdr;

p_dynamic=cache_entry->p_dynamic;

/* And move the entry we're using to the head. */

if(cache_entry!=frame_hdr_cache_head)

{

prev_cache_entry->link=cache_entry->link;

cache_entry->link=frame_hdr_cache_head;

frame_hdr_cache_head=cache_entry;

}

gotofound;

}

...(omitted)...

frame_hdr_cache_head points to the first element of a singly linked list that contains frame_hdr_cache_element(s).
The code iterates through the list and finds the entry for
data->pc in cache.
data->pc is the program counter of the frame we are trying to handle the exception for.

This cache is filled in as the program discovers exception handler frames (eh_frame).

The following is the struct definition for frame_hdr_cache_element:

1

2

3

4

5

6

7

8

9

staticstructframe_hdr_cache_element

{

_Unwind_Ptr pc_low;

_Unwind_Ptr pc_high;

_Unwind_Ptr load_base;

constElfW(Phdr)*p_eh_frame_hdr;

constElfW(Phdr)*p_dynamic;

structframe_hdr_cache_element*link;

}frame_hdr_cache[FRAME_HDR_CACHE_SIZE];

So, if we control where frame_hdr_cache_head points to, we can also construct/control the elements inside. Before we dive into what happens when we find an element in the cache and ‘goto found‘, let’s step back for a minute and see if we can even get to here and what that allows us to do.

The function we just looked at (_Unwind_IteratePhdrCallback) is called from _Unwind_Find_FDE in unwind-dw2-fde-dip.c.
Then, _Unwind_Find_FDE function is called from uw_frame_state_for function in unwind-dw2.c.uw_frame_state_for function is called from _Unwind_RaiseException function in unwind.inc, which provides an interface to raise an exception given an exception object.

Where does _Unwind_RaiseException get called, then?
It gets called by __cxa_throw, and if you remember, our SIGSEGV_handler invokes this function to raise an exception.

Alright. We now have confirmed that we can get to that code by causing the binary to throw an exception and letting libgcc unwinds/handles the exception.

But is there anything interesting in this code path such that we can give us EIP control? Yes.

Let’s review _Unwind_RaiseException a little bit:

unwind.inc

C

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

...(omitted)...

/* Raise an exception, passing along the given exception object. */

_Unwind_Reason_Code LIBGCC2_UNWIND_ATTRIBUTE

_Unwind_RaiseException(struct_Unwind_Exception*exc)

{

struct_Unwind_Context this_context,cur_context;

_Unwind_Reason_Code code;

/* Set up this_context to describe the current stack frame. */

uw_init_context(&this_context);

cur_context=this_context;

/* Phase 1: Search. Unwind the stack, calling the personality routine

with the _UA_SEARCH_PHASE flag set. Do not modify the stack yet. */

while(1)

{

_Unwind_FrameState fs;

/* Set up fs to describe the FDE for the caller of cur_context. The

first time through the loop, that means __cxa_throw. */

code=uw_frame_state_for(&cur_context,&fs);

if(code==_URC_END_OF_STACK)

/* Hit end of stack with no handler found. */

return_URC_END_OF_STACK;

if(code!=_URC_NO_REASON)

/* Some error encountered. Usually the unwinder doesn't

diagnose these and merely crashes. */

return_URC_FATAL_PHASE1_ERROR;

/* Unwind successful. Run the personality routine, if any. */

if(fs.personality)

{

code=(*fs.personality)(1,_UA_SEARCH_PHASE,exc->exception_class,

exc,&cur_context);

if(code==_URC_HANDLER_FOUND)

break;

elseif(code!=_URC_CONTINUE_UNWIND)

return_URC_FATAL_PHASE1_ERROR;

}

/* Update cur_context to describe the same frame as fs. */

uw_update_context(&cur_context,&fs);

}

/* Indicate to _Unwind_Resume and associated subroutines that this

is not a forced unwind. Further, note where we found a handler. */

exc->private_1=0;

exc->private_2=uw_identify_context(&cur_context);

cur_context=this_context;

code=_Unwind_RaiseException_Phase2(exc,&cur_context);

if(code!=_URC_INSTALL_CONTEXT)

returncode;

uw_install_context(&this_context,&cur_context);

}

...(omitted)...

Notice the highlighted lines. What do you see?

A function pointer getting called! And we *may* be able to control
fs.personality .
Let’s find out!

unwind-dw2.c

C

1160

1161

1162

1163

1164

1165

1166

1167

1168

1169

1170

1171

1172

1173

1174

1175

1176

1177

1178

1179

1180

1181

1182

1183

1184

1185

1186

1187

1188

1189

1190

1191

1192

1193

1194

1195

1196

1197

1198

1199

1200

1201

1202

...(omitted)...

/* Given the _Unwind_Context CONTEXT for a stack frame, look up the FDE for

Remember that the struct pointer that we are interested in tracing is fs (aka 2nd argument).
Wee see here that _Unwind_Find_FDE is used to get fde (which is used to get cie), and extract_cie_info takes cie and fs as its first and third argument, respectively.

So, what happens in extract_cie_info?

unwind-dw2.c

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

...(omitted)...

378/* Extract any interesting information from the CIE for the translation

379 unit F belongs to. Return a pointer to the byte after the augmentation,

As we can see in action, this payload triggers the bug and causes infinite SIGSEGV.
We currently chose 0x402eb101 for no particular reason, but we can see that memory is successfully written.

4-2. cache_entry & p_eh_frame_hdr construction

Now, we overwrite frame_hdr_cache_head to point to our stdin buffer.

We are going to start building fake structs from our buffer + 0x1c.

So what values should we use?
To not worry about the search too much, we are going to set pc_low to 0x0 and pc_high to 0xFFFFFFFF. This basically says that this cache entry should be used for any exception thrown in this range of addresses — so we’ll catch everything. Also, to make it easy to do math, we are going to make load_base to 0. Finally, we have to set p_eh_frame_hdr pointer to the fake Elf32_Phdr struct. We will put this fake phdr struct right after our fake cache_entry struct that we are currently building. The rest of the fields are not really used (for our purpose), so we can put dummy values.

This gives us this:

1

2

3

4

5

6

7

*frame_hdr_cache_head:

|pc_low=0x00000000

|pc_high=0xFFFFFFFF

|load_base=0x00000000

|p_eh_frame_hdr=0x40025034

|p_dynamic=0x43434343

|link=0x00000000

For p_eh_frame_hdr struct, we only care about p_vaddr which is used to calculate hdr (unw_eh_frame_hdr).

So, this payload basically lets us to execute
gotofound; code (unwind-dw2-fde-dip.c:225) since the
data->pc will be in between pc_low and pc_high.

Then, on line 315, hdr is calculated by adding p_eh_frame_hdr->p_vaddr and load_base, thus pointing 0x40025054.
Time to build a fake hdr struct!

4-3. hdr & table construction

Starting at +0x54 from our buffer comes the hdr struct.
It’s a 4 byte struct and we fill in reasonable values here, according to the encoding scheme mentioned above.

1

2

3

4

5

*hdr:

|version=0x01

|eh_frame_ptr_enc=0x1b(DW_EH_PE_pcrel|DW_EH_PE_sdata4)

|fde_count_enc=0x03(DW_EH_PE_absptr|DW_EH_PE_udata4)

|table_enc=0x3b(DW_EH_PE_datarel|DW_EH_PE_sdata4)

Then, as we saw earlier, eh_frame is read. Since the value is supposedly encoded with
(DW_EH_PE_pcrel|DW_EH_PE_sdata4) , this value in our data should be an offset from where the hdr is. However, the value of eh_frame isn’t really related to what we do, so we can put any value (read_encoded_value_with_base actually does the calculation given the base to correctly compute eh_frame’s value).

Ok, next check is the following:

1

2

if(hdr->fde_count_enc!=DW_EH_PE_omit

&&hdr->table_enc==(DW_EH_PE_datarel|DW_EH_PE_sdata4))

We have picked the values for encoding schems such that we satisfy both conditions.
Then, fde_count is read.
Since we do not want to create more than one set of fake structs (to be searched with binary search later), we will force this to be 1.

Then, the table comes next. fde_table struct has two fields: initial_loc and fde.

As mentioned earlier, in order for the search to succeed, we need to satisfy
table[mid].initial_loc+data_base<=data->pc<table[mid].initial_loc+data_base+range .

Note that data_base is pointing at hdr (0x40025054). So we can set initial_loc to 0xBFFDAFAC such that
initial_loc+data_base==0x40025054+0xBFFDAFAC==0x0 .

Also, the fde field is actually an (signed) offset from hdr — due to (DW_EH_PE_datarel | DW_EH_PE_sdata4) encoding. So, we set it to 0x14 to indicate that our fake dwarf_fde struct will be located at 0x40025068.

The current payload, when fed to the program, will result in a crash since it will read an invalid value for the range.
To make
data->pc<initial_loc+data_base+range true, we need to construct a fake dwarf_fde now.

4-4. fde & cie construction

As a final step, we are going to construct fde and cie records in our payload.

dwarf_fde struct has length, CIE_delta, and pc_begin fields (followed by fde_augmentation length, which should be 0).

We are going to make the length0x1C, and CIE_delta to 0xFFFFFFE4 (such that
&CIE_delta-CIE_delta==0x40025088 — this will be explained later). We will set pc_begin to 0x0 (doesn’t really matter what we put here).

What comes after pc_begin is the range. To explain a little bit, on line 412 in unwind-dw2-fde-dip.c, range is read from f->pc_begin[f_enc_size] where f_enc_size is 4, making the 4 byte right after pc_begin be the range. Since we made the init_loc to be 0x0, we will make the range to be 0xFFFFFFFF. Then, we pad the last few bytes (so, technically we can fix the length, but that’s what we used during the competition).

Above payload will result in
data->ret to contain a pointer to our FDE struct and return to _Unwind_Find_FDE.

In _Unwind_Find_FDE, nothing interesting happens, and the same (a pointer to our fake FDE struct) is returned.

We are now back to uw_frame_state_for function (line 1180 in unwind-dw2.c). Since fde is not null, extract_cie_info is called with the cie pointer that is based on our fde.

unwind-dw2.c

C

1

2

3

4

5

6

7

1195

1196cie=get_cie(fde);

1197insn=extract_cie_info(cie,context,fs);

1198if(insn==NULL)

1199/* CIE contained unknown augmentation. */

1200return_URC_FATAL_PHASE1_ERROR;

1201

unwind-dw2-fde.h

152

153

154

155

156

157

158

/* Locate the CIE for a given FDE. */

staticinline conststructdwarf_cie *

get_cie(conststructdwarf_fde *f)

{

return(constvoid*)&f->CIE_delta-f->CIE_delta;

}

Looking at the get_cie function, we can see why we put 0xFFFFFFE4 for CIE_delta value in our FDE struct. With our setup, get_cie will return the CIE struct’s address, which will be right after our fake FDE struct (aka 0x40025088).

Now, we have 1 final function that we need to understand: extract_cie_info.

This function is mostly parsing stuff and filling in the _Unwind_Frame_State data based on the CIE record.

Data that follows after augmentation string (code_alignment, data_alignment, return_addr_col) are read in first.
We chose these values just because we saw these in normal CIE struct, but it shouldn’t matter what the values are.

Then, the rest of the data is parsed as augmentation contents (aka ‘zPLR’).

If the first byte is ‘z’, it sets
fs->saw_z flag and note that the length of the extra augmentation data (which follows the length itself) is 0x07.

‘P’ indicates a personality routine is specified in CIE (extra) augmentation, and basically read the personality_ptr value (4-byte) based on the personality_enc encoding scheme — which we set as 0x0 to make it absptr type.

‘L’ indicates a byte showing how the LSDA pointer is encoded. No idea what that is, but it’s not relevant — we put 0x0.

‘R’ indicates a byte indicating how FDE addresses are encoded. We put some sane value that we saw earlier, but shouldn’t matter either.

Alright, now with some padding bytes to make the total length 0x1c, we are set.

6 Responses

Hi, thanks for your wonderful writeup!
Would you mind if I ask you couple questions?
I’m new to DWARF and I don’t understand how you analyze DW_OP_bra,
“the number of bytes of the DWARF expression to skip forward or backward”, it says in the documentation.
DW_OP_bra 50 , DW_OP_bra 29 and DW_OP_bra 8 all jumped to END in your example,
but I couldn’t figure out how to calculate that T_T.
Also, how do you find out that when you satisfy all conditions,
the SIGSEV exception handler dumps to 0x08048CE8? Thank you very much :)

Sure thing!
DW_OP_bra is a branch expression which tells you how many bytes of DWARF expression it needs to skip. So if you count the bytes from each of branch instruction, you can see that it’s pointing to the END in my example. We just found out it dumps you there since you see ‘nested’ printed out and set a breakpoint to double check.